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/⌘Sbefore 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.
datetimeinputs no longer throw errors when therequiredoption isfalseand the value is empty.- HTML entities are no longer doubly-encoded in relation lists.
Code Quality and Consistency, Maintainability, Technical Debt
- Replace
letwithconst. - 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.
- Use Wireit to speed up npm scripts.
- Utilize
useTemplateRef()wherever it makes sense. - Replace
lodash-eswithes-toolkit.
Tiny Features
- Property
descriptions can have\nline breaks now. - The Instance Manager now has a
contextproperty 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 input type.
{
input: {
type: 'number',
min: 5,
}
}term Input Type
There is now a new term input type.
{
input: {
type: 'term',
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 and checkboxes inputs have always been compatible with the select 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, otherwise the description of the currently selected item is printed below the dropdown box.
{
input: {
type: 'select',
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.
{
input: {
type: 'color',
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.
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 and datetime 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 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.
// 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
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.
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.
repeaterfor its nested properties,editorfor 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.
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).
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).
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 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.
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 JSONPath queries as keys and input configurations as values.
These entries are interpreted as nested properties of the input type's value.
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 JSONPath expressions pointing to whole document root nodes in the property value.
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.
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() composable provides three new hooks to be used in input type components:
onCommit(callback): Runs thecallbackjust 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 thecallbackafter 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 thecallbackwhen 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.
// ...
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.
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
conditionsinput type option would also allow to project any other relationship between properties besides visibility, e.g. conditional options for theselectinput type. - Input Type Composition: With the transition to use
v-modelfor input type components, input types could render other input types as part of their template. This would allow for compound input types, e.g. animagepicker with an attachedselectfor choosing an image size.
INFO
Known issues:
embeddedDocumentsare 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
fileinputs to check if a certain file type has been selected).