Theme Framework How the LeaseLeads theme frontend works

data-* options

Scalar config passed PHP → JS as a JSON data-* attribute, read into an instance's options on construction.

On this page 4

When a Module needs to hand its component a bit of scalar configuration — a Splide config object, a set of flags, library options — it writes that config as a JSON string into a data-* attribute. On construction the instance parses it into options, and your JS reads it from there. No fetch, no lookup: the config rides along on the markup.

The PHP side

json_encode the config, then echo it into a data-* attribute. Here Intro builds its carousel options and hangs them off data-splide-gallery:

php
$splide = json_encode([
    'arrows'      => true,
    'pagination'  => true,
    'perPage'     => 3,
    'gap'         => '2.5rem',
    'focus'       => 'center',
    'breakpoints' => [
        '1200' => ['perPage' => 2],
        '640'  => ['perPage' => 1],
    ],
]);
php
<div class="intro__items splide-gallery" data-splide-gallery="<?= esc_attr($splide); ?>">
Important

Always run the encoded JSON through esc_attr() before printing it into the attribute. The value ends up inside double quotes in the HTML, so an unescaped quote in the payload breaks the markup.

The JS side

You never parse the attribute yourself. The framework reads it once, at construction, and the result lands on this.options — ready before your first lifecycle method runs. Your class does two things: declare a static get defaults() baseline for when the attribute is absent, then read this.options wherever you need the config. Here SplideGallery hands the merged options straight to Splide:

js
class SplideGallery extends Instantiable {
  static selector = '.splide-gallery'

  static get defaults() {
    return { type: 'fade', perPage: 1, arrows: false, pagination: false }
  }

  constructor(element, options) {
    super(element, options)
    // the data-* payload, merged over defaults, is already on this.options
    this.#controller = new Splide(this.element.querySelector('.carousel'), this.options)
  }
}

The attribute name matches the key camel-cased: data-splide-gallery is read by the class whose moniker resolves to splideGallery. Match the two and the payload arrives on this.options with nothing else to wire — the framework picks the key by moniker (falling back to the class name, or an explicit static dataKey), JSON.parses it, and falls back to defaults when the attribute is absent.

Note

Why an attribute and not an island. An attribute value is a single string the browser already parsed into dataset, and parseOptions runs it before your first lifecycle method — so config is present the moment the instance exists. That's ideal for scalar, bounded config. The moment the payload becomes a collection — a list of gallery items, a set of map markers — it outgrows an attribute and belongs in a Data island.

When to use it

  • Library options (Splide, Fancybox) the component passes straight through.
  • Feature flags and small enums (layout, pois, cluster).
  • Any bounded, scalar config you'd be comfortable reading inline in the HTML.

Reach for a Data island instead when the payload is a collection or deeply nested structure.

  • Data islands — the sibling bridge for collections and structured payloads.
  • Fields → CSS — how editor style settings cross to SCSS instead of JS.
  • The lifecycle — when options is available relative to boot*/init*.