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 }