Custom field types
A field type decides what the editor asks for, what is stored, how a value is sanitized and how the field is printed on an options page. The built-in types use exactly the same API, so a type of your own is a first-class citizen.
The Field Types API follows semantic versioning from 1.0 on.
FieldRegistry::API_LEVELtells you which level of the API is available.
A complete example
A small plugin that adds a URL field type. First the plugin file:
<?php
/**
* Plugin Name: Admin Options Pages: URL field
* Requires Plugins: admin-options-pages
*/
add_action( 'aop_register_field_types', function ( $registry ) {
require_once __DIR__ . '/class-url-field.php';
$registry->register( \MyPlugin\UrlField::class );
} );
Load the class file inside the callback, as above. Then your plugin never breaks when Admin Options Pages is not active.
Then the class:
<?php
namespace MyPlugin;
use AOP\App\Fields\FieldType;
use AOP\App\Fields\Setting;
class UrlField extends FieldType
{
public static function type(): string
{
return 'url';
}
public function label(): string
{
return __( 'URL', 'my-plugin' );
}
public function settings(): array
{
return [
Setting::label(),
Setting::name(),
Setting::autoload(),
Setting::frontend(),
Setting::text( 'placeholder', __( 'Placeholder', 'my-plugin' ) ),
Setting::make( 'require_https', 'toggle', __( 'Only allow https', 'my-plugin' ), 'toggle_onoff', 'off' ),
Setting::fixed( 'default_value', '' ),
Setting::classAttribute(),
Setting::description(),
];
}
public function sanitizeValue( $value, array $field )
{
$protocols = ( $field['require_https'] ?? 'off' ) === 'on' ? [ 'https' ] : [ 'http', 'https' ];
return esc_url_raw( $value, $protocols );
}
public function displayValue( $value, array $field, array $args ): string
{
if ( ! is_string( $value ) || $value === '' ) {
return '';
}
return sprintf( '<a href="%1$s">%1$s</a>', esc_url( $value ) );
}
public function render( array $field, $value ): void
{
printf(
'<input type="url" name="%1$s" id="%1$s" value="%2$s" class="regular-text code">',
esc_attr( $field['field_name'] ),
esc_url( $value )
);
}
}
That is all. The new type appears in the field picker of the editor, with a settings form built from settings().
Registering
register() returns false for a class that does not exist, that does not extend FieldType, or whose type is already taken. It never causes a fatal error: the problem is shown to administrators instead.
The action runs once, the first time the registry is used, and never before init.
Remove or replace a type with the filter aop_field_types, which receives the classes keyed by type.
The class
| Method | Purpose |
|---|---|
type() |
The identifier in the stored page: lowercase letters, digits and underscores. Never change it once a page uses it. |
label() |
The name in the field picker. |
settings() |
The settings of this type, in the order they are stored. |
sanitizeValue($value, $field) |
Runs when the options page is saved. Return the value to store. |
render($field, $value) |
Print the field. $value is the current option, false when it does not exist. |
displayValue($value, $field, $args) |
The value as HTML for the website (shortcode, bound block). Escape it here. Default: esc_html() of a scalar. |
bindingValue($value, $field, $attribute) |
The value for one attribute of a bound block. Return null when the type has nothing for it. |
group() |
'fields' (default) or 'layout'. |
hasOption() |
true (default). Layout types return false: they only print something. |
optionType() |
The type for register_setting(). Default 'string'. |
labelFor() |
Whether the label of the row points at the input. Default true. |
usesAssets() / enqueue($field) |
Load scripts or styles on the options page. |
Keep the class stateless: no work in the constructor, no get_option(), no hooks. The plugin registers the setting and calls render().
Settings
Every stored field starts with id, type, field_key, page_name and field_toggle. settings() adds the rest, built with Setting:
| Builder | Stores |
|---|---|
Setting::label(), name(), autoload(), classAttribute(), description(), defaultText() |
The settings most types share: field_label, field_name, toggle_autoload, class_attribute, description, default_value. |
Setting::frontend() |
Use in shortcode and blocks. Without it, your type is never shown on the website. |
Setting::text($key, $label) |
A text setting. |
Setting::make($key, $control, $label, $sanitize, $default, $extra) |
Anything else. |
Setting::fixed($key, $value) |
A value that is always stored. |
Setting::derived($key, $callable) |
A value computed from the other settings. |
$control, how the editor asks for it: text, textarea, number, toggle, select (with 'options' => [...] in $extra), choices, extensions or color.
$sanitize, what the server does with the posted setting: a callable fn ($value, array $raw, Validation $validate), ['in', [allowed, ...]], or one of these names: text, textarea, number, toggle, toggle_onoff, class_attribute, field_name, toggle_autoload, toggle_frontend, toggle_text_format, text_field_style, textarea_style, choices, color_hex, extensions, field_id.
The server, not the browser, decides what is stored: a setting that is not in your list is dropped, and a value that is not allowed stops the save.
A type with an option needs a default_value (a fixed or derived one is fine): the option is created with it when the page is saved.
JavaScript
The editor builds its settings forms from GET /wp-json/aop/v1/field-types, so most types need no JavaScript at all.
For a custom settings component or an icon, enqueue a script on aop_enqueue_editor_assets and register the type in the browser too:
window.aop.fields.register( 'url', {
icon: 'admin-links',
EditComponent: MyUrlSettings,
} );
The server decides which types exist: a type that is only registered in the browser is not offered.
An icon for the field picker
Without an icon, your type gets a question mark in the field picker. The icon works like the icon of a block type and can be one of:
| Form | Example |
|---|---|
| The name of a dashicon | icon: 'star-filled' |
| An SVG of your own, as an element | icon: wp.element.createElement( 'svg', … ), or <svg>…</svg> in JSX |
| A component that returns an SVG | icon: MyIcon |
An SVG of your own gets the size and color of the built-in icons (24 pixels, the color of the text and blue on hover), so draw it in a 24 × 24 viewBox and leave out fill. Load your script with the dependency wp-element:
add_action( 'aop_enqueue_editor_assets', function () {
wp_enqueue_script(
'my-rating-field-editor',
plugins_url( 'editor.js', __FILE__ ),
[ 'aop-field-registry-js', 'wp-element' ],
'1.0.0',
true
);
} );
// editor.js
( function ( wp ) {
const el = wp.element.createElement;
window.aop.fields.register( 'rating', {
icon: el(
'svg',
{ xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24' },
el( 'path', { d: 'M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4V6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z' } )
),
} );
} )( window.wp );
A string of SVG markup (
icon: '<svg>…</svg>') is not accepted: it would go into the page unfiltered. The picker shows a question mark instead.
When your plugin is deactivated
Fields of your type are kept exactly as they are, with their values. The editor shows them as not available, and an administrator can delete them on purpose. Activate your plugin again and everything works as before.