Skip to main content
Documentation/Developers/APIs/Dynamic Data API
Developers

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.