-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Field API: add setValue
#71604
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
Field API: add setValue
#71604
Conversation
|
👋 Thanks for your first Pull Request and for helping build the future of Gutenberg and WordPress, @straku! In case you missed it, we'd love to have you join us in our Slack community. If you want to learn more about WordPress development in general, check out the Core Handbook full of helpful information. |
| const data = useMemo( () => { | ||
| return ( view.filters ?? [] ).reduce( | ||
| ( acc, f ) => { | ||
| acc[ f.field ] = f.value; | ||
| ( acc, activeFilter ) => { | ||
| const activeFilterField = fields.find( | ||
| ( f ) => f.id === activeFilter.field | ||
| ); | ||
| if ( activeFilterField ) { | ||
| acc = activeFilterField.setValue( { | ||
| item: undefined, | ||
| value: activeFilter.value, | ||
| } ); | ||
| } | ||
| return acc; | ||
| }, | ||
| {} as Record< string, any > | ||
| ); | ||
| }, [ view.filters ] ); | ||
| }, [ fields, view.filters ] ); |
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 tricky bit.
We are reusing the Edit controls for both DataForm and DataViews filters. At this point, we build an Item that we pass to the field.Edit below: the Item will be filled with the values of the filter, to populate the control when users open it.
This means a few things:
- As a result of reusing the controls in both DataForm and DataViews filters,
field.Editneeds to work with partial Items, as well asfield.getValue,field.isValid.custom, and, potentially, any other field API. - At this point, we can't pass an Item to
field.setValuebecause we don't have it. We can decide how we mark this: either asundefinedItem or an empty object (an extreme case of a partial Item).
| /* | ||
| * We are reusing the field.Edit component for filters. By doing so, | ||
| * we get for free a filter control specific to the field type | ||
| * and other aspects of the field API (Edit control configuration, etc.). | ||
| * | ||
| * This approach comes with an issue: the field.Edit controls work with getValue | ||
| * and setValue methods, which take an item (Item) as parameter. But, at this point, | ||
| * we don't have an item and we don't know how to create one, either. | ||
| * | ||
| * So, what we do is to prepare the data and the relevant field configuration | ||
| * as if Item was a plain object whose keys are the field ids: | ||
| * | ||
| * { | ||
| * [ fieldOne.id ]: value, | ||
| * [ fieldTwo.id ]: value, | ||
| * } | ||
| * | ||
| */ | ||
| const field = useMemo( () => { | ||
| const currentField = fields.find( ( f ) => f.id === filter.field ); | ||
| if ( currentField ) { | ||
| return { | ||
| ...currentField, | ||
| // Deactivate validation for filters. | ||
| isValid: { | ||
| required: false, | ||
| custom: () => null, | ||
| }, | ||
| // Configure getValue/setValue as if Item was a plain object. | ||
| getValue: ( { item }: { item: any } ) => { | ||
| return item[ currentField.id ]; | ||
| }, | ||
| setValue: ( { item, value }: { item: any; value: any } ) => { | ||
| return { | ||
| ...item, | ||
| [ currentField.id ]: value, | ||
| }; | ||
| }, | ||
| }; | ||
| } | ||
| return currentField; | ||
| }, [ fields, filter.field ] ); |
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.
0edf08c to
494d736
Compare
|
@straku I've pushed a fix for the modal at 7429de1 and updated the story so it reflects the use cases we talked about (nested data, data<=>UI control, and derived data) at 494d736. I think this is getting together nicely. One thing I'm not happy with yet is the required changes in the |
66ab8bf to
0768548
Compare
I've got this sorted out. I updated the DeepPartial definition, because it had an intersection with the Item so that all the props were actually required. Before: export type DeepPartial< T > = {
[ P in keyof T ]?: T[ P ] extends object ? DeepPartial< T[ P ] > : T[ P ];
} & T;After: export type DeepPartial< T > = {
[ P in keyof T ]?: T[ P ] extends object ? DeepPartial< T[ P ] > : T[ P ];
};By fixing the type, no changes are required in custom Edit controls. |
|
I've rebased this from |
| // Configure getValue/setValue as if Item was a plain object. | ||
| getValue: ( { item }: { item: any } ) => { | ||
| return item[ currentField.id ]; | ||
| }, | ||
| setValue: ( { item, value }: { item: any; value: any } ) => { | ||
| return { | ||
| ...item, | ||
| [ currentField.id ]: 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 am a little bummed about the usage of any here, but it will be difficult to avoid if we keep reusing the controls for both filtering and direct DataForm usage.
This is implemented in 07a130b |
| const customPasswordRule = ( value: DeepPartial< ValidatedItem > ) => { | ||
| if ( ! value.password ) { | ||
| return 'Password is 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.
@oandregal I wasn't sure how I felt about the ergonomics of having the item wrapped with DeepPartial here, but it opens up a custom way to validate isRequired as presented here. Previously with value typed as ValidatedItem, value.password would be incorrectly always present (it doesn't infer anything from isRequired being set to true or false). After the change, you need to handle it explicitly.
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'm pushing a change to prevent this from being required in the custom rule for educational purposes (it's beneficial to use the required check for that purpose because that triggers browser-level validation).
07a130b to
712d0ab
Compare
|
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. |
packages/dataviews/src/types.ts
Outdated
| required?: boolean; | ||
| custom?: ( item: Item, field: NormalizedField< Item > ) => null | string; | ||
| custom?: ( | ||
| item: DeepPartial< 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.
The consumer may need access to the full Item. For example, in scenarios where items are dependent on each other — as in country/postal code.
If we are introducing breaking changes (need to mark them as such by adding it to the proper section of the packages/dataviews/changelog), we may as well change custom to receive an object with field and value. We'd need to look at specific examples of custom in use to decide if we add item now or leave that for later.
| const onValidateControl = useCallback( | ||
| ( newValue: any ) => { | ||
| const message = field.isValid?.custom?.( | ||
| setValue( { item: data, value: 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.
@oandregal In regards to this comment. Maybe we should just pass data here? We are also passing a field to custom validation function so users will be able to use getValue and setValue that's there.
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 go back and forth on this one. I think having the full item may be beneficial, and it's how we have been operating so far. So, yeah, let's continue to do that.
Edit: I've found examples of this in use and some need the whole Item.
| { | ||
| id: 'revenue', | ||
| label: 'Revenue', | ||
| children: [ |
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 example would be improved when this PR lands, and we can set a summary (revenue.total).
| setValue: ( { value } ) => ( { | ||
| author: Number( 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.
This work, though I wonder if the control should actually be smarter and return a number instead of a string when the field type is integer.
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.
Agree. Do you want to account for it here or in a follow-up? I verified that the behavior of the story is the same on trunk vs this branch.
| // type is used only for filtering functionality and is not directly | ||
| // exposed to users using DataForm. DataForm expects onChange to follow | ||
| // the shape of the data passed to it, filtering is more liberal. | ||
| ( newValue: any ) => { |
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 the type here [ number, number ]? And accounting for the empty value (I suppose it's undefined?).
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.
Integer component props are using DataFormControlProps< Item > (the same as every other control). It doesn't know what to do with a more specific [ number, number ] type here; that's why I am loosening it up with any.
We can try changing the props for the whole component, but the way it's all wired up, it will be challenging to have something custom just for this one alone.
* add setValue to types * provide a fallback aligned with getValue if not set * modify filters logic * modify Text component
This reverts commit 129f0b1.
e17a2c8 to
eb9be76
Compare
oandregal
left a comment
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 working as expected in the tests I've done.
Thanks for bootstrapping, collaborating, and pushing this one forward. It's a tricky and important one. Congrats on your first PR!
Closes #70957
What and why?
Adds
setValuealongside existinggetValueto the Field API. It improves/fixes few cases:getValuefunction #70957)DataFormmodal panel #71384 - describes the problem in detail and offers an alternative fix that was closed in favor of this approach)DataForm(see readme changes and new story for examples of that)How?
setValuemethod in the Field APIonChangeand custom validationisValid.customcallbacksTesting Instructions
DataForm panel layout displayed as modal fix
npm run storybook:dev)openAsargument tomodalValidation regression testing
npm run storybook:dev)Dataviews filtering
npm run storybook:dev)New functionality
npm run storybook:dev)