Field validation
Integrating validator functions
Validator functions need to be included in an instance of a ChangesetWebform component in order for the corresponding validation rules to work. This can be done globally, or for each instance of the component.
Importing all of the default validators
The full set of default validators shipped by Ember Changeset Validations can be imported as below.
import defaultValidators from 'react-changeset-webforms/src/utils/default-validators.js';
Importing a subset of default validators
Note that we can also import a subset of validations, if we don't intend on using all of the default ones, and we'd prefer to avoid importing validators that we don't need.
The snippet below shows the full set of default validators imported in ember-changeset-webforms/utils/default-validators (Used above). Rather than importing the default validators, we could import a subset of those shown below.
import { validateDate, validatePresence, validateLength, validateNumber, validateFormat, validateInclusion, validateExclusion, validateConfirmation } from '../validators';
Importing custom validators
Custom validators can be imported from anywhere. See Integrating custom validators
Integrating validator functions app wide
Validators can be added at the app wide level in services/ember-changeset-webfornms.js as below. We first import the validators in the service as above and then add them to the changesetWebformsDefaults object as below.
validators: defaultValidators,
This would result in all of the default validators being available in all instances of the ChangesetWebform component throughout our app.
Integrating validator functions in an individual form
The example component below shows how the custom phoneNumber validator can be imported and then made available to a single instance of the ChangesetWebform component, by including it in formSchema.validations.
Note that if you've specified app wide validators, as in the above example, those will still be available to this instance of the ChangesetWebform component. This is why validatePresence still works, even though it is not included in formSchema.validations.
import React from 'react'; import ChangesetWebform from 'react-changeset-webforms/src/components/ChangesetWebform.jsx'; import validatePhoneNumber from '../../validators/phone-number'; const formSchema = { validators: { validatePhoneNumber, }, formSettings: { formName: 'Phone number with country code', }, fields: [ { fieldId: 'phoneNumber', fieldType: 'phoneNumberWithCountryCode', fieldLabel: 'Phone number', validationRules: [ { validationMethod: 'validatePresence', arguments: true, }, { validationMethod: 'validatePhoneNumber', }, ], }, ], }; export default function CustomFieldUsage() { const [phoneNumber, setPhoneNumber] = React.useState(null); function updatePhoneNumber(formField, changesetWebform) { if (changesetWebform.changeset.isValid) { setPhoneNumber(formField.fieldValue); } else { setPhoneNumber(null); } } return ( <> <ChangesetWebform formSchema={formSchema} onFieldValueChange={updatePhoneNumber} /> {phoneNumber && <p>The phone number entered is {phoneNumber}</p>} </> ); }
Defining validation rules
This addon uses Ember Changeset Validations to handle validation, and also as its default library of validators.
The Ember Changeset Validations usage documentation outlines how you create a validation map and then pass that map to the changeset generator, so that the validations are integrated into your changeset.
With React Changeset Webforms the importing of the validations library, construction of the validations map and creation of the changeset are handled for you.
You need only specify the validations that you'd like to apply to each field in the validationRules array.
Each item in a field's validationRules array is an object that must contain a validationMethod property, which must correspond to a validation rule in the Ember Changeset Validations validator api, or any custom validators that you have written (More on custom validators below).
Each item in a field's validationRules array may also include an arguments property, where you can pass the arguments relevant to the validator specified by the validationMethod.
Thus, the code below taken from the Ember Changeset Validations usage docs on create a validations map:
firstName: [validatePresence(true),validateLength({ min: 4}) ],would be expressed as the below when defining a formSchema for the changeset-webform component.
validationRules: [ {validationMethod: 'validatePresence',arguments: true, }, {validationMethod: 'validateLength',arguments: { min: 4}, }, ];The example below shows a basic implementation of the validatePresence, validateFormat and validateLength validators.
import ChangesetWebform from 'react-changeset-webforms/src/components/ChangesetWebform.jsx'; const formSchema = { formSettings: { formName: 'validationBasics', hideSubmitButton: true, }, fields: [ { fieldId: 'name', fieldType: 'input', fieldLabel: 'Name', validationRules: [ { validationMethod: 'validatePresence', arguments: { presence: true, }, }, { validationMethod: 'validateLength', arguments: { presence: true, }, }, ], }, { fieldId: 'email', fieldType: 'input', inputType: 'email', fieldLabel: 'Email', validatesOn: ['$inherited', 'insertWithValue'], validationRules: [ { validationMethod: 'validatePresence', arguments: { presence: true, }, validationMethod: 'validateFormat', arguments: { type: 'email', }, }, ], }, ], }; export default function ValidationBasics() { return <ChangesetWebform formSchema={formSchema} />; }
Validation events
In addition to defining validation rules, we can also configure which events should trigger field validation. The addon provides some sane defaults, so that we don't have to configure obvious validation events over and over.
Note that we're not referring to browser events here, but customised event names which the built in fields send as the first argument to the onUserInteraction action, when the user takes the related action.
Validation events are specified in the validatesOn property of a field.
Validation event names for built in fields
The snippet below shows all of the available event names which can be passed to the validatesOn array for a field.
Those which are shown under Included by addon defaults are included in the validatesOn array of the field's definition in addon config.
Such events will trigger validation by default, and do not need to be added to the validatesOn array for a field in our app of component config.
Those under Not included by addon defaults need to be included in app or component config if we would like them to trigger validation.
TODO InterpolatedSimpleJsSnippet
Customising validation events for a field
The example below shows three scenrios when defining validation events.
- The
namefield has novalidatesOnproperty, and so the field uses the defaults. Therefore, it validates onfocusOutbut notkeyUp. - The
emailfield overrides thevalidatesOnproperty. Therefore, it only validates onkeyUpand does not validate onfocusOut. - The
phoneNumberfield addskeyUpto thevalidatesOnproperty. This is achieved by including the srting$inheritedin the array forvalidatesOn. Therefore, it validates both onfocusOutandkeyUp.
import ChangesetWebform from 'react-changeset-webforms/src/components/ChangesetWebform.jsx'; const formSchema = { formSettings: { formName: 'validationEvents', hideSubmitButton: true, }, fields: [ { fieldId: 'name', fieldType: 'input', fieldLabel: 'Name', placeholder: 'Name (default validation events)', validationRules: [ { validationMethod: 'validatePresence', arguments: { presence: true, }, }, ], }, { fieldId: 'email', fieldType: 'input', inputType: 'email', fieldLabel: 'Email', placeholder: 'Email (Custom validation events, without $inherited)', validatesOn: ['keyUp'], validationRules: [ { validationMethod: 'validatePresence', arguments: { presence: true, }, }, { validationMethod: 'validateFormat', arguments: { type: 'email', }, }, ], }, { fieldId: 'phoneNumber', fieldType: 'input', fieldLabel: 'Phone number', placeholder: 'Phone number (Custom validation events, with $inherited)', validatesOn: ['$inherited', 'keyUp'], validationRules: [ { validationMethod: 'validatePresence', arguments: { presence: true, }, }, { validationMethod: 'validateLength', arguments: { min: 3, max: 15, }, }, ], }, ], }; export default function ValidationEvents() { return <ChangesetWebform formSchema={formSchema} />; }
The addon defaults outlined above can be overridden at the app level, or within a particular form schema. See Configuration options.
Forcing validation in an action
Under the hood, each field has property called eventLog, an array which is populated with the names of all the validation events which have occurred. For example when a user types in an input field, the field's eventLog property will have the strings focusIn, keyDown, and keyUp added to it.
If there is any intersection between the eventLog and validatesOn properties, the field's validation is activated.
The addon defaults include forceValidation in a field's validatesOn property.
Thus, you can forcibly activate a field's validation by pushing the string forceValidation into field.eventLog, as shown in terh updateNameField action in the example below.
import React from 'react'; import ChangesetWebform from 'react-changeset-webforms/src/components/ChangesetWebform.jsx'; const formSchema = { formSettings: { formName: 'forcingValidation', hideSubmitButton: true, }, fields: [ { fieldId: 'name', fieldType: 'input', fieldLabel: 'Name', validationRules: [ { validationMethod: 'validatePresence', arguments: { presence: true, }, }, ], }, ], }; export default function ForcingValidation() { const nameFieldRef = React.useRef(null); function afterGenerateChangesetWebform(changesetWebform) { nameFieldRef.current = changesetWebform.fields.find((field) => field.fieldId === 'name'); } function updateNameField() { nameFieldRef.current.eventLog.push('forceValidation'); nameFieldRef.current.updateValue('New Name'); } return ( <> <div className="border rounded p-2 mb-4 bg-light"> <b className="mb-2">These buttons are outside of the ChangesetWebform component</b> <div className="d-flex mt-2"> <button data-test-id="update-name-field" className="btn btn-primary me-2" type="button" onClick={updateNameField} > Update value of name field </button> </div> </div> <ChangesetWebform formSchema={formSchema} afterGenerateChangesetWebform={afterGenerateChangesetWebform} /> </> ); }
Using your own validation event names
The example below shows how you can trigger validation in customised ways by adding an event string to the validatesOn array of a field, and then pushing the same string into the eventLog array of the field when it should be validated.
In the example below, clicking the "Update value of name field" button updates the value of the name field, and validates the field by pushing the custom event name valueExternallyUpdated into eventLog. This works because valueExternallyUpdated is added to the the validatesOn property of the field.
import { useRef, useReducer } from 'react'; import ChangesetWebform from 'react-changeset-webforms/src/components/ChangesetWebform.jsx'; const formSchema = { formSettings: { formName: 'fieldMethods5', hideSubmitButton: true, }, fields: [ { fieldId: 'name', fieldType: 'input', fieldLabel: 'Name', validatesOn: ['$inherited', 'valueExternallyUpdated'], validationRules: [ { validationMethod: 'validatePresence', arguments: { presence: true, }, }, ], }, ], }; export default function FieldMethodsExampleFive() { const nameFieldRef = useRef(null); const [_, forceUpdate] = useReducer((n) => n + 1, 0); function afterGenerateChangesetWebform(changesetWebform) { nameFieldRef.current = changesetWebform.fields.find((field) => field.fieldId === 'name'); } function updateNameField() { nameFieldRef.current.eventLog.push('valueExternallyUpdated'); nameFieldRef.current.updateValue('New Name'); forceUpdate(); } return ( <> <div className="border rounded p-2 mb-4 bg-light"> <b className="mb-2">These buttons are outside of the ChangesetWebform component</b> <div className="d-flex mt-2"> <button data-test-id="update-name-field" className="btn btn-primary me-2" type="button" onClick={updateNameField} > Update value of name field </button> </div> </div> <ChangesetWebform formSchema={formSchema} afterGenerateChangesetWebform={afterGenerateChangesetWebform} /> </> ); }
Setting custom validity on DOM elements
Browsers will automatically apply the valid and invalid pseudo classes to form elements where appropriate. For example, if an input field with type=email is updated with an invalid email address, the input will then have the invalid pseudo class, and will be selectable with :invalid.
DOM elements have a built in setCustomValidity method
See https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/setCustomValidity
data-set-custom-validity
Example
import ChangesetWebform from 'react-changeset-webforms/src/components/ChangesetWebform.jsx'; const formSchema = { formSettings: { formName: 'Signup', submitButtonText: 'Sign up', clearFormAfterSubmit: true, resetFormButton: true, clearFormButton: true, }, fields: [ { fieldId: 'name', fieldLabel: 'Name', fieldType: 'input', showValidationWhenFocussed: true, validationRules: [ { validationMethod: 'validatePresence', arguments: true, }, ], inputType: 'text', }, { fieldId: 'email', fieldLabel: 'Email', fieldType: 'input', validatesOn: ['$inherited', 'insertWithValue'], validationRules: [ { validationMethod: 'validatePresence', arguments: true, }, { validationMethod: 'validateFormat', arguments: { type: 'email' }, }, ], inputType: 'email', }, { fieldId: 'recoveryEmail', fieldLabel: 'Recovery email', fieldType: 'input', validatesOn: ['$inherited', 'insertWithValue'], validationRules: [ { validationMethod: 'validatePresence', arguments: true, }, { validationMethod: 'validateFormat', arguments: { type: 'email' }, }, ], inputType: 'email', }, { fieldId: 'password', fieldLabel: 'Password (Minimum 8 characters)', fieldType: 'input', validationRules: [ { validationMethod: 'validatePresence', arguments: true, }, { validationMethod: 'validateLength', arguments: { min: 8, max: 72 }, }, ], inputType: 'password', }, { fieldId: 'acceptTerms', fieldType: 'radioButtonGroup', fieldLabel: 'Do you agree to the terms and conditions?', validationRules: [ { validationMethod: 'validateInclusion', arguments: { list: ['true'], message: 'You must accept the terms to continue.', }, }, ], options: [ { label: 'I agree', value: 'true', }, { label: 'I do not agree', value: 'false', }, ], }, { fieldId: 'confirmHuman', fieldType: 'singleCheckbox', fieldLabel: 'Are you a human?', checkBoxLabel: 'Are you human', validationRules: [ { validationMethod: 'validatePresence', arguments: { presence: true, message: 'Please confirm that you are not a robot.', }, }, ], }, { fieldId: 'cookieConsent', fieldType: 'checkboxGroup', fieldLabel: 'Please select the cookies you consent to', validationRules: [ { validationMethod: 'validateLength', arguments: { min: 2, allowNone: false, message: 'You must select at least two cookie consent options.', }, }, ], options: [ { label: 'Essential', key: 'essential', }, { label: 'Analytics', key: 'analytics', }, { label: 'Marketing', key: 'marketing', }, ], }, ], }; function submit() { return new Promise((resolve) => { setTimeout(() => { resolve(); }, 500); }); } export default function SignupForm() { return ( <ChangesetWebform formSchema={formSchema} submitData={submit} data={{ email: 'tobias@bluthcompany.com', recoveryEmail: 'test' }} /> ); }