Skip to content

Creating Custom Elements

Every DXPR Builder element is a JavaScript object pushed onto window.dxprBuilder.dxpr_elements. The editor turns each object into an element class with the Style and Animation tabs already attached. This page shows how the shipped elements are defined in dxpr_builder/dxpr_elements.js, how a module adds its own definition to the editor, and what the resulting HTML looks like. There are no server-side Twig templates per element; elements render in the browser.

How the editor registers elements

The editor loads six scripts in this order, defined in dxpr_builder_library_info_build() in dxpr_builder.module as the dxpr_builder/editor.builder library:

  1. dxpr_templates.js: client-side twig.js templates compiled from dxpr_builder/templates/.
  2. dxpr_global.js: shared helpers and the CKEditor 5 integration.
  3. dxpr_events.js
  4. dxpr_param_types.js: pushes one object per input type onto window.dxprBuilder.dxpr_param_types.
  5. dxpr_elements.js: creates the window.dxprBuilder.dxpr_elements array and pushes one definition per element.
  6. dxpr_builder.js: the editor. When this file executes it calls create_dxpr_elements(), which loops over window.dxprBuilder.dxpr_elements and for each definition:
  7. calls register_animated_element(base, is_container, Class), so the class extends AnimatedElement (Style tab, Animation tab, HTML ID, HTML classes and placement parameters);
  8. appends the inherited parameters to the definition's params;
  9. converts every parameter through make_param_type(), which matches param.type against the registered parameter types;
  10. mixes the definition's properties into the class prototype.

A definition therefore has to be on the array after dxpr_elements.js has run and before dxpr_builder.js runs. All six files load with the defer attribute, so the document order of the script tags is the execution order.

The element definition

This is the Alert element from dxpr_builder/dxpr_elements.js (source: dxpr_builder/build/dxpr-elements/elements/az_alert.js), trimmed to the properties every element needs. The original also sets el_class, an onCancel handler and a focusout listener.

window.dxprBuilder.dxpr_elements.push({
  base: "az_alert",
  name: Drupal.t("Alert"),
  nameEn: "Alert",
  icon: "az-custom-icon az-alert-icon",
  largeSettingsWindow: true,
  hidden: false,
  params: [
    {
      type: "textarea",
      heading: Drupal.t("Message"),
      param_name: "content",
      value: Drupal.t(
        "This is a placeholder text. Your actual content will go here. Edit this to include your own information.",
      ),
    },
    {
      type: "dropdown",
      heading: Drupal.t("Type"),
      param_name: "type",
      description: Drupal.t("Select message type."),
      getText: (opt) => opt.text,
      getValue: (opt) => opt.value,
      getDefault: (param) => param.value["alert-success"].value,
      value: {
        "alert-success": { text: Drupal.t("Success"), value: "alert-success" },
        "alert-info": { text: Drupal.t("Info"), value: "alert-info" },
        "alert-warning": { text: Drupal.t("Warning"), value: "alert-warning" },
        "alert-danger": { text: Drupal.t("Danger"), value: "alert-danger" },
      },
      passthrough: true,
    },
  ],
  show_settings_on_create: true,
  has_content: true,
  render(...args) {
    const state = {
      class: `az-element az-alert alert ${this.attrs.type} ${this.get_el_classes()} ${this.get_content_classes()}`,
      style: this.attrs.style,
      message: this.attrs.content,
    };
    this.dom_element = this.baseclass.prototype.render_template(
      "elements/alert/alert",
      state,
    );
    this.baseclass.prototype.render.apply(this, args);
  },
  save_history() {
    const container = this.get_my_container();
    container.save_container_history(Drupal.t("Edit alert"));
  },
});
Property Purpose
base Machine name. It is written to the saved HTML as data-azb="az_alert" and used to find the class when the page is parsed again. Keep the az_ prefix.
name, nameEn Label in the element picker and its untranslated form.
icon CSS classes for the picker icon. The shipped icons are defined in dxpr_builder/sass/dxpr-builder-backend/components/_custom-icons.scss; a custom element needs its own CSS for its icon class.
category Picker tab. The layout elements use Drupal.t("Layout"); blocks and views use "CMS". Omit it for the default content tab.
is_container true when the element holds other elements.
largeSettingsWindow Opens the settings dialog at the larger size.
hidden Hides the element from the picker while keeping it renderable.
params Settings shown in the dialog. See below.
show_settings_on_create Opens the settings dialog as soon as the element is dropped.
has_content The element has an editable content area (dom_content_element).
render(...args) Builds this.dom_element from this.attrs, then calls the base render.
save_history() Pushes an undo entry with a label.
parse_attrs_from_dom(element) Optional. Reads parameter values back out of saved markup, used together with skip_data_azb and skip_params by elements such as Image that store values in the <img> attributes instead of data-azat-*.

Parameters

Each entry in params has:

Key Meaning
type One of the registered parameter types (next section).
heading Field label.
param_name Key in this.attrs and, in saved HTML, the data-azat-<param_name> attribute. content is special: it is the element body, not an attribute.
value Default value. For dropdown it is an object of options with getText, getValue and getDefault callbacks.
description Help text under the field.
tab Groups the field into a named accordion, for example Drupal.t("SEO").
can_be_empty Store an empty string instead of dropping the attribute.
safe Skip URL encoding of the stored value.

The parameter types are the type values pushed onto window.dxprBuilder.dxpr_param_types in dxpr_builder/dxpr_param_types.js: boxmodel, checkbox, checkboxes, colorpicker, datetime, dropdown, dxb_slider, html, icon, image, images, link, links, numericfield, row_layout, saved_datetime, social_links, textarea, textfield and video. A type you do not register falls back to a plain text input (BaseParamType).

Do not reuse a param_name that every element inherits, such as color, background, el_class, hash or the an_* animation names. The Style tab already owns color, so a second parameter with that name is never written to data-azat-color.

What gets saved

update_data() in dxpr_builder.js writes the element to HTML:

  • data-azb="<base>" on the root element;
  • data-azat-<param_name>="<value>" for every parameter whose value differs from its default, URL-encoded unless the parameter sets safe: true;
  • the Style tab parameters as inline style and classes, never as data-azat-* attributes;
  • data-azcnt="true" on the content element when has_content is set.

Viewers who do not get the editor receive this HTML without its data-az* attributes: DxprBuilderService::parseDocumentForCleanup() strips them, except a fixed list it keeps on animated elements and on built-in dynamic elements such as Carousel. If the element needs JavaScript or CSS for visitors, put a data-dxpr-builder-libraries="<key>" attribute on its root: parseDocumentForTemplateLibrary() attaches dxpr_builder/elements.<key> for every key without a slash. The keys are the elements.* libraries in dxpr_builder.libraries.yml; a key with a slash is only honoured when it is the library of a registered icon set.

Minimal working example

The example adds a "Notice" element that renders a paragraph with a chosen colour. It builds its DOM directly because the compiled twig.js templates in dxpr_templates.js are part of the editor bundle and cannot be extended by another module.

  1. Create mymodule/js/notice-element.js:

javascript window.dxprBuilder.dxpr_elements.push({ base: "az_notice", name: Drupal.t("Notice"), nameEn: "Notice", icon: "az-custom-icon az-alert-icon", params: [ { type: "textfield", heading: Drupal.t("Text"), param_name: "text", value: Drupal.t("Notice text"), }, { type: "colorpicker", heading: Drupal.t("Colour"), param_name: "notice_color", value: "#0d6efd", }, ], show_settings_on_create: true, render(...args) { const el = document.createElement("div"); el.className = `az-element az-notice ${this.get_el_classes()}`; if (this.attrs.style) { el.setAttribute("style", this.attrs.style); } const p = document.createElement("p"); p.style.color = this.attrs.notice_color; p.textContent = this.attrs.text; el.appendChild(p); this.dom_element = el; this.baseclass.prototype.render.apply(this, args); }, save_history() { this.get_my_container().save_container_history(Drupal.t("Edit notice")); }, });

  1. Insert the file into the editor library between dxpr_elements.js (weight -4) and dxpr_builder.js (weight -3) in mymodule.module:

php /** * Implements hook_library_info_alter(). */ function mymodule_library_info_alter(array &$libraries, string $extension): void { if ($extension === 'dxpr_builder' && isset($libraries['editor.builder'])) { $path = '/' . \Drupal::service('extension.list.module')->getPath('mymodule'); $libraries['editor.builder']['js'][$path . '/js/notice-element.js'] = [ 'weight' => -3.5, 'preprocess' => FALSE, 'attributes' => ['defer' => TRUE], ]; } }

preprocess: FALSE and defer match the entries the module defines for its own files, so the file keeps its place in the sequence whether editor assets come from the CDN or from a local build. 3. Clear the cache and open a builder page:

bash drush cr

  1. Add the element from the picker, set its text and colour, and save. The field stores:

```html

Hello

```

A visitor gets the same markup without the data-az* attributes. The paragraph keeps its inline colour, so no visitor library is needed for this element.

Limits to know about

  • Profiles do not list custom elements. The element checklist in a builder profile comes from a fixed list in src/Form/DxprBuilderProfileForm.php and src/Service/Handler/ProfileHandler.php. Elements outside that list are never hidden by a profile.
  • Icons for the picker need CSS from your module; the az-custom-icon classes shipped by the builder cover the shipped elements only.
  • DxprBuilderService::getDxprElementsFolders() and its hook_dxpr_builder_elements_folders_alter() return a list of elements folders in themes, but nothing in the editor reads that list. It is not a registration route.
  • Style and Animation tabs are added for you by register_animated_element(); do not redefine those parameters.

What's next?

  • Hooks API for the PHP hooks that add utility classes and icon sets.
  • Architecture for how the editor and the field formatter fit together.
Something wrong or missing on this page? Report it or edit the page.