---
url: /releases/3.28.0.md
description: >-
  WPCE 3.28 release notes covering new number and term inputs, input-type API
  improvements, layout and type-safety changes, and the PHP 8.0 requirement.
---

# WPCE 3.28

This is certainly one of the largest WPCE releases so far, so expect quite some material for reading.

## Bug Fixes

* The label of the "Add element" button previously overflowed narrow containers like small columns. Now the label hides in such situations, only showing the "+" icon.
* Repeater labels had previously not been saved when committing the edit window with `Ctrl+S` / `⌘S` before leaving the repeater label field.
* Edit windows now always work on copies/proxies of data. Previously, some data was mutated directly, which in turn saved changes even though the "cancel" button was used.
* The editor takes full viewport height, properly allowing for shortcuts and right click menus to be used in the initially empty area.
* [`datetime` inputs](../documentation/input-types/datetime.md) no longer throw errors when the `required` option is `false` and the value is empty.
* HTML entities are no longer doubly-encoded in relation lists.

## Code Quality and Consistency, Maintainability, Technical Debt

* Replace `let` with `const`.
* Improved type safety by bumping TypeScript from version 5.7 to version 5.9.
* Overall updates to most dependencies.
* Drop PHP 7.4 support, PHP 8.0 is now required.
* Add linting with [Oxlint](https://oxc.rs/docs/guide/usage/linter.html).
* Use [Wireit](https://github.com/google/wireit) to speed up npm scripts.
* Utilize [`useTemplateRef()`](https://vuejs.org/guide/essentials/template-refs.html) wherever it makes sense.
* Replace [`lodash-es`](https://lodash.com/) with [`es-toolkit`](https://es-toolkit.dev/).

## Tiny Features

* Property `description`s can have `\n` line breaks now.
* The Instance Manager now has a `context` property for plugins/themes to store additional data on.

## `wpceVersion` and `documentVersion`

These properties of each document's root node were of rather obscure nature, because they were never really changed, so they never accurately reflected the WPCE version nor the a document version (a concept which has never been further developed).

From WPCE 3.28, saving a document automatically bumps the `wpceVersion`/`documentVersion`.
The WPCE Version is read from the plugin file itself, the current document version is stored in a `/document_version` file in the project root.

Based on this, a document migration system could be implemented in future WPCE versions.

## `number` Input Type

There is now a new [`number`](../documentation/input-types/number.md) input type.

```js
{
  input: {
    type: 'number', // [!code focus:2]
    min: 5,
  }
}
```

## `term` Input Type

There is now a new [`term`](../documentation/input-types/term.md) input type.

```js
{
  input: {
    type: 'term', // [!code focus:2]
    taxonomy: 'category'
  }
}
```

## `layout: 'custom'` Input Type Option

In addition to `'row'`, `'column'` and `'auto'`, the `layout` option for **input types** can now also be set to `'custom'`.

It allows an input type to completely occupy its available rectangle all by itself, omitting the default label and description.

This is basically a preparation for future, more editor UI related input types that do not necessarily follow the rather tabular approach of current input types.

## `layout: 'followup'` Property Option

In addition to `'row'`, `'column'` and `'auto'`, the `layout` option for **property options** can now also be set to `'followup'`.

This reduces the visual distinction between the property and its previous sibling in the property form, giving them a more cohesive appearance.

## `description` Property for `select` Options

Options for [`radio`](../documentation/input-types/radio.md) and [`checkboxes`](../documentation/input-types/checkboxes.md) inputs have always been compatible with the [`select`](../documentation/input-types/select.md) input — except they both had a `description` property, which the `select` input did lack.

This change smoothes out that missing part by adding a `description` property to the `select` options as well.

The description is presented similarly to the `radio`/`checkboxes` input types: as small text below each option in browsers supporting [`appearance: base-select`](https://developer.mozilla.org/en-US/docs/Web/CSS/appearance#base-select), otherwise the description of the currently selected item is printed below the dropdown box.

```js
{
  input: {
    type: 'select', // [!code focus:6]
    options: [
      { value: 'small', label: 'Small', description: 'max. 300px width' },
      { value: 'medium', label: 'Medium', description: 'max. 768px width' },
      { value: 'large', label: 'Large', description: 'max. 1024px width' },
    ],
  }
}
```

## `display` Input Option for `color` Input

For only a few choices, `radio` inputs are preferred over a `select` dropdown. This had until now no equivalent in the `color` input, which was always a dropdown.

The new `display` option can be set to `inline` (instead of the default `dropdown`) to show color choices as a row of prominent colored circles with color names below, not unlike color selection in the Gutenberg editor.

```js
{
  input: {
    type: 'color', // [!code focus:2]
    display: 'inline',
  }
}
```

## Additional Parameter for `wpce_get_color()` Input

The `wpce_get_color()` function now accepts the additional `$aspect` parameter to return individual aspects of a color, instead of an all in one object.

```php
use WPCE\ColorAspect;

// Get all aspects of the color, same as omitting the aspect parameter
wpce_get_color($props->color, aspect: ColorAspect::All);

// Get the color ID
wpce_get_color($props->color, aspect: ColorAspect::ID);

// Get a hex string (#rrggbb or #rrggbbaa)
wpce_get_color($props->color, aspect: ColorAspect::Hex);

// Get the alpha value (0-1)
wpce_get_color($props->color, aspect: ColorAspect::Alpha);

// Get an r, g, b, a plain object
wpce_get_color($props->color, aspect: ColorAspect::RGBA);

// Get an rgb() CSS string
wpce_get_color($props->color, aspect: ColorAspect::CSS);
```

## `suggestions` Option for `text`/`datetime` Inputs

The [`text`](../documentation/input-types/text.md) and [`datetime`](../documentation/input-types/datetime.md) input types now support a `suggestions` option, which can be used to provide a list of suggested values. These suggestions are shown in a dropdown when the user focuses the input.

## Default Mode for `link` Input

The [`link`](../documentation/input-types/link.md) input type now supports setting a default `mode` by setting `{ mode: 'text' }` as a link's `defaultValue`.

## New Input Type APIs

### Preface

While a lot of new APIs for input types are introduced in this release, WPCE 3.28 should be 100% compatible with previous versions. While the new ways of doing things presented below are more powerful and should preferably be used, the old API is still available and will continue to work for the foreseeable future.

### More Options for Input Type Registration

WPCE 3.27 introduced an options parameter to the `registerInputType` function with a single option: `layout`.

```js
// until v3.26
wpce.registerInputType('multiselect', Multiselect)

// since v3.27
wpce.registerInputType('multiselect', Multiselect, {
  layout: 'row',
})
```

WPCE 3.28 adds quite a number of options to this parameter.

#### Goals

1. **Decouple basic input type metadata from input type Vue components:**

   Move some essential input type operations out of their components to make them independent of a component actually being rendered.

2. **Decouple (built-in) input types from the editor:**

   Currently some built-in input types get special treatment in the editor in some situations (e.g. `repeater` for its nested properties, `editor` for its embedded document). Enough input type options should be available such that this special treatment is no longer necessary.

#### `getDefaultValue(context)`

Get the input type's default value from some context (property configuration, WPCE instance). Just like before, this is overridden if the property configuration defines its own `defaultValue`.

```js
wpce.registerInputType('multiselect', Multiselect, {
  getDefaultValue: ({ property, wpce }) => [],
})
```

**Why?** This was previously part of the input type component, meaning that a default value was only assigned if the property form was ever shown and saved. This is a much larger deal than it looks on first sight, because software using the resulting document trees (e.g. rendering logic) always had to guard not only against values expected from an input type, but also against nullish values.

#### `validate(value, context)`

Validate a value against the input type, given some additional context (property configuration, specific node object, WPCE instance).

```js
wpce.registerInputType('multiselect', Multiselect, {
  validate(value, { property, node, wpce }) {
    if (!property.input.options?.required) return true
    if (value.length > 0) return true

    throw 'This field is required'
  },
})
```

**Why?** Previously it was not possible to simply validate a whole document because the validation was part of the input type component.

#### `checkDirty(newValue, oldValue, context)`

Check whether two values differ, given some additional context (property configuration, specific node object, WPCE instance).

```js
wpce.registerInputType('multiselect', Multiselect, {
  checkDirty(newValue, oldValue, { property, node, wpce }) {
    const serialize = array => JSON.stringify([...array].sort())
    return serialize(newValue) === serialize(oldValue)
  },
})
```

**Why?** It was not possible to do a dirty check on a whole document before, because the dirty check had been part of the input type component.

#### `additionalGuids`

An array of [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html) JSONPath expressions which can be used to identify additional GUIDs in the input type's property value. Knowledge of these GUIDs is essential e.g. for reassigning them on node duplication.

```js
wpce.registerInputType('repeater', Repeater, {
  additionalGuids: ['$[0:].id'],
})
```

**Why?** There have been few cases for additional GUIDs, and all of them have been handled outside of the input type, in some special considerations the editor does e.g. for repeater inputs. Declaring the GUID paths explicitly allows for more flexibility in custom input types and prevents tedious after the fact classification of input types from a stored document.

:::info
The actual GUID paths (not the JSONPath expressions) are written into the document root's meta data on save, to make GUIDs findable in a stored document without knowledge of the input type which created each node.
:::

#### `getNestedProperties(context)`

Given some additional context (current value, input configuration), return a `Map` with [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html) JSONPath queries as keys and input configurations as values.

These entries are interpreted as nested properties of the input type's value.

```js
wpce.registerInputType('repeater', Repeater, {
  getNestedProperties({ value, input }) {
    const entries = input.properties.map(nestedProperty => [
      `$[0:]['value'][${JSON.stringify(nestedProperty.id)}]`,
      nestedProperty.input,
    ])

    return new Map(entries)
  },
})
```

**Why?** Repeater input values had to elaborately be detected to walk property values. This can be done more easily by declaring the paths in the input type.

:::info
Like `additionalGuids`, the actual nested property paths (not the JSONPath expressions) are written into the document root meta data on save to make them traceable without later knowledge of the input type.
:::

#### `embeddedDocuments`

An array of nested [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html) JSONPath expressions pointing to whole document root nodes in the property value.

```js
wpce.registerInputType('editor', Subeditor, {
  embeddedDocuments: ['$'],
})
```

**Why?** Editor input values had to elaborately be detected to walk them recursively where needed. This can be done more easily by declaring the paths in the input type.

:::info
Like `additionalGuids`, the actual nested document paths (not the JSONPath expressions) are written into the document root meta data on save to make them traceable without later knowledge of the input type.
:::

#### `conditions`

Declare a set of conditions the current input type value can be evaluated against.

```js
wpce.registerInputType('multiselect', Multiselect, {
  conditions: {
    isEmpty(value, payload) {
      const isEmpty = value.length === 0
      return isEmpty === (payload ?? true)
    },
    hasSelected(value, payload) {
      if (Array.isArray(payload)) {
        return payload.every(v => value.includes(v))
      } else {
        return value.includes(payload)
      }
    },
  },
})
```

**Why?** There has previously not been any relationship between different properties in the same element. With predefined conditions, input components in a property form can now react to each other.

### Additional Hooks on `useInputType()`

The [`useInputType()`](../documentation/concepts/input-types.md#rendering-an-input-type) composable provides three new hooks to be used in input type components:

* `onCommit(callback)`: Runs the `callback` just before the property form is saved. Use this to perform side effects that are not part of the input's value itself.
* `onValidate(callback)`: Runs the `callback` after the input was successfully validated. The callback may throw an error to still mark the input as invalid. This is useful in situations where basic validation may be achievable without the component, but some more sophisticated validation requires the component to be rendered.
* `onInvalid(callback)`: Runs the `callback` when the input has been deemed invalid. This is useful to perform side effects that should happen when the input is invalid (e.g. expanding repeater items when their nested properties are invalid).

### `v-model` Instead of `getValue()`

Another goal of this release is to get rid of the slightly shaky approach to use exposed methods for state management. And just like we could let go of the `isValid()` and `isDirty()` exposed methods through the new input type registration options, we can also ditch the `getValue()` methods in favor of plain `v-model` bindings.

This also means that we transition from a pull-based to a push-based approach. What does not change is that even with the push-based `v-model` API, new values are stored in an intermediate layer and are only forwarded to the document tree when the surrounding property form is saved.

So while we have a more modern Vue-y API for each input type component, the observable behavior does not change.

### Migrate to the New APIs

**Or: How does WPCE decide which API to use?**

For each individual now-legacy exposed input type component method (`getValue()`, `isValid()`, `isDirty()`), WPCE will check whether such an exposed method exists on the input type component. If it does, the legacy API is used. If not, the modern API is used.

This allows for a gradual transition to the new API, as input types can be converted one by one.

Also, the transition for each input type is not all or nothing itself: Input types can, for example, already use the new validation and dirty checker APIs but keep using the legacy `getValue()` method. This can be necessary for container input types (like repeaters) which cannot guarantee that all their nested input types use the modern API.

## Visibility Conditions

The `isHidden` option in the property configuration has been deprecated and replaced by the more powerful `isVisible` option.

Passed a boolean, it behaves exactly like the previous `isHidden` option (just inverted).

However, the `isVisible` option can also be passed a condition, evaluating its neighboring properties' input type `conditions` to determine its visibility.

```js
// ...
properties: [
  {
    id: 'showAdvanced',
    label: 'Advanced Settings',
    input: {
      type: 'checkboxes',
      options: [
        { value: 'show', label: 'Show advanced settings' },
      ],
    },
  },
  {
    id: 'advancedSetting1',
    label: 'Advanced Setting 1',
    input: {
      type: 'text',
      isVisible: [
        [
          // The neighboring property ID
          'showAdvanced',

          // The condition to evaluate
          // (as declared by the 'checkboxes' input type)
          'hasChecked',

          // The condition payload to evaluate against
          // (optional for some conditions)
          'show',
        ],
      ],
    },
  },
]
```

More conditions for the property's visibility could be added. Also, more complex conditions are possible - see the docs on [dynamic visibility](../documentation/concepts/elements/definition.md#dynamic-visibility).

## Ideas & Opportunities

The implemented concepts outlined above unlock quite powerful new possibilities for future WPCE versions, some of which could be:

* **Prototyping:** Just like we do with features in the Corporate Template, we can prototype new input types in individual projects and then port them over to the WPCE core if they prove useful.
* **Slots:** Explicitly declaring embedded documents could more confidently allow for things like slots (if they were somehow visually hoisted outwards into the element tree). However, this would still need more reliability improvements in sub editors in general.
* **Property Nesting:** Container input types (think ACF's tabs or groups) are made possible by the new input type API (especially `getNestedProperties()`). Also things like automatic migrations in case of new/removed properties would be feasible.
* **Dependencies:** Visibility based on same-element properties is already implemented. However, the declarative `conditions` input type option would also allow to project any other relationship between properties besides visibility, e.g. conditional options for the `select` input type.
* **Input Type Composition:** With the transition to use `v-model` for input type components, input types could render other input types as part of their template. This would allow for compound input types, e.g. an `image` picker with an attached `select` for choosing an image size.

***

:::info
**Known issues:**

* `embeddedDocuments` are taken into account when walking a document tree, however they are not yet considered when querying or validating their parent document.
* Some input types are missing useful conditions (e.g. for `file` inputs to check if a certain file type has been selected).
  :::
