├── .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 | --------------------------------------------------------------------------------