diff --git a/README.md b/README.md index 8e6c2ba..1575841 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,113 @@ **WIP** (the experimentation phase) | [source code on Gitea](https://gitea.jaandrle.cz/jaandrle/deka-dom-el) | [*mirrored* on GitHub](https://github.com/jaandrle/deka-dom-el) # Deka DOM Elements -This is reimplementation of [jaandrle/dollar_dom_component: Functional DOM components without JSX and virtual DOM. Subrepository for https://github.com/jaandrle/jaaJSU ($dom namespace)](https://github.com/jaandrle/dollar_dom_component). +This is reimplementation of [jaandrle/dollar_dom_component: Functional DOM components without JSX and virtual DOM.](https://github.com/jaandrle/dollar_dom_component). The goal is to be even more close to the native JavaScript. + +# 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. + +## Creating element and DOM templates natively +```js +document.body.append( + document.createElement("div"), + document.createElement("span"), + document.createElement("main") +); +//=> HTML output:
+const template= document.createElement("main").append( + document.createElement("div"), + document.createElement("span"), +); +//=> ★:: typeof template==="undefined" +``` +**Pitfalls**: +- there is lots of text +- `.append` methdo returns `void`⇒ it cannot be chained (see ★) + +## Set properties of created element +```js +const element= Object.assign(document.createElement("p"), { className: "red", textContent: "paragraph" }); +document.body.append(element); +//=> HTML output:

paragraph

+``` +**Pitfalls**: +- there is lots of text +- `Object.assign` isn’t ideal as it can set only (some) [IDL](https://developer.mozilla.org/en-US/docs/Glossary/IDL) + +# Events and dynamic parts +```js +const input= document.createElement("input"); +const output= document.createElement("output"); +input.addEventListener("change", function(event){ + output.value= event.target.value; +}); +document.body.append( + output, + input +); +//=> HTML output: +``` +**Pitfalls**: +- there is lots of text +- very hard to organize code + +# Helpers and modifications +Now, let's introduce library helpers and modifications. + +## `.append` +The `.append` is owerwrote to always returns element. This seem to be the best way to do it as it is very hard +to create Proxy around `HTMLElement`, …. +```js +document.body.append( + document.createElement("main").append( + document.createElement("div"), + document.createElement("span"), + ) +); +//=> HTML output:
+``` + +## `el` and `assign` functions +```js +const element= assign(document.createElement("a"), { + className: "red", + dataTest: "test", + href: "www.seznam.cz", + textContent: "Link", + style: { color: "blue" } +}); +document.body.append(element); +assign(element, { style: undefined }); +//=> HTML output: Link +``` +…but for elements/template creations `el` is even better: +```js +document.body.append( + el("div").append( + el("p").append( + el("#text", { textContent: "Link: " }), + el("a", { + href: "www.seznam.cz", + textContent: "example", + }) + ) + ) +); +``` + +## Events and dynamic parts +```js +const output_dynamic= (function(){ + const element= el("span", { style: { fontWeight: "bold" }, textContent: "" }); + return { + element, + onchange: listen("change", event=> assign(element, { textContent: event.target.value })) + }; +})(); +document.body.append( + output_dynamic.element, + el("input", { type: "text" }, output_dynamic.onchange) +); +``` diff --git a/index.js b/index.js index 39efeaa..d4732cd 100644 --- a/index.js +++ b/index.js @@ -2,74 +2,5 @@ const { append }= c.prototype; c.prototype.append= function(...els){ append.apply(this, els); return this; }; }); - -export function createElement(tag, attributes){ - if(tag==="<>") return document.createDocumentFragment(); - if(tag==="") return document.createTextNode(attributes.textContent ?? attributes.innerText ?? attributes.innerHTML); - return assign(document.createElement(tag), attributes); -} -export { createElement as el }; -export function createElementNS(tag, attributes, attributes_todo){ - let namespace= "svg"; - if(typeof attributes_todo !== "undefined"){ - namespace= tag; tag= attributes; attributes= attributes_todo; } - if(tag==="<>") return document.createDocumentFragment(); - if(tag==="") return document.createTextNode(attributes.textContent ?? attributes.innerText ?? attributes.innerHTML); - return assign(document.createElementNS(namespace==="svg" ? "http://www.w3.org/2000/svg" : namespace, tag), attributes); -} -export { createElementNS as elNS }; - -export function assign(element, ...attributes){ - // prefers https://developer.mozilla.org/en-US/docs/Glossary/IDL - if(!attributes.length) return element; - const is_svg= element instanceof SVGElement; - const setRemoveAttr= (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute"); - - Object.entries(Object.assign({}, ...attributes)).forEach(function([ key, attr ]){ - if(key[0]==="=") return setRemoveAttr(key.slice(1), attr); - if(key[0]===".") return setDelete(element, key.slice(1), attr); - if(typeof attr === "object"){ - switch(key){ - case "style": return forEachEntries(attr, setRemove.bind(null, element.style, "Property")) - case "dataset": return forEachEntries(attr, setDelete.bind(null, element.dataset)); - case "ariaset": return forEachEntries(attr, (key, val)=> setRemoveAttr("aria-"+key, val)); - case "classList": return classListDeclartive(element, attr); - default: return Reflect.set(element, key, attr); - } - } - if(/(aria|data)([A-Z])/.test(key)){ - key= key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); - return setRemoveAttr(key, attr); - } - switch(key){ - case "href" || "src" || "style": - return setRemoveAttr(key, attr); - case "xlink:href": - return setRemoveAttr(key, attr, "http://www.w3.org/1999/xlink"); - case "textContent" || "innerText": - if(!is_svg) break; - return element.appendChild(document.createTextNode(attr)); - } - if(key in element && !is_svg) - return setDelete(element, key, attr); - return setRemoveAttr(key, attr); - }); - return element; -} -export function classListDeclartive(element, toggle){ - if(typeof toggle !== "object") return element; - - forEachEntries(toggle, - (class_name, val)=> - element.classList.toggle(class_name, val===-1 ? undefined : Boolean(val))) - return element; -} - -export function empty(el){ Array.from(el.children).forEach(el=> el.remove()); return el; } - -function forEachEntries(obj, cb){ return Object.entries(obj).forEach(([ key, val ])=> cb(key, val)); } -function isUndef(value){ return typeof value==="undefined"; } - -function setRemove(obj, prop, key, val){ return obj[ (isUndef(val) ? "remove" : "set") + prop ](key, val); } -function setRemoveNS(obj, prop, key, val, ns= null){ return obj[ (isUndef(val) ? "remove" : "set") + prop + "NS" ](ns, key, val); } -function setDelete(obj, prop, val){ return Reflect[ isUndef(val) ? "deleteProperty" : "set" ](obj, prop, val); } +export * from "./src/dom.js"; +export * from "./src/events.js"; diff --git a/src/dom.js b/src/dom.js new file mode 100644 index 0000000..ec1fe71 --- /dev/null +++ b/src/dom.js @@ -0,0 +1,75 @@ +export function createElement(tag, attributes, ...connect){ + let el; + switch(true){ + case typeof tag==="function": el= tag(attributes || undefined); break; + case tag==="<>": el= document.createDocumentFragment(); break; + case tag==="#text": el= assign(document.createTextNode(""), attributes); break; + default: el= assign(document.createElement(tag), attributes); + } + connect.forEach(c=> c(el)); + return el; +} +export { createElement as el }; +export function createElementNS(tag, attributes, attributes_todo){ + let namespace= "svg"; + if(typeof attributes_todo !== "undefined"){ + namespace= tag; tag= attributes; attributes= attributes_todo; } + if(tag==="<>") return document.createDocumentFragment(); + if(tag==="") return document.createTextNode(attributes.textContent ?? attributes.innerText ?? attributes.innerHTML); + return assign(document.createElementNS(namespace==="svg" ? "http://www.w3.org/2000/svg" : namespace, tag), attributes); +} +export { createElementNS as elNS }; + +export function assign(element, ...attributes){ + if(!attributes.length) return element; + const is_svg= element instanceof SVGElement; + const setRemoveAttr= (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute"); + + Object.entries(Object.assign({}, ...attributes)).forEach(function([ key, attr ]){ + if(key[0]==="=") return setRemoveAttr(key.slice(1), attr); + if(key[0]===".") return setDelete(element, key.slice(1), attr); + if(typeof attr === "object"){ + switch(key){ + case "style": return forEachEntries(attr, setRemove.bind(null, element.style, "Property")) + case "dataset": return forEachEntries(attr, setDelete.bind(null, element.dataset)); + case "ariaset": return forEachEntries(attr, (key, val)=> setRemoveAttr("aria-"+key, val)); + case "classList": return classListDeclartive(element, attr); + default: return Reflect.set(element, key, attr); + } + } + if(/(aria|data)([A-Z])/.test(key)){ + key= key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); + return setRemoveAttr(key, attr); + } + switch(key){ + case "href" || "src" || "style": + return setRemoveAttr(key, attr); + case "xlink:href": + return setRemoveAttr(key, attr, "http://www.w3.org/1999/xlink"); + case "textContent" || "innerText": + if(!is_svg) break; + return element.appendChild(document.createTextNode(attr)); + } + if(key in element && !is_svg) + return setDelete(element, key, attr); + return setRemoveAttr(key, attr); + }); + return element; +} +export function classListDeclartive(element, toggle){ + if(typeof toggle !== "object") return element; + + forEachEntries(toggle, + (class_name, val)=> + element.classList.toggle(class_name, val===-1 ? undefined : Boolean(val))) + return element; +} + +export function empty(el){ Array.from(el.children).forEach(el=> el.remove()); return el; } + +function forEachEntries(obj, cb){ return Object.entries(obj).forEach(([ key, val ])=> cb(key, val)); } +function isUndef(value){ return typeof value==="undefined"; } + +function setRemove(obj, prop, key, val){ return obj[ (isUndef(val) ? "remove" : "set") + prop ](key, val); } +function setRemoveNS(obj, prop, key, val, ns= null){ return obj[ (isUndef(val) ? "remove" : "set") + prop + "NS" ](ns, key, val); } +function setDelete(obj, prop, val){ return Reflect[ isUndef(val) ? "deleteProperty" : "set" ](obj, prop, val); } diff --git a/src/events.js b/src/events.js new file mode 100644 index 0000000..d20f733 --- /dev/null +++ b/src/events.js @@ -0,0 +1,8 @@ +export function listen(event, listener, options){ + return element=> element.addEventListener(event, listener, options); +} +export function dispatch(event, detail){ + if(typeof event === "string") + event= typeof detail==="undefined" ? new Event(event) : new CustomEvent(event, { detail }); + return element=> element.dispatchEvent(event); +} diff --git a/test/index.js b/test/index.js index fa85fdb..35acdb6 100644 --- a/test/index.js +++ b/test/index.js @@ -1,17 +1,63 @@ -import { el, elNS, assign } from "../index.js"; -Object.assign(globalThis, { el, elNS, assign }); +import { el, elNS, assign, listen, dispatch } from "../index.js"; +Object.assign(globalThis, { el, elNS, assign, listen, dispatch }); -console.log(el("p", { className: "red", textContent: "Hello "})); -console.log(el("p", { className: "red", textContent: "Hello "}) instanceof HTMLParagraphElement); +const { style, css }= createStyle(); +globalThis.test= console.log; +const app= el(component, null, listen("change", globalThis.test)); +dispatch("change", "Peter")(app); +console.log(app, app instanceof HTMLDivElement); -document.head.append( - el("style", { textContent: ` - .red{ color: red; } - ` }) -) -document.body.append( - el("p", { className: "red" }).append( - el("", { textContent: "Hello " }), - el("strong", { textContent: "World" }) +document.head.append(style); +document.body.append(app); + +function component({ value= "World" }= {}){ + const name= "naiveForm"; + css` + .${name}{ + display: flex; + flex-flow: column nowrap; + } + .${name} input{ + margin-inline-start: .5em; + } + `; + + const output= (function(){ + const element= el("strong", { textContent: value }); + return { + element, + onchange: listen("change", function(event){ + assign(element, { textContent: event.target.value }); + }) + } + })(); + const input= (function(){ + const element= el("input", { type: "text", value }, output.onchange); + return { + element, + onchange: listen("change", function(event){ + assign(element, { value: event.detail }); + dispatch("change")(element); + }) + }; + })(); + return el("div", { className: name }, input.onchange).append( + el("p").append( + el("#text", { textContent: "Hello " }), + output.element, + ), + el("label").append( + el("#text", { textContent: "Set name:" }), + input.element + ) ) -); +} +function createStyle(){ + const style= el("style"); + return { + style, + css(...args){ + style.appendChild(el("#text", { textContent: String.raw(...args) })); + } + }; +}