Native JavaScript DOM elements creations
Let’s go through all patterns we would like to use and what needs to be improved for better experience.
// 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 element(s) (with custom attributes)
You can create a native DOM element by using the document.createElement()
. It is also possible to provide a some attribute(s) declaratively using Object.assign()
. More precisely, this way you can sets some IDL also known as a JavaScript property.
document.body.append(
document.createElement("div")
);
console.log(
"Emty div is generated inside <body>:",
document.body.innerHTML.includes("<div></div>")
);
document.body.append(
Object.assign(
document.createElement("p"),
{ textContent: "Element’s text content.", style: "color: coral;" }
)
);
To make this easier, you can use the el
function. Internally in basic examples, it is wrapper around assign(document.createElement(…), { … })
.
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 } }
)
);
The assign
function provides improved behaviour of Object.assign()
. You can declaratively sets any IDL and attribute of the given element. Function prefers IDL and fallback to the element.setAttribute
if there is no writable property in the element prototype.
You can study all JavaScript elements interfaces to the corresponding HTML elements. All HTML elements inherits from HTMLElement. To see all available IDLs for example for paragraphs, see HTMLParagraphElement. Moreover, the assign
provides a way to sets declaratively some convenient properties:
- It is possible to sets
data-*
/aria-*
attributes using object notation. - In opposite, it is also possible to sets
data-*
/aria-*
attribute using camelCase notation. - You can use string or object as a value for
style
property. className
(IDL – preffered)/class
are ways to add CSS class to the element. You can use string (similarly toclass="…"
syntax in HTML) or array of strings. This is handy to concat conditional classes.- Use
classList
to toggle specific classes. This will be handy later when the reactivity via signals is beeing introduced. - The
assign
also accepts theundefined
as a value for any property to remove it from the element declaratively. Also for some IDL the corresponding attribute is removed as it can be confusing. For example, natievly the element’sid
is removed by setting the IDL to an empty string. - You can use
=
or.
to force processing given key as attribute/property of the element.
For processing, the assign
internally uses assignAttribute
and classListDeclarative
.
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
);
# Native JavaScript templating
By default, the native JS has no good way to define HTML template using DOM API:
document.body.append(
document.createElement("div"),
document.createElement("span"),
document.createElement("main")
);
console.log(document.body.innerHTML.includes("<div></div><span></span><main></main>"));
const template= document.createElement("main").append(
document.createElement("div"),
document.createElement("span"),
);
console.log(typeof template==="undefined");
This library therefore overwrites the append
method of created elements to always return parent element.
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";
/** @param {keyof HTMLElementTagNameMap} tag */
const createElement= tag=> chainableAppend(document.createElement(tag));
document.body.append(
createElement("p").append(
createElement("em").append(
"You can also use `chainableAppend`!"
)
)
);
# Basic (state-less) components
You can use functions for encapsulation (repeating) logic. The el
accepts function as first argument. In that case, the function should return dom elements and the second argument for el
is argument for given element.
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 ] }).append(
el("p", textContent)
);
}
As you can see, in case of state-less/basic components there is no difference between calling component function directly or using el
function.
It is nice to use similar naming convention as native DOM API. This allows us to use the destructuring assignment syntax and keep track of the native API (things are best remembered through regular use).
# Creating non-HTML elements
Similarly to the native DOM API (document.createElementNS
) for non-HTML elements we need to tell JavaScript which kind of the element to create. We can use the elNS
function:
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
elMath("math") // see https://developer.mozilla.org/en-US/docs/Web/MathML and https://developer.mozilla.org/en-US/docs/Web/MathML/Global_attributes
);
console.log(
document.body.innerHTML.includes("<svg></svg><math></math>")
)
# Mnemonic
assign(<element>, ...<idl-objects>): <element>
— assign properties to the elementel(<tag-name>, <primitive>)[.append(...)]: <element-from-tag-name>
— simple element containing only textel(<tag-name>, <idl-object>)[.append(...)]: <element-from-tag-name>
— element with more propertiesel(<function>, <function-argument(s)>)[.append(...)]: <element-returned-by-function>
— using component represented by functionel(<...>, <...>, ...<addons>)
— see following pageelNS(<namespace>)(<as-el-see-above>)[.append(...)]: <element-based-on-arguments>
— typically SVG elements