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 and dataKeyName syntaxes are supported (same for aria/ariaset)
Style Handling
Accepts string or object notation for style property
Class Management
Works with className (class) and classList 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

  1. Use component functions for reusable UI fragments: Extract common UI patterns into reusable functions that return elements.
  2. Leverage destructuring for cleaner component code: Use destructuring to extract properties from the props object for cleaner component code.
  3. 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 element
  • el(<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 documentation
  • elNS(<namespace>)(<as-el-see-above>)[.append(...)]: <element-based-on-arguments> — typically SVG elements