PageTree demo by

Developers

Plugin hooks for blocks

Since 2.6, another plugin can extend every PageTree block without patching PageTree: add fields to the block forms, change the template variables, add attributes to the block root, filter the HTML, and load assets in the preview and in the block editors.

Principles

  • The hooks are opt-in: without a listener, PageTree renders the same markup and loads the same assets as before.
  • A listener must be idempotent. An event can fire several times for the same block in a request (the Editor rebuilds the block forms, the preview reloads, a page has several blocks): do not add the same field or register the same handler twice.
  • A listener exception is logged and never breaks the page (see When a listener fails).
EventWhenArguments
pagetree.block.config.fieldsForm definition of a block[$block, &$fields]
pagetree.block.renderBefore the Twig rendering[$block, &$templateVars, &$attributes, $component]
pagetree.block.renderedAfter the block HTML[&$html, $component]
pagetree.preview.assetsEditor preview and website edit mode[$page, &$headAssets]
pagetree.editor.blockfieldsBlock editor and front-end block panel[&$assets, $context]

Add fields to the block forms

pagetree.block.config.fields fires when the form definition of a block is built, before PageTree adds its own Displayed? field. $fields is the array of a config_fields.yaml file, modified by reference. The field can use any October form field type.

Event::listen('pagetree.block.config.fields', function ($block, &$fields) {
    if (isset($fields['fields']['css_classes'])) {
        return; // Several blocks share the same form
    }

    $fields['fields']['css_classes'] = [
        'label' => 'CSS classes',
        'type' => 'text',
        'default' => 'my-default-class',
    ];
});

The event fires in the Editor form, in the preview, in the front-end edit panel and when PageTree resolves the default values of a block: a block saved without the field renders with its default, on the public website too.

Change the variables and the root attributes

pagetree.block.render fires before the Twig template of a block is rendered. $templateVars are the template variables, $attributes an empty array of attributes for the block root, and $component the block descriptor (id, type, data completed with the form defaults).

Event::listen('pagetree.block.render', function ($block, &$templateVars, &$attributes, $component) {
    $classes = trim((string) ($component->data['css_classes'] ?? ''));
    if ($classes !== '') {
        $attributes['class'] = $classes;
        $attributes['data-block-type'] = $component->type;
    }
});

PageTree applies the attributes to the first root element of the block, skipping comments and the link, script, style and meta tags. A class is merged with the existing classes; any other attribute replaces the existing one. Values are escaped, and the attribute names are checked: on*, srcdoc and invalid names are ignored, as well as an address attribute (href, src, action, formaction…) holding a javascript:, vbscript: or data: address.

The blockAttributes variable

A template that places the attributes itself, on a wrapper or an inner element, prints the safe blockAttributes variable. PageTree then does not add them again to the first root element. No |raw is needed, and the variable prints an empty string when there is no attribute.

<section{{ blockAttributes }}>
    ...
</section>

Filter the block HTML

pagetree.block.rendered fires after the HTML of the block is rendered and its root attributes applied, for changes that attributes cannot express (wrapping, schema.org markup, analytics markers…). Change $html by reference instead of returning a string: every listener runs, in registration order, so several plugins can extend the same block.

Event::listen('pagetree.block.rendered', function (&$html, $component) {
    $html .= '<!-- block ' . e($component->id) . ' -->';
});

Load assets in the preview and the editors

pagetree.preview.assets adds assets to the Editor preview and to the website page in edit mode, never to the normal public rendering. pagetree.editor.blockfields adds assets to both block editors (the Editor form and the front-end block panel), with October addCss() / addJs().

Event::listen('pagetree.preview.assets', function ($page, &$headAssets) {
    $headAssets['css'][] = '/plugins/acme/preview/preview.css';
    $headAssets['js'][] = ['url' => '/plugins/acme/preview/preview.js', 'defer' => true];
});

Event::listen('pagetree.editor.blockfields', function (&$assets, $context) {
    $assets['js'][] = '/plugins/acme/editor/editor.js';
});

Each entry is a URL string, or an array with a url and optional attributes (defer, async, type, media, crossorigin, integrity…). PageTree builds and escapes the tags itself. Relative, absolute and protocol-relative URLs are accepted; javascript:, vbscript: and data: URLs are ignored.

JavaScript events

When a block form is displayed, both editors dispatch pagetree:block-form-ready on the current window, once per form. The form root also carries data-block-id, so a plugin does not need a MutationObserver to find the form.

window.addEventListener('pagetree:block-form-ready', function (event) {
    const { element, blockId, blockType, window } = event.detail;
});

The Editor preview still dispatches pagetree:preview-document with { document, window } each time the preview document is installed: use it for behavior that must run in the preview document.

When a listener fails

  • pagetree.block.render: the exception is logged as a warning and the block is rendered as without the listeners, with no attributes and the template variables as they were before the event.
  • pagetree.block.rendered: PageTree logs a warning (without the block data) and returns the HTML it had before the filter. The listeners registered after it do not run for that block, and the changes of those before it are dropped too.
  • pagetree:block-form-ready: the dispatch is wrapped in a try/catch, so a broken listener never breaks the editor.