Skip to content

Conversation

oandregal
Copy link
Member

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 the required validation rule has been implemented in this PR:

{
  isValid: {
    required: true
  }
}

Fields that declare a type (email, integer, and boolean) 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:

{
  type: 'email',
  isValid: {
    required: true
  }
}

Fields that define their own Edit component have access to the validation rules:

{
  id: 'customField',
  Edit: ( { field }) => {
	  return <input required={ !! field.isValid.required } />
  }
}

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:

{
  isValid: {
    required: true,
    custom: ( item: Item, field: Field<Item> ) => {

      /* If the field is invalid, return an error message as string. */ 
      if ( /* field value is invalid */ ) {
        returns 'Field must conform to the rule';
      }
      
      /* Otherwise, return null. */
      return null;
    }
  }
}

Code changes:

  • Field API: the existing isValid function is replaced by an object with rules that consumers can enable/disable. There's also a custom rule for fields to implement their own validation logic.
  • The existing isValid functions in the Field Type Definitions are moved to isValid.custom.
  • @wordpress/components exposes via privateApis the following components: ValidatedTextControl, ValidatedNumberControl, and ValidatedToggleControl in the @wordpress/components package. The existing DataForm controls are updated to use them, and they all support require and custom validation messages.
  • Introduces a new story for validation (check DataForm > Validation) and it allows users to test all controls in a required and not required status.
  • Updates the isItemValid function to consider the new require rule.

How to test

npm install && npm run storybook:dev

Wait for the storybook to open and visit "DataForm > Validation".

@oandregal oandregal self-assigned this Jul 24, 2025
@oandregal oandregal added [Type] Feature New feature to highlight in changelogs. [Package] DataViews /packages/dataviews labels Jul 24, 2025
@oandregal oandregal force-pushed the update/dataform-validation-required branch from 8006f78 to 104ee7f Compare July 24, 2025 22:44
@@ -1,15 +1,15 @@
/**
* WordPress dependencies
*/
import { isEmail } from '@wordpress/url';
Copy link
Member Author

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 )
Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member Author

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

Comment on lines +31 to +45
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;
}
}
Copy link
Member Author

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.

Copy link
Contributor

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?

Copy link
Member Author

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.

Comment on lines +47 to +52
if (
typeof field.isValid.custom === 'function' &&
field.isValid.custom( item, field ) !== null
) {
return false;
}
Copy link
Member Author

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 >[] = [
Copy link
Member Author

@oandregal oandregal Jul 24, 2025

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 );
Copy link
Member Author

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.

Copy link
Contributor

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 }
Copy link
Member Author

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.

Copy link
Contributor

@mcsf mcsf left a 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 ) &&
Copy link
Contributor

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.

Comment on lines +33 to +37
( field.type === 'text' && isEmptyNullOrUndefined( value ) ) ||
( field.type === 'email' && isEmptyNullOrUndefined( value ) ) ||
( field.type === 'integer' &&
isEmptyNullOrUndefined( value ) ) ||
( field.type === undefined && isEmptyNullOrUndefined( value ) )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Factorable

Copy link
Member Author

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 );
Copy link
Contributor

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).

@oandregal oandregal marked this pull request as ready for review July 28, 2025 07:15
@oandregal oandregal requested a review from ajitbohra as a code owner July 28, 2025 07:15
Copy link

github-actions bot commented Jul 28, 2025

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 props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

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>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@oandregal oandregal force-pushed the update/dataform-validation-required branch 2 times, most recently from 4e2ccaa to 33b3ed6 Compare July 28, 2025 07:24
Copy link
Contributor

@gigitux gigitux left a 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 >;
Copy link
Contributor

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?

Copy link
Member Author

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.

Comment on lines +31 to +45
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;
}
}
Copy link
Contributor

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?

@oandregal oandregal force-pushed the update/dataform-validation-required branch from 33b3ed6 to 0a013ad Compare July 28, 2025 15:20
@oandregal
Copy link
Member Author

@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.

@oandregal oandregal force-pushed the update/dataform-validation-required branch from 0a013ad to 263ff42 Compare July 29, 2025 07:04
@oandregal oandregal force-pushed the update/dataform-validation-required branch from 263ff42 to acd1618 Compare July 29, 2025 14:34
@@ -15,6 +15,9 @@ const orderField: Field< BasePost > = {
label: __( 'Order' ),
description: __( 'Determines the order of pages.' ),
filterBy: false,
isValid: {
Copy link
Member Author

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

@oandregal oandregal merged commit 72b7587 into trunk Jul 29, 2025
71 of 83 checks passed
@oandregal oandregal deleted the update/dataform-validation-required branch July 29, 2025 16:25
@github-actions github-actions bot added this to the Gutenberg 21.4 milestone Jul 29, 2025
adamsilverstein pushed a commit to adamsilverstein/gutenberg that referenced this pull request Jul 31, 2025
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] DataViews /packages/dataviews [Type] Feature New feature to highlight in changelogs.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants