Dynamic Data API
Dynamic data is how an element shows a value
that comes from the site rather than from the person building the page — a post title, a
custom field, a featured image. Oxygen ships fields for WordPress core, ACF, Meta Box and
Toolset. The Dynamic Data API is how a plugin adds its own, so its data appears in the same
picker as everything else.
A field is a PHP class plus one line to register it.
A minimal field
use BreakdanceDynamicDataStringField;
use BreakdanceDynamicDataStringData;
class MyPluginMemberLevel extends StringField
{
public function label()
{
return 'Membership Level';
}
public function category()
{
return 'My Plugin';
}
public function slug()
{
return 'myplugin_member_level';
}
public function handler($attributes): StringData
{
return StringData::fromString(myplugin_current_user_level());
}
}
add_action('init', function () {
if (!function_exists('BreakdanceDynamicDataregisterField')) {
return;
}
BreakdanceDynamicDataregisterField(new MyPluginMemberLevel());
});
The guard matters: without it, a site that deactivates Oxygen fatals on every request. Do it
on init or later so the classes are loaded.
Four methods are required on every field:
| Method | Returns |
|---|---|
label() |
The field's name in the picker |
category() |
The group it appears under |
slug() |
A unique identifier. Prefix it — the namespace is shared site-wide |
handler($attributes) |
The value, wrapped in the data object your base class expects |
Choosing a base class
The base class decides what kind of data the field produces, which in turn decides which
element controls will offer it.
StringField
For text. handler() returns StringData.
public function handler($attributes): StringData
{
return StringData::fromString('some string value');
}
StringData::fromString($string) and StringData::emptyString() build one.
String fields get three extra controls for free — Prepend, Append and Limit Characters — plus
a Fallback field, so users can shape the output without you doing anything.
ImageField
For a single image. handler() returns ImageData, and the reliable way to build one is
from a WordPress attachment:
public function handler($attributes): ImageData
{
return ImageData::fromAttachmentId($attachmentId);
}
fromAttachmentId() fills in everything the Image element needs. If your image isn't a
WordPress attachment you can populate the object by hand, but url and sizes are
mandatory — sizes being an array keyed by size slug, each value holding file, width,
height and mime-type. Get that wrong and the Image element renders nothing.
GalleryField
For several images. handler() returns GalleryData, whose images property is an array of
ImageData:
public function handler($attributes): GalleryData
{
$attachedImages = get_attached_media('image', get_the_ID());
$gallery = new GalleryData();
$gallery->images = array_map(
fn($attachment) => ImageData::fromAttachmentId($attachment->ID),
$attachedImages
);
return $gallery;
}
OembedField
For video, either an oEmbed URL or a direct file. handler() returns OembedData, which
needs embedUrl and type ('oembed' or 'video'), plus provider for oEmbed sources.
public function handler($attributes): OembedData
{
return OembedData::fromOembedUrl('https://www.youtube.com/watch?v=…');
}
OembedData::fromOembedUrl($url) fetches the oEmbed data over HTTP — fine for YouTube and
Vimeo, but it is a network request, so don't call it in a loop. OembedData::emptyOembed()
returns an empty object.
RepeaterField
For repeating data, consumed by the
Repeater Fields. handler() returns
RepeaterData (built with RepeaterData::fromArray($rows)), and three more methods are
required: hasSubFields(), setSubFieldIndex($index) and parentField(). Its category
defaults to "Repeaters".
This is the base class the ACF and Meta Box integrations use for repeater fields — read one
of those before writing your own.
Return types
Every field declares which controls may use it, through returnTypes(). Each base class sets
a sensible default — StringField returns ['string'], ImageField returns
['image_url'] — and you override it when a field is legitimately more than one thing:
public function returnTypes()
{
return ['string', 'url'];
}
The types in use are:
| Type | Consumed by |
|---|---|
string |
Any text control |
url |
Link and URL controls |
image_url |
Image controls |
gallery |
Gallery and slideshow controls |
video |
Video controls |
query |
Query controls |
repeater |
The Repeater Field element |
google_map |
Map elements |
A field that returns a profile URL should declare ['string', 'url'] — that way it works
both as link destination and as visible text. A field with the wrong return type isn't
broken so much as invisible: it simply never appears in the picker for the control you
wanted it in.
Optional methods
subcategory() — a second level of grouping inside your category, which is what keeps a
plugin with fifty fields navigable.
controls() — extra controls shown when a user picks your field, for parameters your
handler needs. Whatever the user sets arrives in handler()'s $attributes array.
defaultAttributes() — defaults for those controls.
fallbackControl() — the control offered for a fallback value. The string, image and
video base classes already provide one.
availableForPostType($postType) — return false to hide the field on post types it
doesn't apply to.
availableFor() — which builders the field appears in. Defaults to
['breakdance', 'oxygen']; return ['oxygen'] for an Oxygen-only plugin.
proOnly() — defaults to true. Return false to make the field available on free
installs.
Many fields, one class
When a plugin exposes a user-defined set of fields — as custom-field plugins do — don't write
a class each. Write one class that takes its configuration in the constructor and register an
instance per field:
class MyPluginField extends BreakdanceDynamicDataStringField
{
protected array $fieldData;
public function __construct($fieldData)
{
$this->fieldData = $fieldData;
}
public function label() { return $this->fieldData['label']; }
public function category() { return 'My Plugin'; }
public function subcategory() { return $this->fieldData['group']; }
public function slug() { return 'myplugin_field_' . $this->fieldData['slug']; }
public function handler($attributes): StringData
{
return StringData::fromString((string) get_the_field_value($this->fieldData['slug']));
}
}
add_action('init', function () {
foreach (myplugin_get_fields() as $fieldData) {
BreakdanceDynamicDataregisterField(new MyPluginField($fieldData));
}
});
The slug still has to be unique per instance, which is why it's built from the field's own
identifier.
The other side of the connection
A field only reaches a user if the element control they're editing accepts its return type.
If you also build elements, switch on Enable dynamic property for the relevant controls
and set what each accepts — that's covered under
Element Studio → Controls.
A working example
Soflyy publishes a sample plugin that registers a string field and an image field:
github.com/soflyy/breakdance-sample-dynamic-data.
It's small enough to read in one sitting and it works as-is on an Oxygen install.