Creating Custom Elements.
Register your own content elements in JavaScript
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:
dxpr_templates.js: client-side twig.js templates compiled fromdxpr_builder/templates/.dxpr_global.js: shared helpers and the CKEditor 5 integration.dxpr_events.jsdxpr_param_types.js: pushes one object per input type ontowindow.dxprBuilder.dxpr_param_types.dxpr_elements.js: creates thewindow.dxprBuilder.dxpr_elementsarray and pushes one definition per element.dxpr_builder.js: the editor. When this file executes it callscreate_dxpr_elements(), which loops overwindow.dxprBuilder.dxpr_elementsand for each definition:- calls
register_animated_element(base, is_container, Class), so the class extendsAnimatedElement(Style tab, Animation tab, HTML ID, HTML classes and placement parameters); - appends the inherited parameters to the definition's
params; - converts every parameter through
make_param_type(), which matchesparam.typeagainst the registered parameter types; - mixes the definition's properties into the class prototype.
- calls
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 setssafe: true;- the Style tab parameters as inline
styleand classes, never asdata-azat-*attributes; data-azcnt="true"on the content element whenhas_contentis 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.
-
Create
mymodule/js/notice-element.js: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")); }, }); -
Insert the file into the editor library between
dxpr_elements.js(weight-4) anddxpr_builder.js(weight-3) inmymodule.module:/** * 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: FALSEanddefermatch 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. -
Clear the cache and open a builder page:
drush cr -
Add the element from the picker, set its text and colour, and save. The field stores:
<div class="az-element az-notice" id="..." data-azb="az_notice" data-azat-text="Hello" data-azat-notice_color="%23198754"> <p style="color: #198754;">Hello</p> </div>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.phpandsrc/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-iconclasses shipped by the builder cover the shipped elements only. DxprBuilderService::getDxprElementsFolders()and itshook_dxpr_builder_elements_folders_alter()return a list ofelementsfolders 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.