/** * External dependencies */ import deepMerge from 'deepmerge'; /** * WordPress dependencies */ import { useCallback, useState } from '@wordpress/element'; /** * Internal dependencies */ import DataForm from '../index'; import type { Field } from '../../types'; const DataAdapterComponent = () => { type DataAdapterItem = { user: { profile: { name: string; email: string; }; preferences: { notifications: boolean; }; }; revenue: { total: number; units: number; pricePerUnit: number; }; }; const [ data, setData ] = useState< DataAdapterItem >( { user: { profile: { name: 'John Doe', email: 'john@example.com', }, preferences: { notifications: true, }, }, revenue: { total: 30, units: 10, pricePerUnit: 3, }, } ); const nestedFields: Field< DataAdapterItem >[] = [ // Examples of autogenerated getValue/setValue methods // for nested data based on the field id. { id: 'user.profile.name', label: 'User Name', type: 'text', }, { id: 'user.profile.email', label: 'User Email', type: 'email', }, { id: 'user.profile.tags', label: 'User Tags', type: 'array', placeholder: 'Enter comma-separated tags', description: 'Add tags separated by commas (e.g., "tag1, tag2, tag3")', elements: [ { value: 'astronomy', label: 'Astronomy' }, { value: 'book-review', label: 'Book review' }, { value: 'event', label: 'Event' }, { value: 'photography', label: 'Photography' }, { value: 'travel', label: 'Travel' }, ], }, // Example of adapting a data value to a control value // by providing getValue/setValue methods. { id: 'user.preferences.notifications', label: 'Notifications', type: 'boolean', Edit: 'radio', elements: [ { label: 'Enabled', value: 'enabled' }, { label: 'Disabled', value: 'disabled' }, ], getValue: ( { item } ) => item.user.preferences.notifications === true ? 'enabled' : 'disabled', setValue: ( { value } ) => ( { user: { preferences: { notifications: value === 'enabled' }, }, } ), }, // Example of deriving data by leveraging setValue method. { id: 'revenue.total', label: 'Total Revenue', type: 'integer', readOnly: true, }, { id: 'revenue.pricePerUnit', label: 'Price Per Unit', type: 'integer', setValue: ( { item, value } ) => ( { revenue: { total: value * item.revenue.units, pricePerUnit: value, }, } ), }, { id: 'revenue.units', label: 'Units', type: 'integer', setValue: ( { item, value } ) => ( { revenue: { total: item.revenue.pricePerUnit * value, units: value, }, } ), }, ]; const handleChange = useCallback( ( edits: any ) => { // Edits will respect the shape of the data // because fields provide the proper information // (via field.id or via field.setValue). setData( ( prev ) => deepMerge( prev, edits, { arrayMerge: ( target, source ) => source, } ) ); }, [] ); return ( <>

Data adapter

This story is best looked at with the code on the side. It aims to highlight how DataForm can wrangle data in scenarios such as nested data, bridge data to/from UI controls, and derived data.

Current data snapshot:

{ JSON.stringify( data, null, 2 ) }

Nested data

The first example demonstrates how to signal nested data via{ ' ' } field.id.

By using { `{ id: 'user.profile.name' }` } as field id, when users edit the name, the edits will come in this shape: { `{ user: { profile: { name: 'John Doe' } } }` }

data={ data } fields={ nestedFields } form={ { layout: { type: 'panel', labelPosition: 'top', openAs: 'modal', }, fields: [ { id: 'userProfile', label: 'User Profile', children: [ 'user.profile.name', 'user.profile.email', 'user.profile.tags', ], }, ], } } onChange={ handleChange } />

Adapt data and UI control

Sometimes, we need to adapt the data type to and from the UI control response. This example demonstrates how to adapt a boolean to a text string (Enabled/Disabled).

data={ data } fields={ nestedFields } form={ { layout: { type: 'panel', labelPosition: 'top', openAs: 'modal', }, fields: [ 'user.preferences.notifications' ], } } onChange={ handleChange } />

Derived data

Last, but not least, this example showcases how to work with derived data by providing a custom setValue function. Note how, changing UNITS or PRICE PER UNIT, updates the TOTAL value as well.

data={ data } fields={ nestedFields } form={ { layout: { type: 'panel', labelPosition: 'top', openAs: 'modal', }, fields: [ { id: 'revenue', label: 'Revenue', children: [ 'revenue.pricePerUnit', 'revenue.units', 'revenue.total', ], }, ], } } onChange={ handleChange } /> ); }; export default DataAdapterComponent;