├── .gitignore
├── LICENSE
├── package.json
├── index.min.js
├── index.html
├── index.js
├── README.md
└── types.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright © 2025-today, Andrea Giammarchi, @WebReflection
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the “Software”), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software
10 | is furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included
13 | in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21 | IN THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@webreflection/element",
3 | "version": "0.2.3",
4 | "description": "A minimalistic DOM element creation library.",
5 | "main": "index.js",
6 | "module": "index.js",
7 | "types": "types.ts",
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/WebReflection/element.git"
11 | },
12 | "exports": {
13 | ".": "./index.js",
14 | "./min": "./index.min.js",
15 | "./package.json": "./package.json"
16 | },
17 | "scripts": {
18 | "build": "terser index.js -o index.min.js --compress --mangle --module"
19 | },
20 | "files": [
21 | "index.js",
22 | "index.min.js",
23 | "types.ts",
24 | "README.md",
25 | "LICENSE"
26 | ],
27 | "keywords": [
28 | "minimalistic",
29 | "dom",
30 | "element",
31 | "creation",
32 | "library"
33 | ],
34 | "author": "Andrea Giammarchi",
35 | "license": "MIT",
36 | "type": "module",
37 | "bugs": {
38 | "url": "https://github.com/WebReflection/element/issues"
39 | },
40 | "homepage": "https://github.com/WebReflection/element#readme",
41 | "devDependencies": {
42 | "install": "^0.13.0",
43 | "terser": "^5.44.0"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/index.min.js:
--------------------------------------------------------------------------------
1 | const{isArray:e}=Array,{stringify:t}=JSON,{assign:s}=Object,{ownKeys:i}=Reflect,n={};export default(a,c,...r)=>{let o,l=(c=c??n).document||document,f=!1;if("string"==typeof a)if(a.startsWith("<")){if(o=l.querySelector(a.slice(1)),!o)return null}else{const e="svg"===a;o=e||a.startsWith("svg:")?l.createElementNS("http://www.w3.org/2000/svg",e?a:a.slice(4)):(f=!!c.is)?l.createElement(a,{is:c.is}):l.createElement(a)}else o=a;for(let n of i(c)){if(f&&"is"===n)continue;let a=c[n];if(!(n in o))switch(n){case"aria":for(let e of i(a))o.setAttribute("role"===e?e:`aria-${e.toLowerCase()}`,a[e]);continue;case"data":s(o.dataset,a);continue;case"style":isSVG?o.setAttribute("style",a):o.style.cssText=a;continue;case"class":n="className";break;case"html":n="innerHTML";break;case"text":n="textContent"}if(n in o)if("classList"===n)o.classList.add(...a);else try{o[n]=a}catch{o.setAttribute(n,a)}else{switch(!0){case n.startsWith("?"):a=!!a;case n.startsWith("@"):if(n=n.slice(1),e(a)){o.addEventListener(n,...a);continue}}switch(typeof a){case"boolean":o.toggleAttribute(n,a);continue;case"undefined":case"object":if(!a)continue;if("function"!=typeof a.handleEvent){a=t(a);break}case"function":o.addEventListener(n,a);continue}o.setAttribute(n,a)}}return r.length&&o.append(...r),o};
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | @webreflection/element
7 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /** @import { ChildNodes, Options, Output, Tag } from "./types.ts"; */
2 |
3 | const { isArray } = Array;
4 | const { stringify } = JSON;
5 | const { assign } = Object;
6 | const { ownKeys } = Reflect;
7 | const empty = {};
8 |
9 | /**
10 | * @template {Tag} PassedTag
11 | * @param {PassedTag} tag - The tag name of the element to create or the element to use. If the name starts with `<`, it will be treated as a query selector and the first matching element will be used, if any.
12 | * @param {Options?} [options] - The options object.
13 | * @param {ChildNodes} childNodes - The optional child nodes to append to the element.
14 | * @returns {Output}
15 | */
16 | export default (tag, options, ...childNodes) => {
17 | options ??= empty;
18 | let doc = options.document || document, custom = false, node;
19 | // if `tag` is a string, create a new element, or ...
20 | if (typeof tag === 'string') {
21 | // if tag starts with `<`, use querySelector instead
22 | if (tag.startsWith('<')) {
23 | node = doc.querySelector(tag.slice(1));
24 | // return null if no node is found
25 | if (!node) return null;
26 | }
27 | else {
28 | // create either an SVG or HTML element
29 | // for svg it's either `svg` itself or `svg:` followed by the tag name
30 | const isSVG = tag === 'svg';
31 | const isNS = isSVG || tag.startsWith('svg:');
32 | node = isNS ?
33 | doc.createElementNS(
34 | 'http://www.w3.org/2000/svg',
35 | isSVG ? tag : tag.slice(4),
36 | ) :
37 | ((custom = !!options.is) ?
38 | doc.createElement(tag, { is: options.is }) :
39 | doc.createElement(tag))
40 | ;
41 | }
42 | }
43 | // otherwise, use the provided node
44 | else node = tag;
45 |
46 | // loop through options keys and symbols
47 | for (let key of ownKeys(options)) {
48 | if (custom && key === 'is') continue;
49 | let value = options[key];
50 | // if `key` is not a node known property ...
51 | if (!(key in node)) {
52 | // handle with ease intents: `aria`, `data`, `style`, `html`, `text`
53 | switch (key) {
54 | case 'aria': {
55 | for (let k of ownKeys(value)) {
56 | node.setAttribute(
57 | k === 'role' ? k : `aria-${k.toLowerCase()}`,
58 | value[k],
59 | );
60 | }
61 | continue;
62 | }
63 | case 'data': {
64 | assign(node.dataset, value);
65 | continue;
66 | }
67 | case 'style': {
68 | if (isSVG) node.setAttribute('style', value);
69 | else node.style.cssText = value;
70 | continue;
71 | }
72 | case 'class': {
73 | key = 'className';
74 | break;
75 | }
76 | case 'html': {
77 | key = 'innerHTML';
78 | break;
79 | }
80 | case 'text': {
81 | key = 'textContent';
82 | break;
83 | }
84 | }
85 | }
86 | // if `key` is a node known property ...
87 | if (key in node) {
88 | switch (key) {
89 | case 'classList': {
90 | node.classList.add(...value);
91 | break;
92 | }
93 | default: {
94 | // try to set the value directly
95 | try {
96 | node[key] = value;
97 | }
98 | // otherwise set the value as attribute (svg friendly)
99 | catch {
100 | node.setAttribute(key, value);
101 | }
102 | }
103 | }
104 | continue;
105 | }
106 |
107 | // uhtml / lit style attributes hints friendly
108 | switch (true) {
109 | case key.startsWith('?'):
110 | value = !!value;
111 | case key.startsWith('@'): {
112 | key = key.slice(1);
113 | // allow passing options within the listener
114 | if (isArray(value)) {
115 | node.addEventListener(key, ...value);
116 | continue;
117 | }
118 | break;
119 | }
120 | }
121 |
122 | // decide what to do by inferring the value type
123 | switch (typeof value) {
124 | // toggle boolean attributes
125 | case 'boolean': {
126 | node.toggleAttribute(key, value);
127 | continue;
128 | }
129 | // ignore `null` or `undefined`
130 | case 'undefined':
131 | case 'object': {
132 | if (!value) continue;
133 | // yet consider `handleEvent` as a function
134 | if (typeof value.handleEvent !== 'function') {
135 | // otherwise, stringify the value as JSON
136 | value = stringify(value);
137 | break;
138 | }
139 | }
140 | // listeners as functions or handleEvent based references
141 | case 'function': {
142 | node.addEventListener(key, value);
143 | continue;
144 | }
145 | }
146 | // last resort: set the value as attribute
147 | node.setAttribute(key, value);
148 | }
149 |
150 | if (childNodes.length) node.append(...childNodes);
151 |
152 | return node;
153 | };
154 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @webreflection/element
2 |
3 | **Social Media Photo by [James Owen](https://unsplash.com/@jhjowen) on [Unsplash](https://unsplash.com/)**
4 |
5 |
6 | A minimalistic DOM element creation library.
7 |
8 | ## Usage and Description
9 |
10 | The *default* export is a function that accepts a `tag` and an optional `options` or `setup` literal, plus zero, one or more *childNodes* to append: `(tag:string|Node, options:object?, ...(Node|string)[])`
11 |
12 | ### The `tag`
13 |
14 | * if it's an *Element* already it uses options to enrich the element as described
15 | * if it's a `string` and it does not start with `<`, it creates a new *Element* with such name
16 | * if it starts with `svg:` (followed by its name) or the `tag` value is `svg` itself, it creates an *SVGElement*
17 | * in every other case it creates an *HTMLElement* or, of course, a *CustomElement* with such name or, if `options.is` exists, a custom element builtin extend
18 | * if it's a `string` and it starts with `<` it uses the element found after `document.querySelector`. If no element is found, it returns `null` out of the box.
19 |
20 | ### The `options`
21 |
22 | Each option `key` / `value` pair is handled to enrich the created or retrieved element in a library friendly way.
23 |
24 |
25 | #### The `key`
26 |
27 | * if `key in element` is `false`:
28 | * **aria** and **data** are used to attach `aria-` prefixed attributes (with the `role` exception) or the element `dataset`
29 | * **class**, **html** and **text** are transformed into `className`, `innerHTML` and `textContent` to directly set these properties with less, yet semantic, typing
30 | * **@type** is treated as *listener* intent. If its value is an *array*, it is possible to add the third parameter to `element.addEventListener(key.slice(1), ...value)`, otherwise the listener will be added without options
31 | * **?name** is treated as boolean attribute intent and, like it is for *@type*, the key will see the first char removed
32 | * if `key in element` is `true`:
33 | * **classList** adds all classes via `element.classList.add(...value)`
34 | * **style** content is directly set via `element.style.cssText = value` or via `element.setAttribute('style', value)` in case of *SVG* element
35 | * everything else, including **on...** handlers, is attached directly via `element[key] = value`
36 |
37 |
38 | #### The `value`
39 |
40 | If `key in element` is `false`, the behavior is inferred by the value:
41 |
42 | * a `boolean` value that is not known in the *element* will be handled via `element.toggleAttribute(key, value)`
43 | * a `function` or an `object` with a `handleEvent` are handled via `element.addEventListener(key, value)`
44 | * an `object` without `handleEvent` will be serialized as *JSON* to safely land as `element.setAttribute(key, JSON.stringify(value))`
45 | * `null` and `undefined` are simply ignored
46 | * everything else is simply added as `element.setAttribute(key, value)`
47 |
48 | Please read the [example](#example) to have more complete example of how all these features play together.
49 |
50 | - - -
51 |
52 | ## Example - [Live Demo](https://webreflection.github.io/element/)
53 |
54 | ```js
55 | // https://cdn.jsdelivr.net/npm/@webreflection/element/index.min.js for best compression
56 | import element from 'https://esm.run/@webreflection/element';
57 |
58 | // direct node reference or `< css-selector` to enrich, ie:
59 | // element(document.body, ...) or ...
60 | element(
61 | '< body',
62 | {
63 | // override body.style.cssText = ...
64 | style: 'text-align: center',
65 | // classList.add('some', 'container')
66 | classList: ['some', 'container'],
67 | // a custom listener as object.handleEvent pattern
68 | ['custom:event']: {
69 | count: 0,
70 | handleEvent({ type, currentTarget }) {
71 | console.log(++this.count, type, currentTarget);
72 | },
73 | },
74 | // listener with an extra { once: true } option
75 | ['@click']: [
76 | ({ type, currentTarget }) => {
77 | console.log(type, currentTarget);
78 | currentTarget.dispatchEvent(new Event('custom:event'))
79 | },
80 | { once: true },
81 | ],
82 | },
83 | // body children / childNodes
84 | element('h1', {
85 | // clallName
86 | class: 'name',
87 | // textContent
88 | text: '@webreflection/element',
89 | style: 'color: purple',
90 | // role="heading" aria-level="1"
91 | aria: {
92 | role: 'heading',
93 | level: 1,
94 | },
95 | // dataset.test = 'ok'
96 | data: {
97 | test: 'ok',
98 | },
99 | // serialized as `json` attribute
100 | json: {a: 1, b: 2},
101 | // direct listener
102 | onclick: ({ type, currentTarget }) => {
103 | console.log(type, currentTarget);
104 | },
105 | }),
106 | element(
107 | 'svg',
108 | {
109 | width: 100,
110 | height: 100,
111 | },
112 | // svg children / childNodes
113 | element('svg:circle', {
114 | cx: 50,
115 | cy: 50,
116 | r: 50,
117 | fill: 'violet',
118 | })
119 | ),
120 | element('p', {
121 | // innerHTML
122 | html: 'made with ❤️ for the Web',
123 | })
124 | );
125 | ```
126 |
--------------------------------------------------------------------------------
/types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * List of possible aria attributes.
3 | * @see [ARIA Attributes Reference](https://mdn.io/ARIA/Reference/Attributes)
4 | */
5 | export type AriaAttribute =
6 | | "activedescendant"
7 | | "atomic"
8 | | "autocomplete"
9 | | "braillelabel"
10 | | "brailleroledescription"
11 | | "busy"
12 | | "checked"
13 | | "colcount"
14 | | "colindex"
15 | | "colindextext"
16 | | "colspan"
17 | | "controls"
18 | | "current"
19 | | "describedby"
20 | | "description"
21 | | "details"
22 | | "disabled"
23 | | "errormessage"
24 | | "expanded"
25 | | "flowto"
26 | | "haspopup"
27 | | "hidden"
28 | | "invalid"
29 | | "keyshortcuts"
30 | | "label"
31 | | "labelledby"
32 | | "level"
33 | | "live"
34 | | "modal"
35 | | "multiline"
36 | | "multiselectable"
37 | | "orientation"
38 | | "owns"
39 | | "placeholder"
40 | | "posinset"
41 | | "pressed"
42 | | "readonly"
43 | | "relevant"
44 | | "required"
45 | | "role"
46 | | "roledescription"
47 | | "rowcount"
48 | | "rowindex"
49 | | "rowindextext"
50 | | "rowspan"
51 | | "selected"
52 | | "setsize"
53 | | "sort"
54 | | "valuemax"
55 | | "valuemin"
56 | | "valuenow"
57 | | "valuetext";
58 |
59 | /**
60 | * Object to set aria labels.
61 | */
62 | export type AriaLabels = Readonly> &
63 | Partial>>;
64 |
65 | /**
66 | * Type of tag used to create custom elements.
67 | */
68 | export type CustomElementTagName = `${string}-${string}`;
69 |
70 | /**
71 | * Type of tag used to create HTML elements.
72 | */
73 | export type HTMLElementTag = keyof HTMLElementTagNameMap;
74 |
75 | /**
76 | * Type of tag used to create SVG elements.
77 | */
78 | export type SVGElementTag = "svg" | `svg:${Exclude}`
79 |
80 | /**
81 | * Type of tag used to select an existing element.
82 | */
83 | export type QuerySelectTag = `<${string}`;
84 |
85 | /**
86 | * Type of `tag` argument on main function.
87 | */
88 | export type Tag =
89 | | CustomElementTagName
90 | | Element
91 | | HTMLElementTag
92 | | QuerySelectTag
93 | | SVGElement
94 | | SVGElementTag;
95 |
96 | /**
97 | * Type of `options` argument on main function.
98 | */
99 | export type Options = Readonly> & Readonly<{
100 | /**
101 | * An optional literal describing `aria` attributes such as `role` or `level`
102 | * or `labelledby`.
103 | */
104 | aria?: AriaLabels;
105 | /**
106 | * The optional class to set to the element. as `className`.
107 | */
108 | class?: string;
109 | /**
110 | * The optional class list to add to the element.
111 | */
112 | classList?: readonly string[];
113 | /**
114 | * An optional literal describing `dataset` properties.
115 | */
116 | data?: DOMStringMap;
117 | /**
118 | * An optional document to use, defaults to the global `document`.
119 | */
120 | document?: Document;
121 | /**
122 | * An optional builtin extend custom element name.
123 | */
124 | is?: CustomElementTagName;
125 | /**
126 | * The optional html to set to the element. as `innerHTML`.
127 | */
128 | html?: string;
129 | /**
130 | * The optional text to set to the element. as `textContent`.
131 | */
132 | text?: string;
133 | /**
134 | * The optional style to apply to the element.
135 | */
136 | style?: string;
137 | }>;
138 |
139 | /**
140 | * Type of `...childNodes` argument on main function.
141 | */
142 | export type ChildNodes = readonly (
143 | | DocumentFragment
144 | | Element
145 | | Node
146 | | SVGElement
147 | | string
148 | )[];
149 |
150 | /**
151 | * Tries to infer the type of a Selector, asumes `Element | SVGElement` if can't
152 | * be fount on maps.
153 | */
154 | export type QuerySelect =
155 | | (Selector extends keyof HTMLElementTagNameMap
156 | ? HTMLElementTagNameMap[Selector]
157 | : Selector extends keyof SVGElementTagNameMap
158 | ? SVGElementTagNameMap[Selector]
159 | : Element | SVGElement)
160 | | null;
161 |
162 | /**
163 | * Extracts selector from given {@linkcode QuerySelectTag}.
164 | */
165 | export type ExtractSelector =
166 | SelectorTag extends `<${infer Selector}` ? Selector : never;
167 |
168 | /**
169 | * Strips "svg:" prepend from given {@linkcode SVGElementTag}.
170 | */
171 | export type ExtractSVGTag = SVGTag extends "svg"
172 | ? SVGTag
173 | : SVGTag extends `svg:${infer Tag}`
174 | ? Tag extends keyof SVGElementTagNameMap
175 | ? Tag
176 | : never
177 | : never;
178 |
179 | /**
180 | * Output type that follows the following logic:
181 | *
182 | * - If the passed `tag` is a `string`:
183 | * - If the string starts witn `<` tries to infer the type the same way TS infers from `document.querySelector`.
184 | * - If the string is `svg` or starts with `svg:` it infers the type of the SVGElement.
185 | * - If the string is a known HTMLElement, it infers the type from that.
186 | * - For any other tring, it just asumes it's `Element`.
187 | * - If the passed `tag` is anything other than `string`, then it infers the type is the same as the received element.
188 | */
189 | export type Output = PassedTag extends string
190 | ? PassedTag extends HTMLElementTag
191 | ? HTMLElementTagNameMap[PassedTag]
192 | : PassedTag extends SVGElementTag
193 | ? SVGElementTagNameMap[ExtractSVGTag]
194 | : PassedTag extends QuerySelectTag
195 | ? QuerySelect>
196 | : Element
197 | : PassedTag;
198 |
--------------------------------------------------------------------------------