Declarative DOM Element Creation
Building user interfaces in JavaScript often involves creating and manipulating DOM elements. dd<el> provides a simple yet powerful approach to element creation that is declarative, chainable, and maintains a clean syntax close to HTML structure.
dd<el> Elements: Key Benefits
- Declarative element creation with intuitive property assignment
- Chainable methods for natural DOM tree construction
- Simplified component patterns for code reuse
- Normalized declarative property/attribute handling across browsers
- Smart element return values for cleaner code flow
// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm.js
import {
assign,
el, createElement,
elNS, createElementNS
} from "deka-dom-el";
el===createElement
elNS===createElementNS
// “internal” utils
import {
assignAttribute,
classListDeclarative,
chainableAppend
} from "deka-dom-el";
# Creating Elements: Native vs dd<el>
In standard JavaScript, you create DOM elements using the document.createElement()
method and then set properties individually or with Object.assign()
:
Native DOM API
// Create element with properties
const button = document.createElement('button');
button.textContent = "Click me";
button.className = "primary";
button.disabled = true;
// Or using Object.assign()
const button2 = Object.assign(
document.createElement('button'),
{
textContent: "Click me",
className: "primary",
disabled: true
}
);
// Add to DOM
document.body.append(button);
document.body.append(button2);
dd<el> Approach
import { el } from "deka-dom-el";
// Create element with properties
const button = el("button", {
textContent: "Click me",
className: "primary",
disabled: true,
});
// Shorter and more expressive
// than the native approach
// Add to DOM
document.body.append(button);
The el
function provides a simple wrapper around document.createElement
with enhanced property assignment.
import { el, assign } from "./esm-with-signals.js";
const color= "lightcoral";
document.body.append(
el("p", { textContent: "Hello (first time)", style: { color } })
);
document.body.append(
assign(
document.createElement("p"),
{ textContent: "Hello (second time)", style: { color } }
)
);
# Advanced Property Assignment
The assign
function is the heart of dd<el>’s element property handling. It is internally used to assign properties using the el
function. assign
provides intelligent assignment of both properties (IDL) and attributes:
- Property vs Attribute Priority
- Prefers IDL properties, falls back to setAttribute() when no writable property exists
- Data and ARIA Attributes
- Both
dataset.keyName
anddataKeyName
syntaxes are supported (same foraria
/ariaset
) - Style Handling
- Accepts string or object notation for
style
property - Class Management
- Works with
className
(class
) andclassList
object for toggling classes - Force Modes
- Use
=
prefix to force attribute mode,.
prefix to force property mode - Attribute Removal
- Pass
undefined
to remove a property or attribute
import { assign, assignAttribute, classListDeclarative } from "./esm-with-signals.js";
const paragraph= document.createElement("p");
assignAttribute(paragraph, "textContent", "Hello, world!");
assignAttribute(paragraph, "style", "color: red; font-weight: bold;");
assignAttribute(paragraph, "style", { color: "navy" });
assignAttribute(paragraph, "dataTest1", "v1");
assignAttribute(paragraph, "dataset", { test2: "v2" });
assign(paragraph, { //textContent and style see above
ariaLabel: "v1", //data* see above
ariaset: { role: "none" }, // dataset see above
"=onclick": "console.log(event)",
onmouseout: console.info,
".something": "something",
classList: {} //see below
});
classListDeclarative(paragraph, {
classAdd: true,
classRemove: false,
classAdd1: 1,
classRemove1: 0,
classToggle: -1
});
console.log(paragraph.outerHTML);
console.log("paragraph.something=", paragraph.something);
document.body.append(
paragraph
);
You can explore standard HTML element properties in the MDN documentation for HTMLElement (base class) and specific element interfaces like HTMLParagraphElement.
# Building DOM Trees with Chainable Methods
One of the most powerful features of dd<el> is its approach to building element trees. Unlike the native DOM API which doesn’t return the parent after append()
, dd<el>’s append()
always returns the parent element:
❌ Native DOM API
// Verbose, needs temp variables
const div = document.createElement('div');
const h1 = document.createElement('h1');
h1.textContent = 'Title';
div.append(h1);
const p = document.createElement('p');
p.textContent = 'Paragraph';
div.append(p);
// append doesn't return parent
// so chaining is not possible
// Add to DOM
document.body.append(div);
✅ dd<el> Approach
import { el } from "deka-dom-el";
// Chainable, natural nesting
// append() returns parent element
// making chains easy and intuitive
document.body.append(
el("div").append(
el("h1", "Title"),
el("p", "Paragraph"),
),
);
This chainable pattern is much cleaner and easier to follow, especially for deeply nested elements. It also makes it simple to add multiple children to a parent element in a single fluent expression.
import { el } from "./esm-with-signals.js";
document.head.append(
el("style").append(
"tr, td{ border: 1px solid red; padding: 1em; }",
"table{ border-collapse: collapse; }"
)
);
document.body.append(
el("p", "Example of a complex template. Using for example nesting lists:"),
el("ul").append(
el("li", "List item 1"),
el("li").append(
el("ul").append(
el("li", "Nested list item 1"),
)
)
),
el("table").append(
el("tr").append(
el("td", "Row 1 – Col 1"),
el("td", "Row 1 – Col 2")
)
)
);
import { chainableAppend } from "./esm-with-signals.js";
/**
* @template {keyof HTMLElementTagNameMap} TAG
* @param {TAG} tag
* @returns {ddeHTMLElementTagNameMap[TAG] extends HTMLElement ? ddeHTMLElementTagNameMap[TAG] : ddeHTMLElement}
* */
const createElement= tag=> chainableAppend(document.createElement(tag));
document.body.append(
createElement("p").append(
createElement("em").append(
"You can also use `chainableAppend`!"
)
)
);
# Using Components to Build UI Fragments
The el
function is overloaded to support both tag names and function components. This lets you refactor complex UI trees into reusable pieces:
import { el } from "./esm-with-signals.js";
document.head.append(
el("style").append(
".class1{ font-weight: bold; }",
".class2{ color: purple; }"
)
);
document.body.append(
el(component, { className: "class2", textContent: "Hello World!" }),
component({ className: "class2", textContent: "Hello World!" })
);
function component({ className, textContent }){
return el("div", { className: [ "class1", className ].join(" ") }).append(
el("p", textContent)
);
}
Component functions receive the properties object as their first argument, just like regular elements. This makes it easy to pass data down to components and create reusable UI fragments.
It’s helpful to use naming conventions similar to native DOM elements for your components. This allows you to keeps your code consistent with the DOM API.
Use destructuring assignment to extract the properties from the props
and pass them to the component element: function component({ className }){ return el("p", { className }); }
for make templates cleaner.
# Working with SVG and Other Namespaces
For non-HTML elements like SVG, MathML, or custom namespaces, dd<el> provides the elNS
function which corresponds to the native document.createElementNS
:
import { elNS, assign } from "./esm-with-signals.js";
const elSVG= elNS("http://www.w3.org/2000/svg");
const elMath= elNS("http://www.w3.org/1998/Math/MathML");
document.body.append(
elSVG("svg"), // see https://developer.mozilla.org/en-US/docs/Web/SVG and https://developer.mozilla.org/en-US/docs/Web/API/SVGElement // editorconfig-checker-disable-line
elMath("math") // see https://developer.mozilla.org/en-US/docs/Web/MathML and https://developer.mozilla.org/en-US/docs/Web/MathML/Global_attributes // editorconfig-checker-disable-line
);
console.log(
document.body.innerHTML.includes("<svg></svg><math></math>")
)
This function returns a namespace-specific element creator, allowing you to work with any element type using the same consistent interface.
# Best Practices for Declarative DOM Creation
- Use component functions for reusable UI fragments: Extract common UI patterns into reusable functions that return elements.
- Leverage destructuring for cleaner component code: Use destructuring to extract properties from the props object for cleaner component code.
- Leverage chainable methods for better performance: Use chainable methods
.append()
to build complex DOM trees for better performance and cleaner code.
# Mnemonic
assign(<element>, ...<objects>): <element>
— assign properties (prefered, or attributes) to the elementel(<tag-name>, <primitive>)[.append(...)]: <element-from-tag-name>
— simple element containing only text (accepts string, number or signal)el(<tag-name>, <object>)[.append(...)]: <element-from-tag-name>
— element with more properties (prefered, or attributes)el(<function>, <function-argument(s)>)[.append(...)]: <element-returned-by-function>
— using component represented by function (must accept object like for <tag-name>)el(<...>, <...>, ...<addons>)
— see following section of documentationelNS(<namespace>)(<as-el-see-above>)[.append(...)]: <element-based-on-arguments>
— typically SVG elements