-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Field API: move validation to the field type #73642
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
|
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. |
| } else if ( isValid?.min && validity?.min ) { | ||
| customValidity = validity.min; | ||
| } else if ( isValid?.max && validity?.max ) { | ||
| customValidity = validity.max; | ||
| } else if ( isValid?.minLength && validity?.minLength ) { | ||
| customValidity = validity.minLength; | ||
| } else if ( isValid?.maxLength && validity?.maxLength ) { | ||
| customValidity = validity.maxLength; |
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 something we missed doing in #73465
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 function is a bit weird, how does one combine multiple validation rules. Also should this be called getFieldValidity or getCombinedValidity.
It's also possible that I don't really understand what this function does.
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 function just returns the message to be displayed in the control. It's a single message. It could be refactored to return early if it found a message as well.
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.
What happens if the validation fails on multiple constraints, shouldn't we combine the message? Should we clarify in the name that it returns a message
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's only one message per field, we return early when some constraint failed. This early return prevents triggering multiple network requests (custom validation) when they are not used.
| elements: true, | ||
| custom: ( item: any, normalizedField ) => { | ||
| const value = normalizedField.getValue( { item } ); | ||
| function isValidCustom< Item >( item: Item, field: NormalizedField< 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.
This isValidCustom function is the same as isValid.custom before. It's just unwrapped. The same for every other field type.
| }; | ||
|
|
||
| function isInvalidForRequired( fieldType: string | undefined, value: any ) { | ||
| if ( |
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.
All this logic now lives in specific is-valid-* rules in the field types.
packages/dataviews/src/field-types/utils/is-valid-max-length.ts
Outdated
Show resolved
Hide resolved
packages/dataviews/src/field-types/utils/is-valid-required-for-bool.ts
Outdated
Show resolved
Hide resolved
packages/dataviews/src/field-types/utils/is-valid-required-for-array.ts
Outdated
Show resolved
Hide resolved
| hasElements: boolean; | ||
| sort: ( a: Item, b: Item, direction: SortDirection ) => number; | ||
| isValid: Rules< Item >; | ||
| isValid: NormalizedRules< 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.
isValid not a great name. I also think that "defining the supported constraints" like the field type does, and defining the constraints for a given field shouldn't be the same type. I'm going to do a separate comment that clarifies all what I have in mind as it's now a bit scattered in multiple comments.
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 think it's time to revisit some Field API prop names, specially now that we want to stabilize parts of it. It's grown organically, it was driven by dozens of people, and it's showing a bit.
|
Now that I understand this a bit better, I'm ready to do an alternative proposal which I think would bring clarity IMO. Right now this PR doesn't separate between how a field define its constraints, and how a field type define which constraints are supported. It tries to be smart by making the fieldType take a field object and return the used constraints by merging what the field defines as constraint with some generic function that a field type can used to "override/enhance" what the field defines. This works but IMO, is not really clear and makes our architecture a bit cumbersome. Here's my alternative proposal: I'm going to take how a list of constraints is defined today max:
| false
| {
constraint: any;
validate: (
item: Item,
field: NormalizedField< Item >
) => boolean;
};The ambiguity today, is that this is both how a field configure the constraint but also how it may define how the constraint works (the validation function). Which means in theory, a field can override how "required" works by providing a "validate" function for required. I'm going to propose that we separate these two things. A field only defines the configuration for the constraints: type ValidationConstraintConfig = any;
type ValidationConstraintConfigs = Record< string, ValidationConstraintConfig >;
interface Field {
isValid: ValidationConstraintConfigs;
}A field type on the other side, doesn't define the "config" of a constraint, it defines the constraint itself, a validation function, a function that takes the item, the configuration of the constraint (potentially the field for extra information) and returns the validation result. type ValidationConstraint = ( item, field: Field, constraintConfig: ValidationConstraintConfig ) => ValidationResult
}
interface FieldType {
constraints: ValidationConstraint[]
}now, come const required = ( item, field, constraintConfig ) => {
if ( !! constraintConfig && field.getValue( item ) === undefined ) {
return 'field is required'; // Potentially these messages can be extracted out of the validation function into a "messages" property per validation constraint
}
return true;
}
// ....What about "custom". It's the same, it's just another validation constraint const custom = ( item, field, constraintConfig ) => {
return constraintConfig( item, field );
}I hope this clarifies a bit how I see this evolving and scaling. |
|
Ok thinking more, I changed my mind on whether the |
No, it can't. But I think it's nice that it'd be easy to do if we wanted.
The fields define the constraints: export type Rules< Item > = {
required?: boolean;
elements?: boolean;
pattern?: string;
minLength?: number;
maxLength?: number;
min?: number;
max?: number;
custom?:
| ( ( item: Item, field: NormalizedField< Item > ) => null | string )
| ( (
item: Item,
field: NormalizedField< Item >
) => Promise< null | string > );
};And the field types define the constraints and how they are validated: type NormalizedRule< Item > = {
constraint: any;
validate: ( item: Item, field: NormalizedField< Item > ) => boolean;
};
export type NormalizedRules< Item > = {
required?: NormalizedRule< Item >;
elements?: NormalizedRule< Item >;
pattern?: NormalizedRule< Item >;
minLength?: NormalizedRule< Item >;
maxLength?: NormalizedRule< Item >;
min?: NormalizedRule< Item >;
max?: NormalizedRule< Item >;
custom?:
| ( ( item: Item, field: NormalizedField< Item > ) => null | string )
| ( (
item: Item,
field: NormalizedField< Item >
) => Promise< null | string > );
};So there's already a separation for this. Though, it can be made clearer than it is right now. It relates to this conversation #73642 (comment) I won't be able to push more changes today, but this is the next thing I'll address. |
248cf60 to
ec31378
Compare
8811469 to
fc6cfff
Compare
|
Sorry for the massive ping, folks 🙏 I had an issue with merge/rebase. |
|
While testing this, I found an issue with |
|
This is ready now. |
Part of #73548
What?
This PR refactors validation so that logic lives in the field type and not in the
useFormValidityhook.Why?
By holding the validation logic in the field type, the
useFormValidityhook is field type agnostic. This means we can support registering custom field types. See #73548How?
getIsValidfunction, as( field: Field< Item > ) => NormalizedRules< Item >.NormalizedRulestype with all the rules. Each rule (required, min, max, etc.) can be:false: if the field does not support this rulemin: 5,required: true) and the validate function (which previously lived inuseFormValidate)Testing Instructions
npm run storybook:dev) and go to "DataViews > DataForm > Validation".