- Home
- Extend
- Build on it
- Custom elements
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:
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.
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:
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"));
},
});
- Insert the file into the editor library between
dxpr_elements.js(weight-4) anddxpr_builder.js(weight-3) inmymodule.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
- 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.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.