Skip to content

Conversation

@straku
Copy link
Contributor

@straku straku commented Sep 11, 2025

Closes #70957

What and why?

Adds setValue alongside existing getValue to the Field API. It improves/fixes few cases:

How?

  • Introduces a new setValue method in the Field API
  • Updates all the DataForm controls to use it for preparing the data for the onChange and custom validation isValid.custom callbacks
  • Updates readme and adds new stories to highlight this new functionality

Testing Instructions

DataForm panel layout displayed as modal fix

  1. Run storybook (npm run storybook:dev)
  2. Open DataViews > DataForm > Layout Panel story
  3. Set openAs argument to modal
  4. Click on some of the fields and verify that the modal is shown for each
  5. Do some edits within the modal inputs, but instead of clicking "Apply" in the modal, close it with "Cancel" or click outside
  6. Verify that the edits were not applied
  7. Repeat edits, but this time around click "Apply" in the modal
  8. Verify that the edits were applied

Validation regression testing

  1. Run storybook (npm run storybook:dev)
  2. Open DataViews > DataForm > Validation
  3. Provide unexpected inputs (or no inputs since all the fields there are required) and verify that validation works as you would expect

Dataviews filtering

  1. Run storybook (npm run storybook:dev)
  2. Open DataViews > DataViews > Default
  3. Check whether filtering by "Title" and "Description" works as you would expect

New functionality

  1. Run storybook (npm run storybook:dev)
  2. Open DataViews > DataForm > Data Adapter
  3. Play around with the stories provided there, verify that they are functional, and that the descriptions are clear and use cases make sense

@github-actions github-actions bot added the First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository label Sep 11, 2025
@github-actions
Copy link

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

@oandregal oandregal added [Type] Bug An existing feature does not function as intended [Feature] DataViews Work surrounding upgrading and evolving views in the site editor and beyond labels Sep 11, 2025
Comment on lines 38 to 54
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 ] );
Copy link
Member

@oandregal oandregal Sep 12, 2025

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.Edit needs to work with partial Items, as well as field.getValue, field.isValid.custom, and, potentially, any other field API.
  • At this point, we can't pass an Item to field.setValue because we don't have it. We can decide how we mark this: either as undefined Item or an empty object (an extreme case of a partial Item).

Comment on lines 37 to 74
/*
* 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 ] );
Copy link
Member

Choose a reason for hiding this comment

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

@straku I pushed 83db790 as per our conversations. It seems to work nicely, and so we can continue working with Item data (instead of partials) as parameter to getValue/setValue.

@oandregal
Copy link
Member

@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 fields packages (to adapt custom Edit controls to the required changes in onChange): I wonder if there is another way to set the type that doesn't require this.

@oandregal
Copy link
Member

One thing I'm not happy with yet is the required changes in the fields packages (to adapt custom Edit controls to the required changes in onChange): I wonder if there is another way to set the type that doesn't require this.

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.

@oandregal
Copy link
Member

I've rebased this from trunk, as they were many changes (new controls, some file renames, validation, etc.). Direction seems good and main use cases are working, but all controls and stories need a net review (e.g., update the isValid.custom call to use the setValue, review stories, etc.).

@oandregal oandregal changed the title Dataviews: Add setValue Field API: add setValue Sep 12, 2025
Comment on lines 65 to 73
// 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,
};
Copy link
Contributor Author

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.

@straku
Copy link
Contributor Author

straku commented Sep 15, 2025

update the isValid.custom call to use the setValue

This is implemented in 07a130b

Comment on lines 486 to 624
const customPasswordRule = ( value: DeepPartial< ValidatedItem > ) => {
if ( ! value.password ) {
return 'Password is required.';
}

Copy link
Contributor Author

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.

Copy link
Member

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

@straku straku force-pushed the straku/add-set-value branch from 07a130b to 712d0ab Compare September 15, 2025 15:11
@straku straku marked this pull request as ready for review September 15, 2025 15:12
@github-actions
Copy link

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: straku <[email protected]>
Co-authored-by: oandregal <[email protected]>
Co-authored-by: chihsuan <[email protected]>

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

required?: boolean;
custom?: ( item: Item, field: NormalizedField< Item > ) => null | string;
custom?: (
item: DeepPartial< Item >,
Copy link
Member

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 } ),
Copy link
Contributor Author

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.

Copy link
Member

@oandregal oandregal Sep 16, 2025

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: [
Copy link
Member

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

Comment on lines +88 to +97
setValue: ( { value } ) => ( {
author: Number( value ),
} ),
Copy link
Member

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.

Copy link
Contributor Author

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

@oandregal oandregal Sep 16, 2025

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

Copy link
Contributor Author

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
@straku straku force-pushed the straku/add-set-value branch from e17a2c8 to eb9be76 Compare September 16, 2025 11:23
Copy link
Member

@oandregal oandregal left a 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!

@oandregal oandregal merged commit 45f5bab into WordPress:trunk Sep 16, 2025
69 checks passed
@github-actions github-actions bot added this to the Gutenberg 21.7 milestone Sep 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] DataViews Work surrounding upgrading and evolving views in the site editor and beyond First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DataViews: user-input filters don't work with fields that use a getValue function

2 participants