From 377fe20d3c1f8609c31b78e2962ae8281e689dfa Mon Sep 17 00:00:00 2001 From: Luigi Teschio Date: Wed, 27 Nov 2024 14:25:44 +0100 Subject: [PATCH 1/5] Use DataForm to handle pattern creation --- packages/patterns/package.json | 1 + .../src/components/create-pattern-modal.js | 169 +++++++++++------- 2 files changed, 105 insertions(+), 65 deletions(-) diff --git a/packages/patterns/package.json b/packages/patterns/package.json index b0c6e81f1e2498..28b2c77446539e 100644 --- a/packages/patterns/package.json +++ b/packages/patterns/package.json @@ -40,6 +40,7 @@ "@wordpress/compose": "*", "@wordpress/core-data": "*", "@wordpress/data": "*", + "@wordpress/dataviews": "*", "@wordpress/element": "*", "@wordpress/html-entities": "*", "@wordpress/i18n": "*", diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js index dd86709acbca30..50f681009db6ac 100644 --- a/packages/patterns/src/components/create-pattern-modal.js +++ b/packages/patterns/src/components/create-pattern-modal.js @@ -4,7 +4,6 @@ import { Modal, Button, - TextControl, __experimentalHStack as HStack, __experimentalVStack as VStack, ToggleControl, @@ -14,6 +13,7 @@ import { useState } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { store as noticesStore } from '@wordpress/notices'; import { store as coreStore } from '@wordpress/core-data'; +import { DataForm, isItemValid } from '@wordpress/dataviews'; /** * Internal dependencies @@ -62,9 +62,11 @@ export function CreatePatternModalContents( { defaultSyncType = PATTERN_SYNC_TYPES.full, defaultTitle = '', } ) { - const [ syncType, setSyncType ] = useState( defaultSyncType ); - const [ categoryTerms, setCategoryTerms ] = useState( defaultCategories ); - const [ title, setTitle ] = useState( defaultTitle ); + const [ pattern, setPattern ] = useState( { + title: defaultTitle, + categoryTerms: defaultCategories, + sync: defaultSyncType, + } ); const [ isSaving, setIsSaving ] = useState( false ); const { createPattern } = unlock( useDispatch( patternsStore ) ); @@ -72,15 +74,77 @@ export function CreatePatternModalContents( { const { categoryMap, findOrCreateTerm } = useAddPatternCategory(); + const fields = [ + { + id: 'title', + label: __( 'Name' ), + type: 'text', + isValid: ( value ) => { + return value.title.length > 0; + }, + }, + { + id: 'categoryTerms', + label: __( 'Categories' ), + Edit: ( { field, data, onChange } ) => { + const { id } = field; + const categoryTerms = field.getValue( { item: data } ); + return ( + { + onChange( { + [ id ]: newValue, + } ); + } } + categoryMap={ categoryMap } + /> + ); + }, + }, + { + id: 'sync', + label: __( 'Synced' ), + Edit: ( { field, data, onChange } ) => { + const { id } = field; + const sync = field.getValue( { item: data } ); + return ( + { + onChange( { + [ id ]: + sync === PATTERN_SYNC_TYPES.full + ? PATTERN_SYNC_TYPES.unsynced + : PATTERN_SYNC_TYPES.full, + } ); + } } + /> + ); + }, + }, + ]; + + const form = { + fields: [ 'title', 'categoryTerms', 'sync' ], + }; + + const isFormValid = isItemValid( pattern, fields, form ); + async function onCreate( patternTitle, sync ) { - if ( ! title || isSaving ) { + if ( ! isFormValid || isSaving ) { return; } try { setIsSaving( true ); const categories = await Promise.all( - categoryTerms.map( ( termName ) => + pattern.categoryTerms.map( ( termName ) => findOrCreateTerm( termName ) ) ); @@ -103,71 +167,46 @@ export function CreatePatternModalContents( { onError?.(); } finally { setIsSaving( false ); - setCategoryTerms( [] ); - setTitle( '' ); } } return ( -
{ - event.preventDefault(); - onCreate( title, syncType ); - } } - > - - + { + setPattern( { + ...pattern, + ...newData, + } ); + } } + /> + + + > + { __( 'Cancel' ) } + - - - -
+ + + ); } From 114ecf1c9d15107e00194379714c6d3066ebe135 Mon Sep 17 00:00:00 2001 From: Luigi Teschio Date: Wed, 27 Nov 2024 19:04:28 +0100 Subject: [PATCH 2/5] Experiment - validation dataform --- .../dataviews/src/dataform-hooks/use-form.ts | 47 +++++++++++++++++++ .../dataforms-layouts/data-form-layout.tsx | 33 ++++++++++++- .../dataforms-layouts/is-combined-field.ts | 5 +- packages/dataviews/src/index.ts | 1 + .../dataviews/src/normalize-form-fields.ts | 21 +++++++-- packages/dataviews/src/types.ts | 17 ++++++- .../src/components/create-pattern-modal.js | 30 +++++++----- 7 files changed, 133 insertions(+), 21 deletions(-) create mode 100644 packages/dataviews/src/dataform-hooks/use-form.ts diff --git a/packages/dataviews/src/dataform-hooks/use-form.ts b/packages/dataviews/src/dataform-hooks/use-form.ts new file mode 100644 index 00000000000000..d7abfe29487e99 --- /dev/null +++ b/packages/dataviews/src/dataform-hooks/use-form.ts @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react'; +import { FormField } from '../types'; + +export const useForm = ( supportedFields: Record< string, FormField > ) => { + const [ form, setForm ] = useState( { + fields: supportedFields, + touchedFields: [] as string[], + errors: {}, + } ); + + const setTouchedFields = ( touchedFields: string[] ) => { + setForm( { + ...form, + touchedFields, + } ); + }; + + const setError = ( field: string, error: string ) => { + setForm( { + ...form, + errors: { + ...form.errors, + [ field ]: error, + }, + } ); + }; + + const isFormValid = () => { + return Object.entries( form.fields ).every( ( [ , field ] ) => { + if ( + field.validation.validateWhenDirty === true && + form.touchedFields.includes( field.id ) + ) { + return field.validation.callback().isValid; + } + + return field.validation.callback().isValid; + } ); + }; + + return { + ...form, + setTouchedFields, + setError, + isFormValid: isFormValid(), + }; +}; diff --git a/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx b/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx index 08cc47f569eafe..32d003459c4052 100644 --- a/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx +++ b/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx @@ -47,6 +47,9 @@ export function DataFormLayout< Item >( { [ form ] ); + // @ts-ignore + const { setTouchedFields, setError, touchedFields } = form; + return ( { normalizedFormFields.map( ( formField ) => { @@ -78,7 +81,35 @@ export function DataFormLayout< Item >( { key={ formField.id } data={ data } field={ formField } - onChange={ onChange } + onChange={ ( value ) => { + if ( ! touchedFields.includes( formField.id ) ) { + setTouchedFields( [ + // @ts-ignore + ...form.touchedFields, + formField.id, + ] ); + } + + if ( + ( formField.validation.validateWhenDirty && + // @ts-ignore + form.touchedFields.includes( + formField.id + ) ) || + ! formField.validation.validateWhenDirty + ) { + const { isValid, message } = + formField.validation.callback(); + + if ( ! isValid ) { + setError( formField.id, message ); + } + } + + onChange( value ); + } } + // @ts-ignore + message={ form.errors[ formField.id ] } /> ); } ) } diff --git a/packages/dataviews/src/dataforms-layouts/is-combined-field.ts b/packages/dataviews/src/dataforms-layouts/is-combined-field.ts index 3df6fdc60f906e..0855dbe4a5dac0 100644 --- a/packages/dataviews/src/dataforms-layouts/is-combined-field.ts +++ b/packages/dataviews/src/dataforms-layouts/is-combined-field.ts @@ -1,10 +1,11 @@ /** * Internal dependencies */ -import type { FormField, CombinedFormField } from '../types'; +import { NormalizedFormField } from '../normalize-form-fields'; +import type { FormField, CombinedFormField, NormalizedField } from '../types'; export function isCombinedField( - field: FormField + field: FormField | NormalizedFormField ): field is CombinedFormField { return ( field as CombinedFormField ).children !== undefined; } diff --git a/packages/dataviews/src/index.ts b/packages/dataviews/src/index.ts index 4ca0cf3db0bd3f..79b315ebbbd285 100644 --- a/packages/dataviews/src/index.ts +++ b/packages/dataviews/src/index.ts @@ -4,3 +4,4 @@ export { VIEW_LAYOUTS } from './dataviews-layouts'; export { filterSortAndPaginate } from './filter-and-sort-data-view'; export type * from './types'; export { isItemValid } from './validation'; +export { useForm } from './dataform-hooks/use-form'; diff --git a/packages/dataviews/src/normalize-form-fields.ts b/packages/dataviews/src/normalize-form-fields.ts index 3cd5f67564d7ce..b236e7633ebbf5 100644 --- a/packages/dataviews/src/normalize-form-fields.ts +++ b/packages/dataviews/src/normalize-form-fields.ts @@ -1,13 +1,13 @@ /** * Internal dependencies */ -import type { Form } from './types'; +import type { Form, FormFieldValidation } from './types'; -interface NormalizedFormField { +export type NormalizedFormField = { id: string; layout: 'regular' | 'panel'; labelPosition: 'side' | 'top' | 'none'; -} +} & { validation: FormFieldValidation }; export default function normalizeFormFields( form: Form @@ -20,12 +20,19 @@ export default function normalizeFormFields( const labelPosition = form.labelPosition ?? ( layout === 'regular' ? 'top' : 'side' ); - return ( form.fields ?? [] ).map( ( field ) => { + return Object.entries( form.fields ?? {} ).map( ( [ id, field ] ) => { if ( typeof field === 'string' ) { return { - id: field, + id, layout, labelPosition, + validation: { + callback: () => ( { + isValid: true, + message: '', + } ), + validateWhenDirty: false, + }, }; } @@ -35,8 +42,12 @@ export default function normalizeFormFields( ( fieldLayout === 'regular' ? 'top' : 'side' ); return { ...field, + id, layout: fieldLayout, labelPosition: fieldLabelPosition, + validation: { + ...field.validation, + }, }; } ); } diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 861dc53404f914..22b492b104d84c 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -542,7 +542,7 @@ export type SimpleFormField = { id: string; layout?: 'regular' | 'panel'; labelPosition?: 'side' | 'top' | 'none'; -}; +} & { validation: FormFieldValidation }; export type CombinedFormField = { id: string; @@ -550,10 +550,23 @@ export type CombinedFormField = { layout?: 'regular' | 'panel'; labelPosition?: 'side' | 'top' | 'none'; children: Array< FormField | string >; +} & { validation: FormFieldValidation }; + +export type FormFieldValidation = { + /** + * The validation message. + */ + validateWhenDirty: boolean; + /** + * The validation function. + */ + callback: () => { + isValid: boolean; + message: string; + }; }; export type FormField = SimpleFormField | CombinedFormField; - /** * The form configuration. */ diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js index 50f681009db6ac..691f51f8ce12b6 100644 --- a/packages/patterns/src/components/create-pattern-modal.js +++ b/packages/patterns/src/components/create-pattern-modal.js @@ -13,7 +13,7 @@ import { useState } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { store as noticesStore } from '@wordpress/notices'; import { store as coreStore } from '@wordpress/core-data'; -import { DataForm, isItemValid } from '@wordpress/dataviews'; +import { DataForm, isItemValid, useForm } from '@wordpress/dataviews'; /** * Internal dependencies @@ -27,6 +27,7 @@ import { store as patternsStore } from '../store'; import CategorySelector from './category-selector'; import { useAddPatternCategory } from '../private-hooks'; import { unlock } from '../lock-unlock'; +import { useEffect } from 'react'; export default function CreatePatternModal( { className = 'patterns-menu-items__convert-modal', @@ -74,14 +75,25 @@ export function CreatePatternModalContents( { const { categoryMap, findOrCreateTerm } = useAddPatternCategory(); + const form = useForm( { + title: { + validation: { + validateWhenDirty: true, + callback: () => { + return { + isValid: pattern.title.length > 0, + message: 'Title is required', + }; + }, + }, + }, + } ); + const fields = [ { id: 'title', label: __( 'Name' ), type: 'text', - isValid: ( value ) => { - return value.title.length > 0; - }, }, { id: 'categoryTerms', @@ -130,14 +142,10 @@ export function CreatePatternModalContents( { }, ]; - const form = { - fields: [ 'title', 'categoryTerms', 'sync' ], - }; - - const isFormValid = isItemValid( pattern, fields, form ); + // const isFormValid = isItemValid( pattern, fields, form ); async function onCreate( patternTitle, sync ) { - if ( ! isFormValid || isSaving ) { + if ( isSaving ) { return; } @@ -201,7 +209,7 @@ export function CreatePatternModalContents( { onClick={ async () => { await onCreate( pattern.title, pattern.sync ); } } - aria-disabled={ ! isFormValid } + aria-disabled={ false } isBusy={ isSaving } > { confirmLabel } From 46d683205793e2edb7fae7a1517829239712b19d Mon Sep 17 00:00:00 2001 From: Luigi Teschio Date: Thu, 28 Nov 2024 12:23:01 +0100 Subject: [PATCH 3/5] WIP --- .../dataviews/src/dataform-controls/text.tsx | 24 +++++--- .../dataviews/src/dataform-hooks/use-form.ts | 38 +++++++------ .../dataforms-layouts/data-form-layout.tsx | 55 ++++++++++++------- .../dataforms-layouts/is-combined-field.ts | 5 +- .../src/dataforms-layouts/panel/index.tsx | 7 +++ .../src/dataforms-layouts/regular/index.tsx | 3 + .../dataviews/src/normalize-form-fields.ts | 4 +- packages/dataviews/src/types.ts | 21 +++++-- .../fields/src/actions/duplicate-post.tsx | 6 +- packages/fields/src/actions/reorder-page.tsx | 4 +- .../src/components/create-pattern-modal.js | 29 +++++++--- 11 files changed, 123 insertions(+), 73 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/text.tsx b/packages/dataviews/src/dataform-controls/text.tsx index 7ac095f4abede7..52eb71de6f053b 100644 --- a/packages/dataviews/src/dataform-controls/text.tsx +++ b/packages/dataviews/src/dataform-controls/text.tsx @@ -14,6 +14,7 @@ export default function Text< Item >( { field, onChange, hideLabelFromVision, + errorMessage, }: DataFormControlProps< Item > ) { const { id, label, placeholder } = field; const value = field.getValue( { item: data } ); @@ -27,14 +28,19 @@ export default function Text< Item >( { ); return ( - + <> + + { errorMessage && ( +

{ errorMessage }

+ ) } + ); } diff --git a/packages/dataviews/src/dataform-hooks/use-form.ts b/packages/dataviews/src/dataform-hooks/use-form.ts index d7abfe29487e99..8822f09a3133d5 100644 --- a/packages/dataviews/src/dataform-hooks/use-form.ts +++ b/packages/dataviews/src/dataform-hooks/use-form.ts @@ -1,11 +1,20 @@ -import { useEffect, useState } from 'react'; -import { FormField } from '../types'; +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; -export const useForm = ( supportedFields: Record< string, FormField > ) => { +/** + * Internal dependencies + */ +import type { FormField } from '../types'; + +export const useForm = < Item >( + supportedFields: Record< string, FormField > +) => { const [ form, setForm ] = useState( { fields: supportedFields, touchedFields: [] as string[], - errors: {}, + messageErrors: {}, } ); const setTouchedFields = ( touchedFields: string[] ) => { @@ -15,33 +24,26 @@ export const useForm = ( supportedFields: Record< string, FormField > ) => { } ); }; - const setError = ( field: string, error: string ) => { + const setErrors = ( field: string, error: string | undefined ) => { setForm( { ...form, - errors: { - ...form.errors, + messageErrors: { + ...form.messageErrors, [ field ]: error, }, } ); }; - const isFormValid = () => { + const isFormValid = ( data: Item ) => { return Object.entries( form.fields ).every( ( [ , field ] ) => { - if ( - field.validation.validateWhenDirty === true && - form.touchedFields.includes( field.id ) - ) { - return field.validation.callback().isValid; - } - - return field.validation.callback().isValid; + return field.validation.callback( data ).isValid; } ); }; return { ...form, setTouchedFields, - setError, - isFormValid: isFormValid(), + setErrors, + isFormValid, }; }; diff --git a/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx b/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx index 32d003459c4052..8824726037ef91 100644 --- a/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx +++ b/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx @@ -2,7 +2,7 @@ * WordPress dependencies */ import { __experimentalVStack as VStack } from '@wordpress/components'; -import { useContext, useMemo } from '@wordpress/element'; +import { useContext, useEffect, useMemo } from '@wordpress/element'; /** * Internal dependencies @@ -28,6 +28,7 @@ export function DataFormLayout< Item >( { field: FormField; onChange: ( value: any ) => void; hideLabelFromVision?: boolean; + errorMessage: string | undefined; } ) => React.JSX.Element | null, field: FormField ) => React.JSX.Element; @@ -47,8 +48,20 @@ export function DataFormLayout< Item >( { [ form ] ); - // @ts-ignore - const { setTouchedFields, setError, touchedFields } = form; + const { setTouchedFields, setErrors, touchedFields, messageErrors } = form; + + useEffect( () => { + normalizedFormFields.forEach( ( formField ) => { + const { isValid, errorMessage } = formField.validation.callback( { + ...data, + } ); + if ( ! isValid ) { + setErrors( formField.id, errorMessage ); + } else { + setErrors( formField.id, undefined ); + } + } ); + }, [ data, normalizedFormFields, setErrors ] ); return ( @@ -81,6 +94,14 @@ export function DataFormLayout< Item >( { key={ formField.id } data={ data } field={ formField } + errorMessage={ + ( formField.validation.showErrorOnlyWhenDirty && + touchedFields.includes( formField.id ) ) || + ( ! formField.validation.showErrorOnlyWhenDirty && + messageErrors[ formField.id ] ) + ? messageErrors[ formField.id ] + : undefined + } onChange={ ( value ) => { if ( ! touchedFields.includes( formField.id ) ) { setTouchedFields( [ @@ -90,26 +111,18 @@ export function DataFormLayout< Item >( { ] ); } - if ( - ( formField.validation.validateWhenDirty && - // @ts-ignore - form.touchedFields.includes( - formField.id - ) ) || - ! formField.validation.validateWhenDirty - ) { - const { isValid, message } = - formField.validation.callback(); - - if ( ! isValid ) { - setError( formField.id, message ); - } - } - onChange( value ); + const { isValid, errorMessage } = + formField.validation.callback( { + ...data, + ...value, + } ); + if ( ! isValid ) { + setErrors( formField.id, errorMessage ); + } else { + setErrors( formField.id, undefined ); + } } } - // @ts-ignore - message={ form.errors[ formField.id ] } /> ); } ) } diff --git a/packages/dataviews/src/dataforms-layouts/is-combined-field.ts b/packages/dataviews/src/dataforms-layouts/is-combined-field.ts index 0855dbe4a5dac0..3df6fdc60f906e 100644 --- a/packages/dataviews/src/dataforms-layouts/is-combined-field.ts +++ b/packages/dataviews/src/dataforms-layouts/is-combined-field.ts @@ -1,11 +1,10 @@ /** * Internal dependencies */ -import { NormalizedFormField } from '../normalize-form-fields'; -import type { FormField, CombinedFormField, NormalizedField } from '../types'; +import type { FormField, CombinedFormField } from '../types'; export function isCombinedField( - field: FormField | NormalizedFormField + field: FormField ): field is CombinedFormField { return ( field as CombinedFormField ).children !== undefined; } diff --git a/packages/dataviews/src/dataforms-layouts/panel/index.tsx b/packages/dataviews/src/dataforms-layouts/panel/index.tsx index 269b2bb418a856..9411ba4caf5e82 100644 --- a/packages/dataviews/src/dataforms-layouts/panel/index.tsx +++ b/packages/dataviews/src/dataforms-layouts/panel/index.tsx @@ -66,6 +66,7 @@ function PanelDropdown< Item >( { data, onChange, field, + errorMessage, }: { fieldDefinition: NormalizedField< Item >; popoverAnchor: HTMLElement | null; @@ -73,6 +74,7 @@ function PanelDropdown< Item >( { data: Item; onChange: ( value: any ) => void; field: FormField; + errorMessage: string | undefined; } ) { const fieldLabel = isCombinedField( field ) ? field.label @@ -158,6 +160,7 @@ function PanelDropdown< Item >( { hideLabelFromVision={ ( form?.fields ?? [] ).length < 2 } + errorMessage={ errorMessage } /> ) } @@ -171,6 +174,7 @@ export default function FormPanelField< Item >( { data, field, onChange, + errorMessage, }: FieldLayoutProps< Item > ) { const { fields } = useContext( DataFormContext ); const fieldDefinition = fields.find( ( fieldDef ) => { @@ -221,6 +225,7 @@ export default function FormPanelField< Item >( { data={ data } onChange={ onChange } labelPosition={ labelPosition } + errorMessage={ errorMessage } /> @@ -237,6 +242,7 @@ export default function FormPanelField< Item >( { data={ data } onChange={ onChange } labelPosition={ labelPosition } + errorMessage={ errorMessage } /> ); @@ -259,6 +265,7 @@ export default function FormPanelField< Item >( { data={ data } onChange={ onChange } labelPosition={ labelPosition } + errorMessage={ errorMessage } /> diff --git a/packages/dataviews/src/dataforms-layouts/regular/index.tsx b/packages/dataviews/src/dataforms-layouts/regular/index.tsx index a3d90b807b5cd4..f7357872a22799 100644 --- a/packages/dataviews/src/dataforms-layouts/regular/index.tsx +++ b/packages/dataviews/src/dataforms-layouts/regular/index.tsx @@ -35,6 +35,7 @@ export default function FormRegularField< Item >( { field, onChange, hideLabelFromVision, + errorMessage, }: FieldLayoutProps< Item > ) { const { fields } = useContext( DataFormContext ); @@ -93,6 +94,7 @@ export default function FormRegularField< Item >( { key={ fieldDefinition.id } data={ data } field={ fieldDefinition } + errorMessage={ errorMessage } onChange={ onChange } hideLabelFromVision /> @@ -106,6 +108,7 @@ export default function FormRegularField< Item >( { ( { isValid: true, - message: '', + errorMessage: '', } ), - validateWhenDirty: false, + showErrorOnlyWhenDirty: true, }, }; } diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 22b492b104d84c..5c66fbff405c90 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -184,6 +184,7 @@ export type DataFormControlProps< Item > = { field: NormalizedField< Item >; onChange: ( value: Record< string, any > ) => void; hideLabelFromVision?: boolean; + errorMessage: string | undefined; }; export type DataViewRenderFieldProps< Item > = { @@ -552,18 +553,20 @@ export type CombinedFormField = { children: Array< FormField | string >; } & { validation: FormFieldValidation }; +export type ValidationResult = { + isValid: boolean; + errorMessage: string | undefined; +}; + export type FormFieldValidation = { /** - * The validation message. + * The validation should be triggered only when the field is dirty. */ - validateWhenDirty: boolean; + showErrorOnlyWhenDirty: boolean; /** * The validation function. */ - callback: () => { - isValid: boolean; - message: string; - }; + callback: ( data: any ) => ValidationResult; }; export type FormField = SimpleFormField | CombinedFormField; @@ -574,6 +577,11 @@ export type Form = { type?: 'regular' | 'panel'; fields?: Array< FormField | string >; labelPosition?: 'side' | 'top' | 'none'; + touchedFields: string[]; + messageErrors: Record< string, string | undefined >; + setTouchedFields: ( touchedFields: string[] ) => void; + setErrors: ( field: string, error: string | undefined ) => void; + isFormValid: ( data: Record< string, any > ) => boolean; }; export interface DataFormProps< Item > { @@ -588,4 +596,5 @@ export interface FieldLayoutProps< Item > { field: FormField; onChange: ( value: any ) => void; hideLabelFromVision?: boolean; + errorMessage: string | undefined; } diff --git a/packages/fields/src/actions/duplicate-post.tsx b/packages/fields/src/actions/duplicate-post.tsx index fd7e0ae9de4ad1..b66fba37500379 100644 --- a/packages/fields/src/actions/duplicate-post.tsx +++ b/packages/fields/src/actions/duplicate-post.tsx @@ -23,9 +23,6 @@ import type { BasePost, CoreDataError } from '../types'; import { getItemTitle } from './utils'; const fields = [ titleField ]; -const formDuplicateAction = { - fields: [ 'title' ], -}; const duplicatePost: Action< BasePost > = { id: 'duplicate-post', @@ -139,7 +136,8 @@ const duplicatePost: Action< BasePost > = { setItem( ( prev ) => ( { ...prev, diff --git a/packages/fields/src/actions/reorder-page.tsx b/packages/fields/src/actions/reorder-page.tsx index 1820884d8d8c73..068793d1ed8b83 100644 --- a/packages/fields/src/actions/reorder-page.tsx +++ b/packages/fields/src/actions/reorder-page.tsx @@ -39,7 +39,7 @@ function ReorderModal( { async function onOrder( event: React.FormEvent ) { event.preventDefault(); - + // @ts-ignore if ( ! isItemValid( item, fields, formOrderAction ) ) { return; } @@ -68,6 +68,7 @@ function ReorderModal( { } ); } } + // @ts-ignore const isSaveDisabled = ! isItemValid( item, fields, formOrderAction ); return (
@@ -80,6 +81,7 @@ function ReorderModal( { setItem( { diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js index 691f51f8ce12b6..7c184f0ffaf235 100644 --- a/packages/patterns/src/components/create-pattern-modal.js +++ b/packages/patterns/src/components/create-pattern-modal.js @@ -13,7 +13,7 @@ import { useState } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { store as noticesStore } from '@wordpress/notices'; import { store as coreStore } from '@wordpress/core-data'; -import { DataForm, isItemValid, useForm } from '@wordpress/dataviews'; +import { DataForm, useForm } from '@wordpress/dataviews'; /** * Internal dependencies @@ -27,7 +27,6 @@ import { store as patternsStore } from '../store'; import CategorySelector from './category-selector'; import { useAddPatternCategory } from '../private-hooks'; import { unlock } from '../lock-unlock'; -import { useEffect } from 'react'; export default function CreatePatternModal( { className = 'patterns-menu-items__convert-modal', @@ -78,11 +77,25 @@ export function CreatePatternModalContents( { const form = useForm( { title: { validation: { - validateWhenDirty: true, - callback: () => { + validateWhenDirty: false, + callback: ( data ) => { + if ( data.title.length === 0 ) { + return { + isValid: false, + errorMessage: 'Title is required', + }; + } + + if ( data.title.length > 5 ) { + return { + isValid: false, + errorMessage: 'Title is too long', + }; + } + return { - isValid: pattern.title.length > 0, - message: 'Title is required', + isValid: true, + errorMessage: undefined, }; }, }, @@ -142,8 +155,6 @@ export function CreatePatternModalContents( { }, ]; - // const isFormValid = isItemValid( pattern, fields, form ); - async function onCreate( patternTitle, sync ) { if ( isSaving ) { return; @@ -209,7 +220,7 @@ export function CreatePatternModalContents( { onClick={ async () => { await onCreate( pattern.title, pattern.sync ); } } - aria-disabled={ false } + aria-disabled={ ! form.isFormValid( pattern ) } isBusy={ isSaving } > { confirmLabel } From e01fb7f7aec7990ae6aabba0f574bfbabab35ad5 Mon Sep 17 00:00:00 2001 From: Luigi Teschio Date: Thu, 28 Nov 2024 14:31:09 +0100 Subject: [PATCH 4/5] fix naming --- packages/patterns/src/components/create-pattern-modal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js index 7c184f0ffaf235..ffceaa6289d0bc 100644 --- a/packages/patterns/src/components/create-pattern-modal.js +++ b/packages/patterns/src/components/create-pattern-modal.js @@ -77,7 +77,7 @@ export function CreatePatternModalContents( { const form = useForm( { title: { validation: { - validateWhenDirty: false, + showErrorOnlyWhenDirty: true, callback: ( data ) => { if ( data.title.length === 0 ) { return { From 7b8acfa1fd1474f52fb30d499866c96e977826e8 Mon Sep 17 00:00:00 2001 From: Luigi Teschio Date: Thu, 28 Nov 2024 15:10:05 +0100 Subject: [PATCH 5/5] fix performance issue --- .../dataviews/src/dataform-hooks/use-form.ts | 12 +++--- .../dataforms-layouts/data-form-layout.tsx | 38 ++++++++++++------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/dataviews/src/dataform-hooks/use-form.ts b/packages/dataviews/src/dataform-hooks/use-form.ts index 8822f09a3133d5..23fed3f72894fa 100644 --- a/packages/dataviews/src/dataform-hooks/use-form.ts +++ b/packages/dataviews/src/dataform-hooks/use-form.ts @@ -18,20 +18,20 @@ export const useForm = < Item >( } ); const setTouchedFields = ( touchedFields: string[] ) => { - setForm( { - ...form, + setForm( ( prevForm ) => ( { + ...prevForm, touchedFields, - } ); + } ) ); }; const setErrors = ( field: string, error: string | undefined ) => { - setForm( { - ...form, + setForm( ( prevForm ) => ( { + ...prevForm, messageErrors: { ...form.messageErrors, [ field ]: error, }, - } ); + } ) ); }; const isFormValid = ( data: Item ) => { diff --git a/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx b/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx index 8824726037ef91..7cf51fd7107476 100644 --- a/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx +++ b/packages/dataviews/src/dataforms-layouts/data-form-layout.tsx @@ -2,7 +2,7 @@ * WordPress dependencies */ import { __experimentalVStack as VStack } from '@wordpress/components'; -import { useContext, useEffect, useMemo } from '@wordpress/element'; +import { useContext, useEffect, useMemo, useRef } from '@wordpress/element'; /** * Internal dependencies @@ -48,9 +48,15 @@ export function DataFormLayout< Item >( { [ form ] ); + const isFirstValidationAlreadyRunRef = useRef( false ); + const { setTouchedFields, setErrors, touchedFields, messageErrors } = form; useEffect( () => { + if ( isFirstValidationAlreadyRunRef.current ) { + return; + } + normalizedFormFields.forEach( ( formField ) => { const { isValid, errorMessage } = formField.validation.callback( { ...data, @@ -61,6 +67,8 @@ export function DataFormLayout< Item >( { setErrors( formField.id, undefined ); } } ); + + isFirstValidationAlreadyRunRef.current = true; }, [ data, normalizedFormFields, setErrors ] ); return ( @@ -89,36 +97,38 @@ export function DataFormLayout< Item >( { return children( FieldLayout, formField ); } + const shouldShowError = formField.validation + .showErrorOnlyWhenDirty + ? touchedFields.includes( formField.id ) + : true; + + const errorMessage = shouldShowError + ? messageErrors[ formField.id ] + : undefined; + return ( { + onChange( value ); + if ( ! touchedFields.includes( formField.id ) ) { setTouchedFields( [ - // @ts-ignore - ...form.touchedFields, + ...touchedFields, formField.id, ] ); } - onChange( value ); - const { isValid, errorMessage } = + const { isValid, errorMessage: message } = formField.validation.callback( { ...data, ...value, } ); if ( ! isValid ) { - setErrors( formField.id, errorMessage ); + setErrors( formField.id, message ); } else { setErrors( formField.id, undefined ); }