Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
000595e
Add support for validating elements
elazzabi Aug 13, 2025
cf015c5
Add validation input and how to
elazzabi Aug 13, 2025
14e0308
Add array validation with required param
elazzabi Aug 15, 2025
59ade44
Update validation logic to check if the token matches any available e…
elazzabi Aug 15, 2025
1db0a20
Enhance validation logic to support array types by ensuring all value…
elazzabi Aug 15, 2025
b7b321a
Add tests for array and text field validations and validating against…
elazzabi Aug 15, 2025
81d355f
Add categories field to DataFormValidationComponent with validation r…
elazzabi Aug 18, 2025
b3b0c28
Add ValidatedFormTokenControl component to enhance array input valida…
elazzabi Aug 22, 2025
382cb4c
Enhance array validation logic to ensure elements are only checked if…
elazzabi Aug 22, 2025
203af53
Refactor array control to support internationalization and improve va…
elazzabi Aug 22, 2025
a65e3b0
Add countries field to DataFormValidationComponent with validation ru…
elazzabi Aug 22, 2025
538fc3a
Merge branch 'trunk' into add/elements-validation
elazzabi Sep 10, 2025
8d5d551
Expose ValidatedFormTokenField from private API
elazzabi Sep 10, 2025
0559f4c
Remove customValidation
elazzabi Sep 10, 2025
b3b0024
Import ValidatedFormTokenField and use it in Array
elazzabi Sep 10, 2025
fc42406
Add displayTransform and __experimentalRenderItem
elazzabi Sep 11, 2025
6d527cd
Replace arrays for display const and remove unused findElementByValue
elazzabi Sep 11, 2025
3f625d8
Adapt suggestions to pass values
elazzabi Sep 11, 2025
53c83fd
Add customValidity and onFocus
elazzabi Sep 11, 2025
822e31c
Add validation with validateTokens
elazzabi Sep 11, 2025
d24449b
Rename function
elazzabi Sep 11, 2025
6617584
Add changelogs
elazzabi Sep 11, 2025
75a0a5f
Change changelog for components
elazzabi Sep 11, 2025
fe03b3d
Call onInputChange after reseting the input
elazzabi Sep 11, 2025
6ef2a57
Pass an onInputChange instead of onFocus
elazzabi Sep 11, 2025
1a94d66
Merge branch 'add/elements-validation' of https://github.com/elazzabi…
elazzabi Sep 11, 2025
f2e301d
Remove validation for elements as it is replaced by control-level val…
elazzabi Sep 11, 2025
27536bc
Add documentation for elements
elazzabi Sep 11, 2025
bc66855
Merge branch 'trunk' into add/elements-validation
elazzabi Sep 18, 2025
300841d
Add changes to use the new getValue/setValue API
elazzabi Sep 18, 2025
08f9b87
Remove validateToken from onChange
elazzabi Sep 18, 2025
a9872b2
Remove customInvalidMessage and messages
elazzabi Sep 18, 2025
32b0d52
Fix linting
elazzabi Sep 18, 2025
5ed466c
Moves changelog entries to Unreleased
elazzabi Sep 18, 2025
20410e4
Remove input logic
oandregal Sep 19, 2025
87c94f6
Update message by including tokens
elazzabi Sep 19, 2025
a4b1a3a
Merge branch 'trunk' into add/elements-validation
elazzabi Sep 23, 2025
ce7465f
Change order of changelog to reflect the timeline
elazzabi Sep 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- `TextareaControl`: Add default resize: vertical rule ([#71736](https://github.com/WordPress/gutenberg/pull/71736)).

### Internal

- Expose `ValidatedFormTokenField` via private APIs [#71194](https://github.com/WordPress/gutenberg/pull/71194)

## 30.4.0 (2025-09-17)

### Bug Fixes
Expand Down
2 changes: 2 additions & 0 deletions packages/components/src/private-apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
ValidatedToggleControl,
ValidatedToggleGroupControl,
} from './validated-form-controls';
import { ValidatedFormTokenField } from './validated-form-controls/components/form-token-field';
import { Picker } from './color-picker/picker';

export const privateApis = {};
Expand All @@ -49,4 +50,5 @@ lock( privateApis, {
ValidatedTextareaControl,
ValidatedToggleControl,
ValidatedToggleGroupControl,
ValidatedFormTokenField,
} );
3 changes: 2 additions & 1 deletion packages/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Introduce a new `DataViewsPicker` component. [#70971](https://github.com/WordPress/gutenberg/pull/70971) and [#71836](https://github.com/WordPress/gutenberg/pull/71836).
- DataForm: Add support for elements validation in array fields [#71194](https://github.com/WordPress/gutenberg/pull/71194)

### Bug Fixes

Expand Down Expand Up @@ -32,7 +33,7 @@
- DataViews: support groupBy in the list layout. [#71548](https://github.com/WordPress/gutenberg/pull/71548)
- DataForm: support validation in select control [#71665](https://github.com/WordPress/gutenberg/pull/71665)
- DataForm: support validation in toggleGroup control. ([#71666](https://github.com/WordPress/gutenberg/pull/71666))
- DataForm: Add object configuration support for Edit property with some options. ([#71582](https://github.com/WordPress/gutenberg/pull/71582))
- DataForm: Add object configuration support for Edit property with some options. ([#71582](https://github.com/WordPress/gutenberg/pull/71582))
- DataForm: Add summary field support for composed fields. ([#71614](https://github.com/WordPress/gutenberg/pull/71614))
- DataForm: update radio control to support `required` and `custom` validation. [#71664](https://github.com/WordPress/gutenberg/pull/71664)

Expand Down
1 change: 1 addition & 0 deletions packages/dataviews/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,7 @@ Example:
Object that contains the validation rules for the field. If a rule is not met, the control will be marked as invalid and a message will be displayed.

- `required`: boolean indicating whether the field is required or not.
- `elements`: boolean restricting selection to the provided list of elements only. Used with the `array` field type.
- `custom`: a function that validates a field's value. If the value is invalid, the function should return a string explaining why the value is invalid. Otherwise, the function must return null.

Example:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,8 @@ const ValidationComponent = ( {
integer: number;
boolean: boolean;
customEdit: string;
categories: string[];
countries: string[];
password: string;
toggle?: boolean;
toggleGroup?: string;
Expand All @@ -541,6 +543,8 @@ const ValidationComponent = ( {
color: '#ff6600',
integer: 2,
boolean: true,
categories: [ 'astronomy' ],
countries: [ 'us' ],
customEdit: 'custom control',
password: 'secretpassword123',
toggle: undefined,
Expand Down Expand Up @@ -755,6 +759,41 @@ const ValidationComponent = ( {
custom: maybeCustomRule( customBooleanRule ),
},
},
{
id: 'categories',
type: 'array' as const,
label: 'Categories',
isValid: {
required,
},
elements: [
{ value: 'astronomy', label: 'Astronomy' },
{ value: 'book-review', label: 'Book review' },
{ value: 'event', label: 'Event' },
{ value: 'photography', label: 'Photography' },
{ value: 'travel', label: 'Travel' },
],
},
{
id: 'countries',
label: 'Countries Visited',
type: 'array' as const,
placeholder: 'Select countries',
description: 'Countries you have visited',
isValid: {
required,
elements: true,
},
elements: [
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'fr', label: 'France' },
{ value: 'de', label: 'Germany' },
{ value: 'jp', label: 'Japan' },
{ value: 'au', label: 'Australia' },
],
},
{
id: 'customEdit',
label: 'Custom Control',
Expand Down Expand Up @@ -812,6 +851,8 @@ const ValidationComponent = ( {
'color',
'integer',
'boolean',
'categories',
'countries',
'toggle',
'toggleGroup',
'password',
Expand Down
177 changes: 137 additions & 40 deletions packages/dataviews/src/dataform-controls/array.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
/**
* External dependencies
*/
import deepMerge from 'deepmerge';

/**
* WordPress dependencies
*/
import { FormTokenField } from '@wordpress/components';
import { useCallback, useMemo } from '@wordpress/element';
import { privateApis } from '@wordpress/components';
import { useCallback, useMemo, useState } from '@wordpress/element';
import { _n, sprintf } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import type { DataFormControlProps } from '../types';
import { unlock } from '../lock-unlock';

const { ValidatedFormTokenField } = unlock( privateApis );

export default function ArrayControl< Item >( {
data,
Expand All @@ -18,66 +27,154 @@ export default function ArrayControl< Item >( {
const { label, placeholder, elements, getValue, setValue } = field;
const value = getValue( { item: data } );

const findElementByValue = useCallback(
( suggestionValue: string ) => {
return elements?.find(
( suggestion ) => suggestion.value === suggestionValue
);
},
[ elements ]
);
const [ customValidity, setCustomValidity ] = useState<
| {
type: 'validating' | 'valid' | 'invalid';
message: string;
}
| undefined
>( undefined );

const findElementByLabel = useCallback(
( suggestionLabel: string ) => {
return elements?.find(
( suggestion ) => suggestion.label === suggestionLabel
);
},
[ elements ]
);

// Ensure value is an array
const arrayValue = useMemo(
// Convert stored values to element objects for the token field
const arrayValueAsElements = useMemo(
() =>
Array.isArray( value )
? value.map( ( token ) => {
const tokenLabel = findElementByValue( token )?.label;
return tokenLabel || token;
const element = elements?.find(
( suggestion ) => suggestion.value === token
);
return element || { value: token, label: token };
} )
: [],
[ value, findElementByValue ]
[ value, elements ]
);

const onChangeControl = useCallback(
( tokens: ( string | { value: string } )[] ) => {
// Convert TokenItem objects to strings
const stringTokens = tokens.map( ( token ) => {
if ( typeof token !== 'string' ) {
const validateTokens = useCallback(
( tokens: ( string | { value: string; label?: string } )[] ) => {
// Extract actual values from tokens for validation
const tokenValues = tokens.map( ( token ) => {
if ( typeof token === 'object' && 'value' in token ) {
return token.value;
}
return token;
} );

// First, check if elements validation is required and any tokens are invalid
if ( field.isValid?.elements && elements ) {
const invalidTokens = tokenValues.filter( ( tokenValue ) => {
return ! elements.some(
( element ) => element.value === tokenValue
);
} );

if ( invalidTokens.length > 0 ) {
setCustomValidity( {
type: 'invalid',
message: sprintf(
/* translators: %s: list of invalid tokens */
_n(
'Please select from the available options: %s is invalid.',
'Please select from the available options: %s are invalid.',
invalidTokens.length
),
invalidTokens.join( ', ' )
),
} );
return;
}
}

// Then check custom validation if provided.
if ( field.isValid?.custom ) {
const result = field.isValid?.custom?.(
deepMerge(
data,
setValue( {
item: data,
value: tokenValues,
} ) as Partial< Item >
),
field
);

if ( result ) {
setCustomValidity( {
type: 'invalid',
message: result,
} );
return;
}
}

const tokenByLabel = findElementByLabel( token );
// If no validation errors, clear custom validity
setCustomValidity( undefined );
},
[ elements, data, field, setValue ]
);

return tokenByLabel?.value || token;
const onChangeControl = useCallback(
( tokens: ( string | { value: string; label?: string } )[] ) => {
const valueTokens = tokens.map( ( token ) => {
if ( typeof token === 'object' && 'value' in token ) {
return token.value;
}
// If it's a string, it's either a new suggestion value or user input
return token;
} );

onChange( setValue( { item: data, value: stringTokens } ) );
onChange( setValue( { item: data, value: valueTokens } ) );
},
[ onChange, setValue, data, findElementByLabel ]
[ onChange, setValue, data ]
);

return (
<FormTokenField
<ValidatedFormTokenField
required={ !! field.isValid?.required }
onValidate={ validateTokens }
customValidity={ customValidity }
label={ hideLabelFromVision ? undefined : label }
value={ arrayValue }
value={ arrayValueAsElements }
onChange={ onChangeControl }
placeholder={ placeholder }
suggestions={
elements?.map( ( suggestion ) => suggestion.label ) ?? []
}
suggestions={ elements?.map( ( element ) => element.value ) }
__experimentalValidateInput={ ( token: string ) => {
// If elements validation is required, check if token is valid
if ( field.isValid?.elements && elements ) {
return elements.some(
( element ) =>
element.value === token || element.label === token
);
}

// For non-elements validation, allow all tokens
return true;
} }
__experimentalExpandOnFocus={ elements && elements.length > 0 }
__next40pxDefaultSize
__nextHasNoMarginBottom
__experimentalShowHowTo={ ! field.isValid?.elements }
displayTransform={ ( token: any ) => {
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 decided to use the displayTransform instead of using the label to token conversion, given that it's fragile.

// For existing tokens (element objects), display their label
if ( typeof token === 'object' && 'label' in token ) {
return token.label;
}
// For suggestions (value strings), find the corresponding element and show its label
if ( typeof token === 'string' && elements ) {
const element = elements.find(
( el ) => el.value === token
);
return element?.label || token;
}
return token;
} }
__experimentalRenderItem={ ( { item }: { item: any } ) => {
// Custom rendering for suggestion items (item is a value string)
if ( typeof item === 'string' && elements ) {
const element = elements.find(
( el ) => el.value === item
);
return <span>{ element?.label || item }</span>;
}
return <span>{ item }</span>;
} }
/>
);
}
8 changes: 0 additions & 8 deletions packages/dataviews/src/field-types/array.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,6 @@ const arrayFieldType: FieldTypeDefinition< any > = {
return __( 'Every value must be a string.' );
}

if ( field?.elements ) {
const validValues = field.elements.map( ( f ) => f.value );
if (
! value.every( ( v: any ) => validValues.includes( v ) )
) {
return __( 'Value must be one of the elements.' );
}
}
return null;
},
},
Expand Down
Loading
Loading