DataViews, DataForm, et al. in WordPress 6.9

This is a summary of the changes introduced in the “dataviews space” during the WordPress 6.9 cycle. They have been posted in the corresponding iteration issue as well. There’s a new issue for the WordPress 7.0 cycle, subscribe there for updates.

Field APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways.

For more information, consult the two reference documents:

  • the developer docs for API description
  • the storybook where users can interact with each field type in its different forms

type

Developer docs

The field type has been expanded to describe the default field’s behaviors for sorting, validation, filtering, editing, and rendering. The field author can still override those defaults, but they don’t need to, they can have a working field by just providing the type.

{
  id: 'fieldId',
  type: 'number'
}

We’ve also expanded the initial 3 types into 13:

Field Type6.86.9
datetime
integer
text
array
boolean
color
date
email
media
number
password
telephone
url

Legend:

  • ✓ = Fully implemented with field type definition
  • Empty = Not available

isValid

Developer docs

Validation has been completely overhauled to leverage a rule-base system. All field types support the required and elements rule, as well as defining custom validation via the custom function — that supports both sync and async modes. We plan to expand the available rules in future versions, follow-up this issue for updates.

// 6.8
{
  isValid: ( item: Item, context?: ValidationContext ) => boolean;
}

// 6.9
{
  isValid: {
    required: boolean,
    element: boolean,
    custom:
        | ( ( item: Item, field: NormalizedField< Item > ) => null | string ) )
        | ( (
            item: Item,
            field: NormalizedField< Item >
          ) => Promise< null | string > );
  }
}

setValue

Developer docs

The introduction of setValue, alongside the existing getValue, has added a few capabilitiescapability capability is permission to perform one or more types of task. Checking if a user has a capability is performed by the current_user_can function. Each user of a WordPress site might have some permissions but not others, depending on their role. For example, users who have the Author role usually have permission to edit their own posts (the “edit_posts” capability), but not permission to edit other users’ posts (the “edit_others_posts” capability). to the framework. See the related story: live, code.

Working with nested data

By using the dot notation in the field.id (e.g., user.profile.name), the getValue and setValue methods will automatically read and write the data from the corresponding object property — though field authors can still override getValue and setValue methods.

  const data = {
    user: {
      profile: {
        name: 'John Doe',
        email: 'john@example.com'
      }
    }
  };

  const fields = [
    {
      id: 'user.profile.name',  // Dot notation for nested access
      label: 'User Name',
      type: 'text',
    },
    {
      id: 'user.profile.email',
      label: 'Email',
      type: 'email',
    }
  ];

Derived data

setValue can be leveraged to decide which parts of the data should be updated, including modifying multiples parts of it.

  const data = {
    product: {
      price: 10,
      quantity: 3,
      total: 30  // Derived from price × quantity
    }
  };

  const fields = [
    {
      id: 'product.price',
      label: 'Price',
      type: 'integer',
      // Updates total when price changes
      setValue: ( { item, value } ) => ({
        product: {
          price: value,
          total: value * item.product.quantity
        }
      })
    },
    {
      id: 'product.quantity',
      label: 'Quantity',
      type: 'integer',
      // Updates total when quantity changes
      setValue: ( { item, value } ) => ({
        product: {
          quantity: value,
          total: item.product.price * value
        }
      })
    },
    {
      id: 'product.total',
      label: 'Total',
      type: 'integer',
      readOnly: true  // Calculated field, not editable
    }
  ];

Bridge data and UIUI User interface.

In certain situations, the data and the UI control representing the data need to be adapted. Field authors can use getValue and setValue for this.

  const data = {
    user: {
      preferences: {
        notifications: true  // Boolean in data
      }
    }
  };

  const fields = [
    {
      id: 'notifications',
      label: 'Notifications',
      type: 'boolean',
      Edit: 'radio',
      elements: [
        { label: 'Enabled', value: 'enabled' },
        { label: 'Disabled', value: 'disabled' }
      ],
      // Transform boolean to string for UI
      getValue: ( { item } ) =>
        item.user.preferences.notifications === true
          ? 'enabled'
          : 'disabled',
      // Transform string back to boolean for data
      setValue: ( { value } ) => ({
        user: {
          preferences: {
            notifications: value === 'enabled'
          }
        }
      })
    }
  ];

Edit

Developer docs

Field authors can define how a field behaves when it’s edited. They can provide a custom edit control, but also select one of bundled ones — which have been expanded from 5 to 16:

ControlDescription6.86.9
datetimeDate time picker
integerNumber input
radioRadio buttons
selectDropdown select
textText input
arrayMulti-select
checkboxCheckbox control
colorColor picker
dateDate picker
emailEmail input
passwordPassword input
telephonePhone input
textareaMulti-line text
toggleToggle switch
toggleGroupButton group
urlURLURL A specific web address of a website or web page on the Internet, such as a website’s URL www.wordpress.org input

Additionally, some bundled edit controls now support a configuration option to control its behavior or look-and-feel.

 // Control the rows in a textarea
 {
    type: 'text',
    Edit: {
      control: 'textarea',
      rows: 5
    }
}

// Add prefixes/suffixes to control
{
  type: 'text',
  Edit: {
    control: 'text',
    prefix: DollarPrefix, // React component
    suffix: USDSuffix, // React component
  }
}
Image

filterBy

Developer docs

Filters have gained more operators (from 6 to 22) and they are now bound to the field type.

The initial 6 operators available (is, isNot, isAny, isNone, isAll, isNotAll) have been expanded into 22 total:

  • for integer and number: lessThan, greaterThan, lessThanOrEqual, greaterThanOrEqual, between
  • for date and datetime: before, after, beforeInc, afterInc, on, notOn, inThePast, over, between
  • for text: contains, notContains, startsWith

The addition of new field types and edit controls has also expanded the filters available, that are now dependent on the field type, and they include support for user input as well:

User inputDate filter
ImageImage ImageImage

getElements

Developer docs

{
    getElements: () => Promise<Option[]>
}

We’ve introduced a getElements async function that fetches elements on demand, only when they are needed. This is a new API that will continue to evolve, follow-up changes at 70834.

This API can be seen in use in the Pages screen of the Site Editor. The author filter and edit control won’t fetch the authors until the user interacts with those controls.

readOnly

We’ve added a readOnly property (boolean) to mark fields that cannot be edited. In edit contexts, they’ll be displayed using the render property. For example, this can be useful when displaying non-editable metadata (file size, etc.).

DataViews

For more information, consult the two reference documents:

view

Developer docs

Group by

All the bundled layouts –table, grid, and list– support now grouping the items by a given field by declaring a groupByField in the view. Follow issue for updates, and take a look at the storybook.

{
 groupByField: 'fieldId'
}
gridlisttable
ImageImageImage

Locked filters

Locked filters are filters that cannot be edited or removed by the user. They appear in the filters UI but are marked as read-only, ensuring certain filtering conditions are always applied.

{
  filters: [ {
    field: 'status',
    operator: OPERATOR_IS_ANY,
    value: 'publish',
    isLocked: true,
  } ]
}
Image

Table layout: align and lock movers

There are two new options that apply to the table layout:

{
  type: 'table',
  layout: {
    styles: {
      fieldId: {
        align: 'start' | 'center' | 'end'
      }
    },
    enableMoving: false
  }
}
  • align is a new property to control the horizontal alignment of content within the cells. It’s specificied per field in the layout.
  • enableMoving: boolean to control whether the fields can be moved around. True by default. If false, it removes the moving controls in the table headerHeader The header of your site is typically the first thing people will experience. The masthead or header art located across the top of your page is part of the look and feel of your website. It can influence a visitor’s opinion about your content and you/ your organization’s brand. It may also look different on different screen sizes. and disables moving the fields via the view config as well.
TableView config
ImageImage

actions

Developer docs

The modal actions have now more options to control its behavior:

  • modalHeader: string | ( ( items: Item[] ) => string );. Can now take a function to dynamically set the header based on the selected items.
  • modalSize: small | medium | large | fill. Controls the size of the modal.
  • modalFocusOnMount: true | false | firstElement | firstContentElement. Controls the focus behavior when the modal opens.

paginationInfo

Developer docs

All bundled layouts -table, list, and grid- now support infinite scroll. It’s enabled per view type, and DataViews takes a infiniteScrollHandler function that listens to scroll events and handles the data retrieval. See storybook live and implementation for details.

const view = {
  layout: 'grid',
  infiniteScrollEnabled: boolean
}

const paginationInfo = {
  totalItems: totalItems,
  totalPages: totalPages,
  infiniteScrollHandler: () => { /* Fetch data, update view, etc. */ }
};

return (
  <DataViews
    view={view}
    paginationInfo={paginationInfo}
);

Developer docs

The renderItemLink prop lets you provide a custom component that wraps clickable fields. It can render regular links, but also allows integration with routing libraries (like TanStack Router or ReactReact React is a JavaScript library that makes it easy to reason about, construct, and maintain stateless and stateful user interfaces. https://reactjs.org/. Router).

The component receives the following props:

  • item: The data item that was clicked
  • Additional standard HTMLHTML HyperText Markup Language. The semantic scripting language primarily used for outputting content in web browsers. anchor props (className, style, etc.)
<DataViews
    renderItemLink={ ( { item, ...props } ) => (
        <Link to={ `/sites/${ item.slug }` } preload="intent" { ...props } />
    ) }
/>

config

Developer docs

Prop to configure the list of perPageSizes available in the view config. Defaults to [10, 20, 50, 100]).

Image

empty

Developer docs

An element to display when the data prop is empty. Defaults to <p>No results</p>.

children

Developer docs, Storybook

We’ve exported many of the subcomponents DataViews is made up of, for them to be reused to build UIs.

When DataViews receives no children, it renders its complete built-in UI (search bar, filter controls, view config, layout, etc.):
  

<DataViews
  data={ posts }
  fields={ fields }
  view={ view }
  onChangeView={ setView }
  actions={ actions }
  paginationInfo={ paginationInfo }
/>

When children are provided, you can build custom layouts while leveraging DataViews’ internal state management and data handling logic:

  <DataViews
    data={ posts }
    fields={ fields }
    view={ view }
    onChangeView={ setView }
    paginationInfo={ paginationInfo }
  >
    <DataViews.Search />
    <DataViews.Layout />
    <DataViews.Pagination />
  </DataViews>

DataViews exposes 9 subcomponents for free composition:

ComponentPurpose
DataViews.BulkActionToolbarToolbar for bulk actions
DataViews.FiltersFilter controls panel
DataViews.FiltersToggleButton to show/hide filters panel
DataViews.FiltersToggledBadge showing active filters count
DataViews.FooterComponent that renders pagination and bulk actions
DataViews.LayoutRenders the actual data (grid/table/list)
DataViews.LayoutSwitcherButtons to switch between layouts
DataViews.PaginationPage navigation controls
DataViews.SearchSearch input field
DataViews.ViewConfigButton to open view configuration

Persist views via @wordpress/views

Documentation.

A new package for managing DataViews view state with persistence using WordPress preferences.

The @wordpress/views package provides:

  • Persistence: Automatically saves and restores DataViews state using @wordpress/preferences
  • View Modification Detection: Tracks when views differ from their default state
  • Reset Functionality: Simple reset to default view capabilitycapability capability is permission to perform one or more types of task. Checking if a user has a capability is performed by the current_user_can function. Each user of a WordPress site might have some permissions but not others, depending on their role. For example, users who have the Author role usually have permission to edit their own posts (the “edit_posts” capability), but not permission to edit other users’ posts (the “edit_others_posts” capability).
  • Clean Integration: Drop-in replacement for manual view state management

Usage:

// In route loader
const view = await loadView( {
    kind: 'postType',
    name: 'wp_template',
    slug: 'active',
    defaultView,
    queryParams: { /* ... */ },
} );

// In screen
const { view, updateView, isModified, resetToDefault } = useView( {
    kind: 'postType',
    name: 'wp_template',
    slug: 'active',
    defaultView,
    queryParams: { /* ... */},
} );

DataViewsPicker

DataViewsPicker is a selection-focused variant of DataViews designed for building item picker interfaces. Use cases include a media picker for selecting images (#71944), or a generalised post picker for selecting posts or page parents (#71128). It currently only supports a grid layout view.

  • Selection Management: Track which items users have selected via selection state and onChangeSelection callbacks
  • Action Buttons: Include action buttons (like “Confirm” or “Cancel”) that operate on selected items via the actions prop
  • Familiar DataViews API: Same field definitions, filtering, pagination, and view configuration as DataViews
  • Customizable Layouts: Supports DataViews picker-specific layouts (to begin with, there is just a grid picker view available)
  • Default UI: Like DataViews, uses a pre-built interface including search, filters, layout switcher, and action toolbar—or use custom children to build your own

Basic usage:

<DataViewsPicker
  data={items}
  fields={fields}
  selection={selectedIds}
  onChangeSelection={setSelectedIds}
  view={view}
  onChangeView={setView}
  paginationInfo={{ totalItems: 100, totalPages: 10 }}
  actions={[
    { id: 'cancel', label: 'Cancel', callback: () => {...} },
    { id: 'confirm', label: 'Confirm', isPrimary: true, callback: () => {...} },
  ]}
  defaultLayouts={{ [LAYOUT_PICKER_GRID]: {} }}
/>

DataForm

For more information, consult the two reference documents:

form

Developer docs

The form API has been updated to accommodate the changes to existing layouts and the introduction of two new ones: card and row.

panel

Developer docs, Storybook

There are two new properties, openAs and summary, to configure the behavior of the layout. openAs determines how the panel opens, either as a dropdown (default) or a modal. summary controls what fields summarize the panel, while before the first children was used.

{
  layout: {
    type: 'panel',
    labelPosition: 'top' | 'side' | 'none',
    openAs: 'dropdown' | 'modal',
    summary: 'summaryField' | [ 'summaryFieldOne', 'summaryFieldTwo' ]
  }
}
Image

card

Developer docs, Storybook

The new card layout is a container layout: it wraps other layouts (regular, panel, etc.) within a card component.

  • withHeader: controls whether the card has a header
  • isOpened: controls how the card loads (opened or closed)
  • summary: a mechanism to render fields in the header, and control when they are displayed, either when the card is collapsed (the default) or always
// Card open and summary displayed only when collapsed
{
  layout: {
    type: 'card',
    summary: 'plan-summary'
  },
}

// Card with no header
{
  layout: {
    type: 'card',
    withHeader: false,
  },
} 

// Card collapsed with summary
{
  layout: {
    type: 'card',
    isOpened: false,
    summary: [ { id: 'dueDate', visibility: 'always' } ],
  },
}
Image

row

Developer docs, Storybook.

The new row layout is a container layout: it enables positioning other layouts horizontally.

  • alignment: control vertical alignment of fields within the row
  • styles: provides flex styles, for the width to be adjusted per field
{
  layout: {
    type: 'row',
    alignment: 'start' | 'center' | 'end',
    styles: {
      "fieldId": {
        flex: CSSFlexProperties
      }
    }
  }
}
Image

validity

Validation has been revamped in 6.9: it’s now controlled (DataForm has a validity prop), and supports asynchronous validation. All the DataForm controls gained support for all validation rules (required, elements, and custom). See related Field API updates and the Field API validation issue.

There’s a new useFormValidity hook to compute the validity prop, and the isItemValid utility has been removed.

const fields = [
  {
    id: 'title',
    type: 'text',
    label: 'Text',
    isValid: {
      required: true,
      custom: ( item: Item, field: NormalizedField< Item > ) => {
        // returns a validation message if field is invalid
      }
    }
  },
  // other fields
];

/*
 * validity: object, represents the validation state of the form
 * isValid: boolean, whether the form is valid or not
 */
const { validity, isValid } = useFormValidity( data, fields, form );

return (
    <form>
        <VStack alignment="left">
            <DataForm< Item >
                data={ data }
                fields={ fields }
                form={ form }
                validity={ validity }
                onChange={ onChange }
            />
            <Button disabled={ ! isValid } >
                Submit
            </Button>
        </VStack>
    </form>
);

Props to @priethor for review, and @andrewserong for the DataViewsPicker updates.

#6-9, #dev-notes, #dev-notes-6-9