Skip to content

Commit

Permalink
add useAllFormData ui:option for ui:validations (#32791)
Browse files Browse the repository at this point in the history
* add useAllFormData ui:option for ui:validations
  • Loading branch information
rhasselle-oddball authored Nov 4, 2024
1 parent bac2287 commit fdd53c0
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 6 deletions.
8 changes: 5 additions & 3 deletions src/platform/forms-system/src/js/components/SchemaForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ class SchemaForm extends React.Component {
}

validate(formData, errors) {
const { schema, uiSchema, appStateData } = this.props;
const { schema, uiSchema, appStateData, getFormData } = this.props;
if (uiSchema) {
uiSchemaValidate(
errors,
Expand All @@ -175,6 +175,7 @@ class SchemaForm extends React.Component {
'',
null,
appStateData,
getFormData,
);
}
return errors;
Expand Down Expand Up @@ -227,7 +228,6 @@ class SchemaForm extends React.Component {
}

SchemaForm.propTypes = {
idSchema: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
schema: PropTypes.object.isRequired,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
Expand All @@ -239,8 +239,10 @@ SchemaForm.propTypes = {
editModeOnReviewPage: PropTypes.bool,
formContext: PropTypes.shape({}),
formOptions: PropTypes.shape({}),
getFormData: PropTypes.func,
hideTitle: PropTypes.bool,
pagePerItemIndex: PropTypes.number,
idSchema: PropTypes.object,
pagePerItemIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
reviewMode: PropTypes.bool,
safeRenderCompletion: PropTypes.bool,
onChange: PropTypes.func,
Expand Down
12 changes: 11 additions & 1 deletion src/platform/forms-system/src/js/containers/FormPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,15 @@ class FormPage extends React.Component {
}
};

formData = () => {
/**
* @param {Object} [options]
* @param {boolean} [options.all] If true, return the entire form data regardless of context
*/
formData = ({ all } = {}) => {
if (all) {
return this.props.form.data;
}

const { pageConfig } = this.props.route;
// If it's a CustomPage, return the entire form data
if (pageConfig.CustomPage && !pageConfig.customPageUsesPagePerItemData) {
Expand Down Expand Up @@ -340,6 +348,7 @@ class FormPage extends React.Component {
uploadFile={this.props.uploadFile}
schema={schema}
uiSchema={uiSchema}
getFormData={this.formData}
goBack={this.goBack}
goForward={this.onSubmit}
goToPath={this.goToPath}
Expand Down Expand Up @@ -374,6 +383,7 @@ class FormPage extends React.Component {
uiSchema={uiSchema}
pagePerItemIndex={params ? params.index : undefined}
formContext={formContext}
getFormData={this.formData}
trackingPrefix={this.props.form.trackingPrefix}
uploadFile={this.props.uploadFile}
onChange={this.onChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export default function ArrayBuilderItemPage({
uiSchema={uiSchema}
pagePerItemIndex={props.pagePerItemIndex}
formContext={props.formContext}
getFormData={props.getFormData}
trackingPrefix={props.trackingPrefix}
onChange={onChange}
onSubmit={onSubmit}
Expand Down Expand Up @@ -142,6 +143,7 @@ export default function ArrayBuilderItemPage({
PageContentBeforeButtons: PropTypes.node,
data: PropTypes.object,
formContext: PropTypes.object,
getFormData: PropTypes.func,
goBack: PropTypes.func,
goToPath: PropTypes.func,
onChange: PropTypes.func,
Expand Down
1 change: 1 addition & 0 deletions src/platform/forms-system/src/js/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@
* @property {boolean} [useVaCards] For arrays on a single page. If true, will use the `VaCard` component to wrap each item in the array. Has a white background with border instead of gray background.
* @property {boolean} [reflectInputError] Whether or not to add usa-input--error as class if error message is outside of component.
* @property {string} [reviewItemHeaderLevel] Optional level for the item-header on Review page - for arrays. Defaults to '5' for a <h5> header-tag.
* @property {boolean} [useAllFormData] `formData` will return all form data instead of just the current item in an array. Applicable to `ui:validations`. TODO other fields.
* @property {boolean} [useDlWrap] On the review page, moves \<dl\> tag to immediately surrounding the \<dt\> field instead of using a \<div\>. \<dt\> fields should be wrapped in \<dl\> fields, so this fixes that a11y issue. Formats fields horizontally.
* @property {'single' | 'multiple'} [useFormsPattern] Used if you want to define the formHeading and formDescription for the web component field, which can include JSX, so it can be read out by screen readers. Accepts 'single' for a single field on the page where the error will show on the entire block, or 'multiple' for multiple fields on the page where the error will show only on the field.
* @property {boolean} [useHeaderStyling] Enables developer to implement and use alternate style classes for auto generated html elements such as in ObjectField or ArrayField
Expand Down
17 changes: 15 additions & 2 deletions src/platform/forms-system/src/js/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ export function transformErrors(errors, uiSchema) {
* @param {Object} formData The (flattened) data for the entire form
* @param {string} [path] The path to the current field relative to the root of the page.
* @param {number} [currentIndex] Used to select the correct field data to validate against
* @param {Object} [appStateData] Data from the app state
* @param {({ all }) => any} [getFormData] Function to get the form data. Useful if you need all form data
*/

export function uiSchemaValidate(
Expand All @@ -143,6 +145,7 @@ export function uiSchemaValidate(
path = '',
currentIndex = null,
appStateData,
getFormData,
) {
if (uiSchema && schema) {
const currentData = path !== '' ? get(path, formData) : formData;
Expand Down Expand Up @@ -170,6 +173,7 @@ export function uiSchemaValidate(
newPath,
index,
appStateData,
getFormData,
);
});
} else if (!uiSchema.items) {
Expand All @@ -195,19 +199,23 @@ export function uiSchemaValidate(
nextPath,
currentIndex,
appStateData,
getFormData,
);
});
}

const validations = uiSchema['ui:validations'];
const useAllFormData = uiSchema?.['ui:options']?.useAllFormData;
const data =
useAllFormData && getFormData ? getFormData({ all: true }) : formData;
if (validations && currentData !== undefined) {
validations.forEach(validation => {
const pathErrors = path ? get(path, errors) : errors;
if (typeof validation === 'function') {
validation(
pathErrors,
currentData,
formData,
data,
schema,
uiSchema['ui:errorMessages'],
currentIndex,
Expand All @@ -217,7 +225,7 @@ export function uiSchemaValidate(
validation.validator(
pathErrors,
currentData,
formData,
data,
schema,
uiSchema['ui:errorMessages'],
validation.options,
Expand Down Expand Up @@ -264,6 +272,7 @@ export function isValidForm(form, pageList, isTesting = false) {
);

const v = new Validator();
let getFormData;

return validPages.reduce(
({ isValid, errors }, page) => {
Expand Down Expand Up @@ -309,6 +318,9 @@ export function isValidForm(form, pageList, isTesting = false) {

const result = v.validate(formData, schema);

// mimics FormPage formData() function
getFormData = ({ all }) => (all ? form.data : formData);

if (result.valid) {
const customErrors = {};
uiSchemaValidate(
Expand All @@ -319,6 +331,7 @@ export function isValidForm(form, pageList, isTesting = false) {
'',
null,
appStateData,
getFormData,
);

return {
Expand Down
103 changes: 103 additions & 0 deletions src/platform/forms-system/test/js/validation.unit.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,109 @@ describe('Schemaform validations', () => {
),
).to.be.true;
});

it('can use global formData for validation on fields in array', () => {
const errors = {};
const validatorLocal = sinon.spy();
const validatorGlobal = sinon.spy();
const schemaArray = {
type: 'array',
items: [
{
properties: {
field1: {
type: 'string',
},
field2: {
type: 'string',
},
},
},
],
};
const schema = {
type: 'object',
properties: {
otherItem: {
type: 'string',
},
arrayItems: schemaArray,
},
};
const uiSchemaArray = {
items: {
field1: {
'ui:title': 'Field 1',
'ui:validations': [validatorLocal],
},
field2: {
'ui:title': 'Field 2',
'ui:validations': [validatorGlobal],
'ui:options': {
useAllFormData: true,
},
},
},
};
const uiSchema = {
otherItem: {
'ui:title': 'Other item',
},
arrayItems: uiSchemaArray,
};
const arrayFormData = [
{
field1: 'foo',
field2: 'bar',
},
];

const globalFormData = {
otherItem: 'test',
arrayItems: arrayFormData,
};

const getFormData = ({ all }) => (all ? globalFormData : arrayFormData);

uiSchemaValidate(
errors,
uiSchemaArray,
schemaArray,
arrayFormData,
undefined,
undefined,
undefined,
getFormData,
);

let v = validatorLocal.args[0];
expect(v[1]).to.equal('foo');
expect(v[2]).to.equal(arrayFormData);

[v] = validatorGlobal.args;
expect(v[1]).to.equal('bar');
expect(v[2]).to.equal(globalFormData);

uiSchemaValidate(
{},
uiSchema,
schema,
globalFormData,
undefined,
undefined,
undefined,
getFormData,
);

[v] = validatorLocal.args;
expect(v[1]).to.equal('foo');
expect(v[2]).to.equal(arrayFormData);

[v] = validatorGlobal.args;
expect(v[1]).to.equal('bar');
expect(v[2]).to.equal(globalFormData);
});

it('should skip validation when array is undefined', () => {
const errors = {};
const validator = sinon.spy();
Expand Down

0 comments on commit fdd53c0

Please sign in to comment.