-
Notifications
You must be signed in to change notification settings - Fork 4.5k
DataForm: bootstrap validation (required and typechecks) #70901
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
8006f78
to
104ee7f
Compare
@@ -1,15 +1,15 @@ | |||
/** | |||
* WordPress dependencies | |||
*/ | |||
import { isEmail } from '@wordpress/url'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are some differences between this utility and how the HTML standard expect inputs of type email to be validated (see). I've updated this to follow the standard, which is how the UI components at @wordpress/components
work as well.
One notable difference is that the standard (and the UI components) consider a@b
a valid email, while the isEmail
utility doesn't. If we wanted to keep using isEmail
, we'd need to look at how the UI components's behavior can be overridden.
onChange( { | ||
[ id ]: Number( newValue ), | ||
} ), | ||
[ id ]: [ undefined, '', null ].includes( newValue ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We were converting empty values to 0.
There's an additional bug here that I haven't fixed in this PR. We are assuming the new value will be stored under data.id, but it doesn't need to be that way, even if that's the most common scenario. We have getValue
precisely to take the data from any part of the object (e.g., the field wich title
id can take its value from item.title.rendered
). We need a setValue
equivalent as well. This can be a follow-up PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, we can update onChange
to only rely the updated value, not the whole item.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've tracked this issue at #70957
if ( field.isValid.required ) { | ||
if ( | ||
( field.type === 'text' && isEmptyNullOrUndefined( value ) ) || | ||
( field.type === 'email' && isEmptyNullOrUndefined( value ) ) || | ||
( field.type === 'integer' && | ||
isEmptyNullOrUndefined( value ) ) || | ||
( field.type === undefined && isEmptyNullOrUndefined( value ) ) | ||
) { | ||
return false; | ||
} | ||
|
||
if ( field.type === 'boolean' && value !== true ) { | ||
return false; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the implementation of the isValid.required
rule.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it better move in a dedicated function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can extract when we have more rules.
if ( | ||
typeof field.isValid.custom === 'function' && | ||
field.isValid.custom( item, field ) !== null | ||
) { | ||
return false; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This essentially covers what was isValid
before in the Field Type Definitions (type checks, mostly).
customValidation: 'potato', | ||
} ); | ||
|
||
const _fields: Field< ValidatedItem >[] = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is one field per each type that supports validation so far, plus specific cases for when a field provides a custom Edit
or isValid.custom
.
], | ||
}; | ||
|
||
const canSave = isItemValid( post, _fields, form ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Riad mentioned that instead of using this model, we could rely the errors from the onChange
method of DataForm. I think that's interesting, and it's relevant to discuss together with this other aspect: validation may be implemented at form and/or field level.
- Form-level validation: we need this to implement cross-field validation – a field needs to follow some rule depending on the value of another field. For example, the valid values for the "province" field depend on the "country" field that is selected.
- Field-level validation: type checks. A field of type
email
should always be validated for that format, we shouldn't require the form to provide that info we already have.
I don't think we need to act on this right now, but wanted to share to be vigilant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that some cross-field model should be explored.
- Form-level validation: we need this to implement cross-field validation – a field needs to follow some rule depending on the value of another field. For example, the valid values for the "province" field depend on the "country" field that is selected.
I'd just add that this sounds like more than just validation, no? It's about letting fields react more broadly to the form data, including updating their own elements set (in this example, updating the available provinces for the current country).
@@ -17,7 +20,21 @@ export default function Boolean< Item >( { | |||
const { id, getValue, label } = field; | |||
|
|||
return ( | |||
<ToggleControl | |||
<ValidatedToggleControl | |||
required={ !! field.isValid.required } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way the ValidatedToggleControl
works is this: when it is required, the only valid value is true
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this direction.
const value = field.getValue( { item } ); | ||
|
||
if ( | ||
! [ undefined, '', null ].includes( value ) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should standardise this exemption for empty values. Sounds like it could be error-prone.
( field.type === 'text' && isEmptyNullOrUndefined( value ) ) || | ||
( field.type === 'email' && isEmptyNullOrUndefined( value ) ) || | ||
( field.type === 'integer' && | ||
isEmptyNullOrUndefined( value ) ) || | ||
( field.type === undefined && isEmptyNullOrUndefined( value ) ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Factorable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you expand this comment?
], | ||
}; | ||
|
||
const canSave = isItemValid( post, _fields, form ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that some cross-field model should be explored.
- Form-level validation: we need this to implement cross-field validation – a field needs to follow some rule depending on the value of another field. For example, the valid values for the "province" field depend on the "country" field that is selected.
I'd just add that this sounds like more than just validation, no? It's about letting fields react more broadly to the form data, including updating their own elements set (in this example, updating the available provinces for the current country).
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.
To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
4e2ccaa
to
33b3ed6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @oandregal for working on this! I left a note about the isValid change. Should we support the isValid: () => ...
format as well? Otherwise, this would be a breaking change for any consumers currently using the isValid callback.
For the rest, I'm fine with merging this PR! 🙇
@@ -116,7 +112,7 @@ export type FieldTypeDefinition< Item > = { | |||
/** | |||
* Callback used to validate the field. | |||
*/ | |||
isValid: ( item: Item, context?: ValidationContext ) => boolean; | |||
isValid: Rules< Item >; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it a breaking change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is. I've documented it in the Changelog as such and updated the README as well 0a013ad
I considered retrofitting the existing isValid
function as isValid.custom
but there were a few things I didn't like:
- Doing so overrides the existing
custom
validation provided by the field type definitions (e.g.,integer
). - We'll need to wrap that function with a generic message when it fails, such as "Field is invalid". Not a great experience.
All in all, we're iterating now on a different validation model, so I thought it'd be best to communicate that clearly. I don't have a strong feeling either way, and can add those back-compat changes if necessary.
if ( field.isValid.required ) { | ||
if ( | ||
( field.type === 'text' && isEmptyNullOrUndefined( value ) ) || | ||
( field.type === 'email' && isEmptyNullOrUndefined( value ) ) || | ||
( field.type === 'integer' && | ||
isEmptyNullOrUndefined( value ) ) || | ||
( field.type === undefined && isEmptyNullOrUndefined( value ) ) | ||
) { | ||
return false; | ||
} | ||
|
||
if ( field.type === 'boolean' && value !== true ) { | ||
return false; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it better move in a dedicated function?
33b3ed6
to
0a013ad
Compare
@mcsf @gigitux @youknowriad I'd like to land this to open iteration, unless there's something affecting public API that merits discussion or changing the approach. |
0a013ad
to
263ff42
Compare
263ff42
to
acd1618
Compare
@@ -15,6 +15,9 @@ const orderField: Field< BasePost > = { | |||
label: __( 'Order' ), | |||
description: __( 'Determines the order of pages.' ), | |||
filterBy: false, | |||
isValid: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before this PR, the integer
field type definition also checked that an integer wasn't an empty string, making it required. This is to make sure this interaction still works as before (setting the menu order for the pages screen in the site editor):
Screen.Recording.2025-07-29.at.16.27.48.mov
…0901) Co-authored-by: oandregal <oandregal@git.wordpress.org> Co-authored-by: mcsf <mcsf@git.wordpress.org> Co-authored-by: gigitux <gigitux@git.wordpress.org> Co-authored-by: youknowriad <youknowriad@git.wordpress.org>
What
This PR bootstraps basic validation (required, typechecks) for text, email, integer, and boolean field types in DataForm.
Screen.Recording.2025-07-25.at.00.29.40.mov
Why
We want to introduce validation.
How
Validation is declared in the Field API via the
isValid
object, and fields opt-in into the existing rules. Only therequired
validation rule has been implemented in this PR:Fields that declare a type (
email
,integer
, andboolean
) have automatic type checks, if they require so. For example, the following requires the field value not to be empty and to conform to the email standard:Fields that define their own
Edit
component have access to the validation rules:When a field wants to define its own validation mechanism, it can use the
custom
rule. If the field value doesn't conform to the custom rule, it must return a string which will be used by the UI control to display the error message:Code changes:
isValid
function is replaced by an object with rules that consumers can enable/disable. There's also acustom
rule for fields to implement their own validation logic.isValid
functions in the Field Type Definitions are moved toisValid.custom
.@wordpress/components
exposes via privateApis the following components:ValidatedTextControl
,ValidatedNumberControl
, andValidatedToggleControl
in the@wordpress/components
package. The existing DataForm controls are updated to use them, and they all supportrequire
andcustom
validation messages.isItemValid
function to consider the newrequire
rule.How to test
npm install && npm run storybook:dev
Wait for the storybook to open and visit "DataForm > Validation".