diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cae4f4c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,26 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = tab +trim_trailing_whitespace = true +max_line_length = 120 + +[*.json] +max_line_length = unset + +[*.yml] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[LICENSE] +max_line_length = unset + +[dist/**] +indent_style = unset +max_line_length = unset +trim_trailing_whitespace = unset diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..11e01b9 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,23 @@ +# https://nektosact.com/usage/index.html +# https://github.com/reviewdog/action-eclint +name: On PR +on: + workflow_dispatch: + pull_request: + branches: [main] + +jobs: + pr: + name: Validates formatting and linting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: reviewdog/action-eclint@d51e853275e707b64c0526881ada324f454c1110 # v1.7.1 + with: + reporter: github-pr-check + eclint_flags: '--fix' + - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + with: + node-version: 20.16 + - run: npm ci + - run: bs/lint.sh diff --git a/README.md b/README.md index e3bda25..eae793f 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,64 @@ -**WIP** (the experimentation phase) | [source code on GitHub](https://github.com/jaandrle/deka-dom-el) | [*mirrored* on Gitea](https://gitea.jaandrle.cz/jaandrle/deka-dom-el) +**WIP** (the experimentation phase) +| [source code on GitHub](https://github.com/jaandrle/deka-dom-el) +| [*mirrored* on Gitea](https://gitea.jaandrle.cz/jaandrle/deka-dom-el) ***Vanilla for flavouring — a full-fledged feast for large projects*** *…use simple DOM API by default and library tools and logic when you need them* -```js +```javascript document.body.append( - el("h1", "Hello World 👋"), - el("p", "See some syntax examples here:"), - el("ul").append( - el("li").append( - el("a", { textContent: "Link to the library repo", title: "Deka DOM El — GitHub", href: "https://github.com/jaandrle/deka-dom-el" }) - ), - el("li").append( - "Use extended Vanilla JavaScript DOM/IDL API: ", - el("span", { textContent: "» this is a span with class=cN and data-a=A, data-b=B «", className: "cN", dataset: { a: "A", b: "B" } }) - ), - el("li").append( - el(component, { textContent: "A component", className: "example" }, on("change", console.log)) - ) - ) - + el(HelloWorldComponent, { initial: "🚀" }) ); -function component({ textContent, className }){ - const value= S("onchange"); - +/** @typedef {"🎉" | "🚀"} Emoji */ +/** @param {{ initial: Emoji }} attrs */ +function HelloWorldComponent({ initial }){ + const clicks= S(0); + const emoji= S(initial); + /** @param {HTMLOptionElement} el */ + const isSelected= el=> (el.selected= el.value===initial); + // @ts-expect-error 2339: The has only two options with {@link Emoji} + const onChange= on("change", event=> emoji(event.target.value)); + + return el().append( + el("p", { + textContent: S(() => `Hello World ${emoji().repeat(clicks())}`), + className: "example", + ariaLive: "polite", //OR ariaset: { live: "polite" }, + dataset: { example: "Example" }, //OR dataExample: "Example", + }), + el("button", + { textContent: "Fire", type: "button" }, + on("click", ()=> clicks(clicks() + 1)), + on("keyup", ()=> clicks(clicks() - 2)), + ), + el("select", null, onChange).append( + el(OptionComponent, "🎉", isSelected),//OR { textContent: "🎉" } + el(OptionComponent, "🚀", isSelected),//OR { textContent: "🚀" } + ) + ); +} +function OptionComponent({ textContent }){ + return el("option", { value: textContent, textContent }) +} diff --git a/docs/components/examples/scopes/class-component.js b/docs/components/examples/scopes/class-component.js index 45a1bfd..a525da4 100644 --- a/docs/components/examples/scopes/class-component.js +++ b/docs/components/examples/scopes/class-component.js @@ -36,7 +36,7 @@ function elClass(_class, attributes, ...addons){ }); element.prepend(el_mark); if(is_fragment) element_host= el_mark; - + chainableAppend(element); addons.forEach(c=> c(element_host)); scope.pop(); diff --git a/docs/components/examples/scopes/cleaning.js b/docs/components/examples/scopes/cleaning.js index 923b331..f648322 100644 --- a/docs/components/examples/scopes/cleaning.js +++ b/docs/components/examples/scopes/cleaning.js @@ -1,4 +1,6 @@ -import { el, empty, on } from "deka-dom-el"; +import { el, on } from "deka-dom-el"; +/** @param {HTMLElement} el */ +const empty= el=> Array.from(el.children).forEach(c=> c.remove()); document.body.append( el(component), el("button", { diff --git a/docs/components/examples/scopes/imperative.js b/docs/components/examples/scopes/imperative.js index 90f822e..012514c 100644 --- a/docs/components/examples/scopes/imperative.js +++ b/docs/components/examples/scopes/imperative.js @@ -23,7 +23,7 @@ function component(){ * const data= O("data"); * ul.append(el("li", data)); * }); - * + * * // THE HOST IS PROBABLY DIFFERENT THAN * // YOU EXPECT AND OBSERVABLES MAY BE * // UNEXPECTEDLY REMOVED!!! diff --git a/docs/components/examples/signals/dom-attrs.js b/docs/components/examples/signals/dom-attrs.js index a26219e..0941766 100644 --- a/docs/components/examples/signals/dom-attrs.js +++ b/docs/components/examples/signals/dom-attrs.js @@ -4,7 +4,7 @@ const count= S(0); import { el } from "deka-dom-el"; document.body.append( el("p", S(()=> "Currently: "+count())), - el("p", { classList: { red: S(()=> count()%2) }, dataset: { count }, textContent: "Attributes example" }) + el("p", { classList: { red: S(()=> count()%2 === 0) }, dataset: { count }, textContent: "Attributes example" }), ); document.head.append( el("style", ".red { color: red; }") diff --git a/docs/components/mnemonic/customElement-init.js b/docs/components/mnemonic/customElement-init.js index 0454726..301f3e2 100644 --- a/docs/components/mnemonic/customElement-init.js +++ b/docs/components/mnemonic/customElement-init.js @@ -4,25 +4,32 @@ import { mnemonicUl } from "../mnemonicUl.html.js"; export function mnemonic(){ return mnemonicUl().append( el("li").append( - el("code", "customElementRender(, , [, ])"), " — use function to render DOM structure for given ", + el("code", "customElementRender(, [, ])"), + " — use function to render DOM structure for given custom element (or its Shadow DOM)", ), el("li").append( - el("code", "customElementWithDDE()"), " — register to DDE library, see also `lifecyclesToEvents`, can be also used as decorator", + el("code", "customElementWithDDE()"), + " — register to DDE library, see also `lifecyclesToEvents`, can be also used as decorator", ), el("li").append( - el("code", "observedAttributes()"), " — returns record of observed attributes (keys uses camelCase)", + el("code", "observedAttributes()"), + " — returns record of observed attributes (keys uses camelCase)", ), el("li").append( - el("code", "S.observedAttributes()"), " — returns record of observed attributes (keys uses camelCase and values are signals)", + el("code", "S.observedAttributes()"), + " — returns record of observed attributes (keys uses camelCase and values are signals)", ), el("li").append( - el("code", "lifecyclesToEvents()"), " — convert lifecycle methods to events, can be also used as decorator", + el("code", "lifecyclesToEvents()"), + " — convert lifecycle methods to events, can be also used as decorator", ), el("li").append( - el("code", "simulateSlots(, [, ])"), " — simulate slots for Custom Elements without shadow DOM", + el("code", "simulateSlots(, )"), + " — simulate slots for Custom Elements without shadow DOM", ), el("li").append( - el("code", "simulateSlots([, ])"), " — simulate slots for “dde”/functional components", + el("code", "simulateSlots([, ])"), + " — simulate slots for “dde”/functional components", ), ); } diff --git a/docs/components/mnemonic/elements-init.js b/docs/components/mnemonic/elements-init.js index 011f63d..0893128 100644 --- a/docs/components/mnemonic/elements-init.js +++ b/docs/components/mnemonic/elements-init.js @@ -4,22 +4,28 @@ import { mnemonicUl } from "../mnemonicUl.html.js"; export function mnemonic(){ return mnemonicUl().append( el("li").append( - el("code", "assign(, ...): "), " — assign properties to the element", + el("code", "assign(, ...): "), + " — assign properties (prefered, or attributes) to the element", ), el("li").append( - el("code", "el(, )[.append(...)]: "), " — simple element containing only text", + el("code", "el(, )[.append(...)]: "), + " — simple element containing only text", ), el("li").append( - el("code", "el(, )[.append(...)]: "), " — element with more properties", + el("code", "el(, )[.append(...)]: "), + " — element with more properties (prefered, or attributes)", ), el("li").append( - el("code", "el(, )[.append(...)]: "), " — using component represented by function", + el("code", "el(, )[.append(...)]: "), + " — using component represented by function (must accept object like for )", ), el("li").append( - el("code", "el(<...>, <...>, ...)"), " — see following page" + el("code", "el(<...>, <...>, ...)"), + " — see following section of documentation", ), el("li").append( - el("code", "elNS()()[.append(...)]: "), " — typically SVG elements", + el("code", "elNS()()[.append(...)]: "), + " — typically SVG elements", ) ); } diff --git a/docs/components/mnemonic/events-init.js b/docs/components/mnemonic/events-init.js index 77252b0..aae2f28 100644 --- a/docs/components/mnemonic/events-init.js +++ b/docs/components/mnemonic/events-init.js @@ -4,17 +4,21 @@ import { mnemonicUl } from "../mnemonicUl.html.js"; export function mnemonic(){ return mnemonicUl().append( el("li").append( - el("code", "on(, [, ])()"), " — just ", el("code", ".addEventListener(, [, ])") + el("code", "on(, [, ])()"), + " — just ", el("code", ".addEventListener(, [, ])") ), el("li").append( - el("code", "on.(, [, ])()"), " — corresponds to custom elemnets callbacks ", el("code", "Callback(...){...}"), + el("code", "on.(, [, ])()"), + " — corresponds to custom elemnets callbacks ", el("code", "Callback(...){...}"), ". To connect to custom element see following page, else it is simulated by MutationObserver." ), el("li").append( - el("code", "dispatchEvent([, ])(element)"), " — just ", el("code", ".dispatchEvent(new Event([, ]))") + el("code", "dispatchEvent([, ])(element)"), + " — just ", el("code", ".dispatchEvent(new Event([, ]))") ), el("li").append( - el("code", "dispatchEvent([, ])(element, detail)"), " — just ", el("code", ".dispatchEvent(new CustomEvent(, { detail, ... }))") + el("code", "dispatchEvent([, ])(element, detail)"), + " — just ", el("code", ".dispatchEvent(new CustomEvent(, { detail, ... }))") ), ); } diff --git a/docs/components/mnemonic/scopes-init.js b/docs/components/mnemonic/scopes-init.js index 61ff544..62dd70d 100644 --- a/docs/components/mnemonic/scopes-init.js +++ b/docs/components/mnemonic/scopes-init.js @@ -4,13 +4,16 @@ import { mnemonicUl } from "../mnemonicUl.html.js"; export function mnemonic(){ return mnemonicUl().append( el("li").append( - el("code", "el(, )[.append(...)]: "), " — using component represented by function", + el("code", "el(, )[.append(...)]: "), + " — using component represented by function", ), el("li").append( - el("code", "scope.host()"), " — get current component reference" + el("code", "scope.host()"), + " — get current component reference" ), el("li").append( - el("code", "scope.host(...)"), " — use addons to current component", + el("code", "scope.host(...)"), + " — use addons to current component", ) ); } diff --git a/docs/components/mnemonic/signals-init.js b/docs/components/mnemonic/signals-init.js index 7a6d0a6..2e29529 100644 --- a/docs/components/mnemonic/signals-init.js +++ b/docs/components/mnemonic/signals-init.js @@ -7,22 +7,28 @@ export function mnemonic(){ el("code", "S()"), " — signal: reactive value", ), el("li").append( - el("code", "S(()=> )"), " — read-only signal: reactive value dependent on calculation using other signals", + el("code", "S(()=> )"), + " — read-only signal: reactive value dependent on calculation using other signals", ), el("li").append( - el("code", "S.on(, [, ])"), " — listen to the signal value changes", + el("code", "S.on(, [, ])"), + " — listen to the signal value changes", ), el("li").append( - el("code", "S.clear(...)"), " — off and clear signals", + el("code", "S.clear(...)"), + " — off and clear signals", ), el("li").append( - el("code", "S(, )"), " — signal: pattern to create complex reactive objects/arrays", + el("code", "S(, )"), + " — signal: pattern to create complex reactive objects/arrays", ), el("li").append( - el("code", "S.action(, , ...)"), " — invoke an action for given signal" + el("code", "S.action(, , ...)"), + " — invoke an action for given signal" ), el("li").append( - el("code", "S.el(, )"), " — render partial dom structure (template) based on the current signal value", + el("code", "S.el(, )"), + " — render partial dom structure (template) based on the current signal value", ) ); } diff --git a/docs/global.css.js b/docs/global.css.js index c3878b2..e956052 100644 --- a/docs/global.css.js +++ b/docs/global.css.js @@ -103,7 +103,7 @@ main{ [main-start] min(var(--body-max-width), 90%) [main-end] 1fr [full-main-end]; } -main > *{ +main > *, main slot > *{ grid-column: main; } .icon { diff --git a/docs/index.html.js b/docs/index.html.js index fa8140a..60e1a18 100644 --- a/docs/index.html.js +++ b/docs/index.html.js @@ -27,7 +27,7 @@ export function page({ pkg, info }){ const page_id= info.id; return el(simplePage, { info, pkg }).append( el("p", t`The library tries to provide pure JavaScript tool(s) to create reactive interfaces using …`), - + el(h3, t`Event-driven programming (3 parts separation ≡ 3PS)`), el("p").append(t` Let's introduce the basic principle on which the library is built. We'll use the JavaScript listener as @@ -36,7 +36,7 @@ export function page({ pkg, info }){ el(code, { src: fileURL("./components/examples/introducing/3ps.js"), page_id }), el("p").append(...T` As we can see, in the code at location “A” we define ${el("em", t`how to react`)} when the function - is called with any event as an argument. At that moment, we ${el("em", t`don't care who/why/how`)} + is called with any event as an argument. At that moment, we ${el("em", t`don’t care who/why/how`)} the function was called. Similarly, at point “B”, we reference to a function to be called on the event ${el("em", t`without caring`)} what the function will do at that time. Finally, at point “C”, we tell the application that a change has occurred, in the input, and we ${el("em", t`don't care if/how someone`)} @@ -56,7 +56,7 @@ export function page({ pkg, info }){ describe usage in specific situations, see for example ${el("a", { textContent: t`MVVM`, ...references.w_mvv })} or ${el("a", { textContent: t`MVC`, ...references.w_mvc })}. `), - + el(h3, t`Organization of the documentation`), ); } diff --git a/docs/layout/head.html.js b/docs/layout/head.html.js index e55b41e..cc8f7d8 100644 --- a/docs/layout/head.html.js +++ b/docs/layout/head.html.js @@ -69,7 +69,7 @@ function metaFacebook({ name, description, homepage }){ function iconGitHub(){ const el= elNS("http://www.w3.org/2000/svg"); return el("svg", { className: "icon", viewBox: "0 0 32 32" }).append( - el("path", { d: [ //see https://svg-path-visualizer.netlify.app/#M16%200.395c-8.836%200-16%207.163-16%2016%200%207.069%204.585%2013.067%2010.942%2015.182%200.8%200.148%201.094-0.347%201.094-0.77%200-0.381-0.015-1.642-0.022-2.979-4.452%200.968-5.391-1.888-5.391-1.888-0.728-1.849-1.776-2.341-1.776-2.341-1.452-0.993%200.11-0.973%200.11-0.973%201.606%200.113%202.452%201.649%202.452%201.649%201.427%202.446%203.743%201.739%204.656%201.33%200.143-1.034%200.558-1.74%201.016-2.14-3.554-0.404-7.29-1.777-7.29-7.907%200-1.747%200.625-3.174%201.649-4.295-0.166-0.403-0.714-2.030%200.155-4.234%200%200%201.344-0.43%204.401%201.64%201.276-0.355%202.645-0.532%204.005-0.539%201.359%200.006%202.729%200.184%204.008%200.539%203.054-2.070%204.395-1.64%204.395-1.64%200.871%202.204%200.323%203.831%200.157%204.234%201.026%201.12%201.647%202.548%201.647%204.295%200%206.145-3.743%207.498-7.306%207.895%200.574%200.497%201.085%201.47%201.085%202.963%200%202.141-0.019%203.864-0.019%204.391%200%200.426%200.288%200.925%201.099%200.768%206.354-2.118%2010.933-8.113%2010.933-15.18%200-8.837-7.164-16-16-16z + el("path", { d: [ //see https://svg-path-visualizer.netlify.app/#M16%200.395c-8.836%200-16%207.163-16%2016%200%207.069%204.585%2013.067%2010.942%2015.182%200.8%200.148%201.094-0.347%201.094-0.77%200-0.381-0.015-1.642-0.022-2.979-4.452%200.968-5.391-1.888-5.391-1.888-0.728-1.849-1.776-2.341-1.776-2.341-1.452-0.993%200.11-0.973%200.11-0.973%201.606%200.113%202.452%201.649%202.452%201.649%201.427%202.446%203.743%201.739%204.656%201.33%200.143-1.034%200.558-1.74%201.016-2.14-3.554-0.404-7.29-1.777-7.29-7.907%200-1.747%200.625-3.174%201.649-4.295-0.166-0.403-0.714-2.030%200.155-4.234%200%200%201.344-0.43%204.401%201.64%201.276-0.355%202.645-0.532%204.005-0.539%201.359%200.006%202.729%200.184%204.008%200.539%203.054-2.070%204.395-1.64%204.395-1.64%200.871%202.204%200.323%203.831%200.157%204.234%201.026%201.12%201.647%202.548%201.647%204.295%200%206.145-3.743%207.498-7.306%207.895%200.574%200.497%201.085%201.47%201.085%202.963%200%202.141-0.019%203.864-0.019%204.391%200%200.426%200.288%200.925%201.099%200.768%206.354-2.118%2010.933-8.113%2010.933-15.18%200-8.837-7.164-16-16-16z // editorconfig-checker-disable-line "M 16,0.395", "c -8.836,0 -16,7.163 -16,16", "c 0,7.069 4.585,13.067 10.942,15.182", diff --git a/docs/p02-elements.html.js b/docs/p02-elements.html.js index b26a158..34043a8 100644 --- a/docs/p02-elements.html.js +++ b/docs/p02-elements.html.js @@ -54,7 +54,7 @@ export function page({ pkg, info }){ `), el(code, { src: fileURL("./components/examples/elements/intro.js"), page_id }), - + el(h3, t`Creating element(s) (with custom attributes)`), el("p").append(...T` You can create a native DOM element by using the ${el("a", references.mdn_create).append( @@ -77,8 +77,9 @@ export function page({ pkg, info }){ el("p").append(...T` You can study all JavaScript elements interfaces to the corresponding HTML elements. All HTML elements inherits from ${el("a", { textContent: "HTMLElement", ...references.mdn_el })}. To see - all available IDLs for example for paragraphs, see ${el("a", { textContent: "HTMLParagraphElement", ...references.mdn_p })}. - Moreover, the ${el("code", "assign")} provides a way to sets declaratively some convenient properties: + all available IDLs for example for paragraphs, see ${el("a", { textContent: "HTMLParagraphElement", + ...references.mdn_p })}. Moreover, the ${el("code", "assign")} provides a way to sets declaratively + some convenient properties: `), el("ul").append( el("li").append(...T` @@ -91,8 +92,7 @@ export function page({ pkg, info }){ el("li").append(...T`You can use string or object as a value for ${el("code", "style")} property.`), el("li").append(...T` ${el("code", "className")} (IDL – preffered)/${el("code", "class")} are ways to add CSS classes - to the element. You can use string (similarly to ${el("code", "class=\"…\"")} syntax in HTML) or - array of strings. This is handy to concat conditional classes. + to the element. You can use string (similarly to ${el("code", "class=\"…\"")} syntax in HTML). `), el("li").append(...T` Use ${el("code", "classList")} to toggle specific classes. This will be handy later when @@ -114,7 +114,7 @@ export function page({ pkg, info }){ ${el("code", "classListDeclarative")}. `), el(example, { src: fileURL("./components/examples/elements/dekaAssign.js"), page_id }), - + el(h3, t`Native JavaScript templating`), el("p", t`By default, the native JS has no good way to define HTML template using DOM API:`), el(example, { src: fileURL("./components/examples/elements/nativeAppend.js"), page_id }), @@ -123,8 +123,8 @@ export function page({ pkg, info }){ parent element. `), el(example, { src: fileURL("./components/examples/elements/dekaAppend.js"), page_id }), - - + + el(h3, t`Basic (state-less) components`), el("p").append(...T` You can use functions for encapsulation (repeating) logic. The ${el("code", "el")} accepts function @@ -149,7 +149,7 @@ export function page({ pkg, info }){ the ${el("code", "elNS")} function: `), el(example, { src: fileURL("./components/examples/elements/dekaElNS.js"), page_id }), - + el(mnemonic) ); } diff --git a/docs/p03-events.html.js b/docs/p03-events.html.js index 9652936..3ea0268 100644 --- a/docs/p03-events.html.js +++ b/docs/p03-events.html.js @@ -29,7 +29,7 @@ const references= { }, /** Custom Element lifecycle callbacks */ mdn_customElement: { - href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks" + href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks" // editorconfig-checker-disable-line }, /** MutationObserver */ mdn_mutation: { @@ -50,9 +50,9 @@ export function page({ pkg, info }){ We quickly introduce helper to listening to the native DOM events. And library syntax/pattern so-called ${el("em", t`Addon`)} to incorporate not only this in UI templates declaratively. `), - + el(code, { src: fileURL("./components/examples/events/intro.js"), page_id }), - + el(h3, t`Events and listenners`), el("p").append(...T` In JavaScript you can listen to the native DOM events of the given element by using @@ -105,17 +105,20 @@ export function page({ pkg, info }){ `), el("p").append(...T` The library provide three additional live-cycle events corresponding to how they are named in a case of - custom elements: ${el("code", "on.connected")}, ${el("code", "on.disconnected")} and ${el("code", "on.attributeChanged")}. + custom elements: ${el("code", "on.connected")}, ${el("code", "on.disconnected")} and ${el("code", + "on.attributeChanged")}. `), el(example, { src: fileURL("./components/examples/events/live-cycle.js"), page_id }), el("p").append(...T` For Custom elements, we will later introduce a way to replace ${el("code", "*Callback")} syntax with ${el("code", "dde:*")} events. The ${el("code", "on.*")} functions then listen to the appropriate - Custom Elements events (see ${el("a", { textContent: t`Custom element lifecycle callbacks | MDN`, ...references.mdn_customElement })}). + Custom Elements events (see ${el("a", { textContent: t`Custom element lifecycle callbacks | MDN`, + ...references.mdn_customElement })}). `), el("p").append(...T` - But, in case of regular elemnets the ${el("a", references.mdn_mutation).append(el("code", "MutationObserver"), " | MDN")} - is internaly used to track these events. Therefore, there are some drawbacks: + But, in case of regular elemnets the ${el("a", references.mdn_mutation).append(el("code", + "MutationObserver"), " | MDN")} is internaly used to track these events. Therefore, there are some + drawbacks: `), el("ul").append( el("li").append(...T` @@ -136,14 +139,15 @@ export function page({ pkg, info }){ native behaviour re-(dis)connecting element, use: `), el("ul").append( - el("li").append(...T`custom ${el("code", "MutationObserver")} or logic in (dis)${el("code", "connectedCallback")} or…`), + el("li").append(...T`custom ${el("code", "MutationObserver")} or logic in (dis)${el("code", + "connectedCallback")} or…`), el("li").append(...T`re-add ${el("code", "on.connected")} or ${el("code", "on.disconnected")} listeners.`) ), el(h3, t`Final notes`), el("p", t`The library also provides a method to dispatch the events.`), el(example, { src: fileURL("./components/examples/events/compareDispatch.js"), page_id }), - + el(mnemonic) ); } diff --git a/docs/p04-signals.html.js b/docs/p04-signals.html.js index 6cfedc8..0f3a6b5 100644 --- a/docs/p04-signals.html.js +++ b/docs/p04-signals.html.js @@ -49,7 +49,7 @@ export function page({ pkg, info }){ programming. If we desire to solve the issue in a declarative manner, signals may be a viable approach. `), el(code, { src: fileURL("./components/examples/signals/intro.js"), page_id }), - + el(h3, t`Introducing signals`), el("p").append(...T` Let’s re-introduce @@ -62,7 +62,7 @@ export function page({ pkg, info }){ `), el(example, { src: fileURL("./components/examples/signals/signals.js"), page_id }), el("p").append(...T` - All this is just an example of + All this is just an example of ${el("a", { textContent: t`Event-driven programming`, ...references.wiki_event_driven })} and ${el("a", { textContent: t`Publish–subscribe pattern`, ...references.wiki_pubsub })} (compare for example with ${el("a", { textContent: t`fpubsub library`, ...references.fpubsub })}). All three parts can be in @@ -109,8 +109,8 @@ export function page({ pkg, info }){ el("p").append(...T` To derived attribute based on value of signal variable just use the signal as a value of the attribute (${el("code", "assign(element, { attribute: S('value') })")}). ${el("code", "assign")}/${el("code", "el")} - provides ways to glue reactive attributes/classes more granularly into the DOM. Just use dedicated build-in - attributes ${el("code", "dataset")}, ${el("code", "ariaset")} and ${el("code", "classList")}. + provides ways to glue reactive attributes/classes more granularly into the DOM. Just use dedicated build-in + attributes ${el("code", "dataset")}, ${el("code", "ariaset")} and ${el("code", "classList")}. `), el("p").append(...T` For computation, you can use the “derived signal” (see above) like diff --git a/docs/p05-scopes.html.js b/docs/p05-scopes.html.js index d104ffd..2572d40 100644 --- a/docs/p05-scopes.html.js +++ b/docs/p05-scopes.html.js @@ -36,7 +36,7 @@ export function page({ pkg, info }){ `), el(code, { src: fileURL("./components/examples/scopes/intro.js"), page_id }), el("p").append(...T`The library therefore use ${el("em", t`scopes`)} to provide these functionalities.`), - + el(h3, t`Scopes and hosts`), el("p").append(...T` The ${el("strong", "host")} is the name for the element representing the component. This is typically diff --git a/docs/p06-customElement.html.js b/docs/p06-customElement.html.js index 8d471f6..0bc4c81 100644 --- a/docs/p06-customElement.html.js +++ b/docs/p06-customElement.html.js @@ -21,7 +21,7 @@ const references= { /** observedAttributes on MDN */ mdn_observedAttributes: { title: t`MDN documentation page for observedAttributes`, - href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#responding_to_attribute_changes", + href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#responding_to_attribute_changes", // editorconfig-checker-disable-line }, /** Custom Elements on MDN */ mdn_custom_elements: { @@ -55,10 +55,10 @@ export function page({ pkg, info }){ return el(simplePage, { info, pkg }).append( el("h2", t`Using web components in combinantion with DDE`), el("p").append(...T` - The DDE library allows for use within ${el("a", references.mdn_web_components).append( el("strong", t`Web Components`) )} - for dom-tree generation. However, in order to be able to use signals (possibly mapping to registered - ${el("a", references.mdn_observedAttributes).append( el("code", "observedAttributes") )}) and additional - functionality is (unfortunately) required to use helpers provided by the library. + The DDE library allows for use within ${el("a", references.mdn_web_components).append( el("strong", + t`Web Components`) )} for dom-tree generation. However, in order to be able to use signals (possibly + mapping to registered ${el("a", references.mdn_observedAttributes).append( el("code", "observedAttributes") + )}) and additional functionality is (unfortunately) required to use helpers provided by the library. `), el(code, { src: fileURL("./components/examples/customElement/intro.js"), page_id }), @@ -82,8 +82,8 @@ export function page({ pkg, info }){ `), el("p").append(...T` Also see the Life Cycle Events sections, very similarly we would like to use - ${el("a", { textContent: t`DDE events`, href: "./p03-events.html", title: t`See events part of the library documentation` })}. - To do it, the library provides function ${el("code", "customElementWithDDE")}… + ${el("a", { textContent: t`DDE events`, href: "./p03-events.html", title: t`See events part of the library + documentation` })}. To do it, the library provides function ${el("code", "customElementWithDDE")}… `), el(example, { src: fileURL("./components/examples/customElement/customElementWithDDE.js"), page_id }), diff --git a/examples/components/3rd-party.js b/examples/components/3rd-party.js index 21db48c..01c819e 100644 --- a/examples/components/3rd-party.js +++ b/examples/components/3rd-party.js @@ -21,7 +21,8 @@ export function thirdParty(){ }); // Array.from((new URL(location)).searchParams.entries()) // .forEach(([ key, value ])=> S.action(store, "set", key, value)); - // S.on(store, data=> history.replaceState("", "", "?"+(new URLSearchParams(JSON.parse(JSON.stringify(data)))).toString())); + // S.on(store, data=> history.replaceState("", "", + // "?"+(new URLSearchParams(JSON.parse(JSON.stringify(data)))).toString())); useStore(store_adapter, { onread(data){ Array.from(data.entries()) diff --git a/examples/components/fullNameComponent.js b/examples/components/fullNameComponent.js index 7dd28b1..cae66ab 100644 --- a/examples/components/fullNameComponent.js +++ b/examples/components/fullNameComponent.js @@ -15,6 +15,7 @@ export function fullNameComponent(){ on.disconnected(()=> console.log(fullNameComponent)) ); + const style= { height: "80px", display: "block", fill: "currentColor" }; const elSVG= elNS("http://www.w3.org/2000/svg"); return el("div", { className }).append( el("h2", "Simple form:"), @@ -29,7 +30,7 @@ export function fullNameComponent(){ ": ", el("#text", full_name) ), - elSVG("svg", { viewBox: "0 0 240 80", style: { height: "80px", display: "block" } }).append( + elSVG("svg", { viewBox: "0 0 240 80", style }).append( //elSVG("style", { }) elSVG("text", { x: 20, y: 35, textContent: "Text" }), ) diff --git a/examples/components/todosComponent.js b/examples/components/todosComponent.js index 65a750c..ab70675 100644 --- a/examples/components/todosComponent.js +++ b/examples/components/todosComponent.js @@ -17,22 +17,25 @@ const className= style.host(todosComponent).css` /** @param {{ todos: string[] }} */ export function todosComponent({ todos= [ "Task A" ] }= {}){ let key= 0; - const todosO= S(new Map(), { + const todosO= S( /** @type {Map>} */ (new Map()), { + /** @param {string} v */ add(v){ this.value.set(key++, S(v)); }, + /** @param {number} key */ remove(key){ S.clear(this.value.get(key)); this.value.delete(key); } }); todos.forEach(text=> S.action(todosO, "add", text)); const name= "todoName"; const onsubmitAdd= on("submit", event=> { - const el= event.target.elements[name]; + const el_form= /** @type {HTMLFormElement} */ (event.target); + const el= /** @type {HTMLInputElement} */ (el_form.elements[name]); event.preventDefault(); S.action(todosO, "add", el.value); el.value= ""; }); - const onremove= on("remove", event=> - S.action(todosO, "remove", event.detail)); - + const onremove= on("remove", /** @param {CustomEvent} event */ + event=> S.action(todosO, "remove", event.detail)); + return el("div", { className }).append( el("div").append( el("h2", "Todos:"), @@ -60,19 +63,24 @@ export function todosComponent({ todos= [ "Task A" ] }= {}){ ) } /** + * @param {{ textContent: ddeSignal, value: ddeSignal }} attrs * @dispatchs {number} remove * */ function todoComponent({ textContent, value }){ const { host }= scope; + const dispatchRemove= /** @type {(data: number) => void} */ + (dispatchEvent("remove", null, host)); const onclick= on("click", event=> { - const value= Number(event.target.value); + const el= /** @type {HTMLButtonElement} */ (event.target); + const value= Number(el.value); event.preventDefault(); event.stopPropagation(); - dispatchEvent("remove")(host(), value); + dispatchRemove(value); }); const is_editable= S(false); const onedited= on("change", ev=> { - textContent(ev.target.value); + const el= /** @type {HTMLInputElement} */ (ev.target); + textContent(el.value); is_editable(false); }); return el("li").append( diff --git a/examples/components/webComponent.js b/examples/components/webComponent.js index e225dd4..b0dd0dc 100644 --- a/examples/components/webComponent.js +++ b/examples/components/webComponent.js @@ -12,7 +12,7 @@ export class CustomHTMLTestElement extends HTMLElement{ } connectedCallback(){ if(!this.hasAttribute("pre-name")) this.setAttribute("pre-name", "default"); - customElementRender(this, this.attachShadow({ mode: "open" }), this.render, this.attributes) + customElementRender(this.attachShadow({ mode: "open" }), this.render, this.attributes) } attributes(element){ @@ -22,7 +22,7 @@ export class CustomHTMLTestElement extends HTMLElement{ render({ name, preName, test }){ console.log(scope.state); scope.host( - on.connected(()=> console.log(CustomHTMLTestElement)), + on.connected(console.log), on.attributeChanged(e=> console.log(e)), on.disconnected(()=> console.log(CustomHTMLTestElement)) ); @@ -34,11 +34,13 @@ export class CustomHTMLTestElement extends HTMLElement{ text(test), text(name), text(preName), - el("button", { type: "button", textContent: "pre-name", onclick: ()=> preName("Ahoj") }) + el("button", { type: "button", textContent: "pre-name", onclick: ()=> preName("Ahoj") }), + " | ", + el("slot", { className: "test", name: "test" }), ); } test= "A"; - + get name(){ return this.getAttribute("name"); } set name(value){ this.setAttribute("name", value); } /** @attr pre-name */ @@ -61,7 +63,7 @@ export class CustomSlottingHTMLElement extends HTMLElement{ )); } connectedCallback(){ - customElementRender(this, this, this.render); + customElementRender(this, this.render); } } customElementWithDDE(CustomSlottingHTMLElement); diff --git a/examples/index.js b/examples/index.js index 9fc6332..8767717 100644 --- a/examples/index.js +++ b/examples/index.js @@ -1,4 +1,9 @@ import { style, el, S } from './exports.js'; +style.css` +:root{ + color-scheme: dark light; +} +`; document.head.append(style.element); import { fullNameComponent } from './components/fullNameComponent.js'; import { todosComponent } from './components/todosComponent.js'; @@ -10,7 +15,10 @@ document.body.append( el("h1", "Experiments:"), el(fullNameComponent), el(todosComponent), - el(CustomHTMLTestElement.tagName, { name: "attr" }), + el(CustomHTMLTestElement.tagName, { name: "attr" }).append( + el("span", { textContent: "test", slot: "test" }), + el("span", { textContent: "test", slot: "test" }), + ), el(thirdParty), el(CustomSlottingHTMLElement.tagName, { onclick: ()=> toggle(!toggle()) }).append( el("strong", { slot: "name", textContent: "Honzo" }), diff --git a/index.d.ts b/index.d.ts index 270cdf2..63effa8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,63 +2,87 @@ declare global{ /* ddeSignal */ } type CustomElementTagNameMap= { '#text': Text, '#comment': Comment } type SupportedElement= - HTMLElementTagNameMap[keyof HTMLElementTagNameMap] - | SVGElementTagNameMap[keyof SVGElementTagNameMap] - | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] - | CustomElementTagNameMap[keyof CustomElementTagNameMap] + HTMLElementTagNameMap[keyof HTMLElementTagNameMap] + | SVGElementTagNameMap[keyof SVGElementTagNameMap] + | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] + | CustomElementTagNameMap[keyof CustomElementTagNameMap] declare global { type ddeComponentAttributes= Record | undefined; - type ddeElementAddon= (element: El)=> El | void; + type ddeElementAddon= (element: El)=> any; + type ddeString= string | ddeSignal + type ddeStringable= ddeString | number | ddeSignal } type PascalCase= `${Capitalize}${string}`; type AttrsModified= { /** * Use string like in HTML (internally uses `*.setAttribute("style", *)`), or object representation (like DOM API). */ - style: string | Partial | ddeSignal | Partial<{ [K in keyof CSSStyleDeclaration]: ddeSignal }> + style: Partial | ddeString + | Partial<{ [K in keyof CSSStyleDeclaration]: ddeSignal }> /** - * Provide option to add/remove/toggle CSS clasess (index of object) using 1/0/-1. In fact `el.classList.toggle(class_name)` for `-1` and `el.classList.toggle(class_name, Boolean(...))` for others. + * Provide option to add/remove/toggle CSS clasess (index of object) using 1/0/-1. + * In fact `el.classList.toggle(class_name)` for `-1` and `el.classList.toggle(class_name, Boolean(...))` + * for others. */ classList: Record>, /** - * By default simiral to `className`, but also supports `string[]` + * Used by the dataset HTML attribute to represent data for custom attributes added to elements. + * Values are converted to string (see {@link DOMStringMap}). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMStringMap) * */ - className: string | (string|boolean|undefined|ddeSignal)[]; + dataset: Record, /** * Sets `aria-*` simiraly to `dataset` * */ - ariaset: Record>, -} & Record<`=${string}` | `data${PascalCase}` | `aria${PascalCase}`, string|ddeSignal> & Record<`.${string}`, any> + ariaset: Record, +} & Record<`=${string}` | `data${PascalCase}` | `aria${PascalCase}`, ddeString> + & Record<`.${string}`, any> type _fromElsInterfaces= Omit; +type IsReadonly = + T extends { readonly [P in K]: T[K] } ? true : false; /** * Just element attributtes * - * In most cases, you can use native propertie such as [MDN WEB/API/Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) and so on (e.g. [`Text`](https://developer.mozilla.org/en-US/docs/Web/API/Text)). + * In most cases, you can use native propertie such as + * [MDN WEB/API/Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) and so on + * (e.g. [`Text`](https://developer.mozilla.org/en-US/docs/Web/API/Text)). * * There is added support for `data[A-Z].*`/`aria[A-Z].*` to be converted to the kebab-case alternatives. * @private */ -type ElementAttributes= Partial<{ [K in keyof _fromElsInterfaces]: _fromElsInterfaces[K] | ddeSignal<_fromElsInterfaces[K]> } & AttrsModified> & Record; -export function classListDeclarative(element: El, classList: AttrsModified["classList"]): El +type ElementAttributes= Partial<{ + [K in keyof _fromElsInterfaces]: IsReadonly<_fromElsInterfaces, K> extends false + ? _fromElsInterfaces[K] | ddeSignal<_fromElsInterfaces[K]> + : ddeStringable +} & AttrsModified> & Record; +export function classListDeclarative( + element: El, + classList: AttrsModified["classList"] +): El export function assign(element: El, ...attrs_array: ElementAttributes[]): El -export function assignAttribute>(element: El, attr: ATT, value: ElementAttributes[ATT]): ElementAttributes[ATT] +export function assignAttribute>( + element: El, + attr: ATT, + value: ElementAttributes[ATT] +): ElementAttributes[ATT] type ExtendedHTMLElementTagNameMap= HTMLElementTagNameMap & CustomElementTagNameMap; -type textContent= string | ddeSignal; export function el< TAG extends keyof ExtendedHTMLElementTagNameMap, - EL extends ExtendedHTMLElementTagNameMap[TAG] >( tag_name: TAG, - attrs?: ElementAttributes | textContent, - ...addons: ddeElementAddon[] + attrs?: ElementAttributes]> | ddeStringable, + ...addons: ddeElementAddon< + ExtendedHTMLElementTagNameMap[NoInfer] + >[], // TODO: for now addons must have the same element ): TAG extends keyof ddeHTMLElementTagNameMap ? ddeHTMLElementTagNameMap[TAG] : ddeHTMLElement export function el( tag_name?: "<>", ): ddeDocumentFragment export function el( tag_name: string, - attrs?: ElementAttributes | textContent, + attrs?: ElementAttributes | ddeStringable, ...addons: ddeElementAddon[] ): ddeHTMLElement @@ -66,9 +90,11 @@ export function el< C extends (attr: ddeComponentAttributes)=> SupportedElement | ddeDocumentFragment >( component: C, - attrs?: Parameters[0] | textContent, + attrs?: Parameters[0] | ddeStringable, ...addons: ddeElementAddon>[] -): ReturnType extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? ReturnType : ( ReturnType extends ddeDocumentFragment ? ReturnType : ddeHTMLElement ) +): ReturnType extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] + ? ReturnType + : ( ReturnType extends ddeDocumentFragment ? ReturnType : ddeHTMLElement ) export { el as createElement } export function elNS( @@ -78,8 +104,8 @@ export function elNS( EL extends ( TAG extends keyof SVGElementTagNameMap ? SVGElementTagNameMap[TAG] : SVGElement ), >( tag_name: TAG, - attrs?: ElementAttributes | textContent, - ...addons: ddeElementAddon[] + attrs?: ElementAttributes> | ddeStringable, + ...addons: ddeElementAddon>[] )=> TAG extends keyof ddeSVGElementTagNameMap ? ddeSVGElementTagNameMap[TAG] : ddeSVGElement export function elNS( namespace: "http://www.w3.org/1998/Math/MathML" @@ -88,54 +114,60 @@ export function elNS( EL extends ( TAG extends keyof MathMLElementTagNameMap ? MathMLElementTagNameMap[TAG] : MathMLElement ), >( tag_name: TAG, - attrs?: string | textContent | Partial<{ [key in keyof EL]: EL[key] | ddeSignal | string | number | boolean }>, - ...addons: ddeElementAddon[] + attrs?: ddeStringable | Partial<{ + [key in keyof EL]: EL[key] | ddeSignal | string | number | boolean + }>, + ...addons: ddeElementAddon>[] )=> ddeMathMLElement export function elNS( namespace: string ): ( tag_name: string, - attrs?: string | textContent | Record, + attrs?: string | ddeStringable | Record, ...addons: ddeElementAddon[] )=> SupportedElement export { elNS as createElementNS } export function chainableAppend(el: EL): EL; -/** - * Mapper function (optional). Pass for coppying attributes, this is NOT implemented by {@link simulateSlots} itself! - * */ -type simulateSlotsMapper= (body: HTMLSlotElement, el: HTMLElement)=> void; /** Simulate slots for ddeComponents */ -export function simulateSlots(root: EL, mapper?: simulateSlotsMapper): EL +export function simulateSlots( + root: EL, +): EL /** * Simulate slots in Custom Elements without using `shadowRoot`. * @param el Custom Element root element * @param body Body of the custom element * */ -export function simulateSlots(el: HTMLElement, body: EL, mapper?: simulateSlotsMapper): EL +export function simulateSlots( + el: HTMLElement, + body: EL, +): EL export function dispatchEvent(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any)=> void; -export function dispatchEvent(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (()=> SupportedElement)): - (data?: any)=> void; +export function dispatchEvent( + name: keyof DocumentEventMap | string, + options: EventInit | null, + element: SupportedElement | (()=> SupportedElement) +): (data?: any)=> void; interface On{ /** Listens to the DOM event. See {@link Document.addEventListener} */ < - EE extends ddeElementAddon, - El extends ( EE extends ddeElementAddon ? El : never ), - Event extends keyof DocumentEventMap>( + Event extends keyof DocumentEventMap, + EE extends ddeElementAddon= ddeElementAddon, + >( type: Event, - listener: (this: El, ev: DocumentEventMap[Event]) => any, + listener: (this: EE extends ddeElementAddon ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions ) : EE; < - EE extends ddeElementAddon, - El extends ( EE extends ddeElementAddon ? El : never )>( + EE extends ddeElementAddon= ddeElementAddon, + >( type: string, - listener: (this: El, ev: Event | CustomEvent ) => any, + listener: (this: EE extends ddeElementAddon ? El : never, ev: Event | CustomEvent ) => any, options?: AddEventListenerOptions ) : EE; - /** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ + /** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */// editorconfig-checker-disable-line connected< EE extends ddeElementAddon, El extends ( EE extends ddeElementAddon ? El : never ) @@ -143,7 +175,7 @@ interface On{ listener: (this: El, event: CustomEvent) => any, options?: AddEventListenerOptions ) : EE; - /** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ + /** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */// editorconfig-checker-disable-line disconnected< EE extends ddeElementAddon, El extends ( EE extends ddeElementAddon ? El : never ) @@ -151,7 +183,7 @@ interface On{ listener: (this: El, event: CustomEvent) => any, options?: AddEventListenerOptions ) : EE; - /** Listens to the element attribute changes. In case of custom elements uses [`attributeChangedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ + /** Listens to the element attribute changes. In case of custom elements uses [`attributeChangedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */// editorconfig-checker-disable-line attributeChanged< EE extends ddeElementAddon, El extends ( EE extends ddeElementAddon ? El : never ) @@ -162,7 +194,12 @@ interface On{ } export const on: On; -type Scope= { scope: Node | Function | Object, host: ddeElementAddon, custom_element: false | HTMLElement, prevent: boolean } +type Scope= { + scope: Node | Function | Object, + host: ddeElementAddon, + custom_element: false | HTMLElement, + prevent: boolean +}; /** Current scope created last time the `el(Function)` was invoke. (Or {@link scope.push}) */ export const scope: { current: Scope, @@ -176,7 +213,7 @@ export const scope: { * — `scope.host(on.connected(console.log))`. * */ host: (...addons: ddeElementAddon[])=> HTMLElement, - + state: Scope[], /** Adds new child scope. All attributes are inherited by default. */ push(scope: Partial): ReturnType["push"]>, @@ -190,7 +227,6 @@ export function customElementRender< EL extends HTMLElement, P extends any = Record> >( - custom_element: EL, target: ShadowRoot | EL, render: (props: P)=> SupportedElement | DocumentFragment, props?: P | ((el: EL)=> P) @@ -202,12 +238,12 @@ export function observedAttributes(custom_element: HTMLElement): Record= (...nodes: (Node | string)[])=> el; - + interface ddeDocumentFragment extends DocumentFragment{ append: ddeAppend; } interface ddeHTMLElement extends HTMLElement{ append: ddeAppend; } interface ddeSVGElement extends SVGElement{ append: ddeAppend; } interface ddeMathMLElement extends MathMLElement{ append: ddeAppend; } - + interface ddeHTMLElementTagNameMap { "a": ddeHTMLAnchorElement; "area": ddeHTMLAreaElement; @@ -350,6 +386,7 @@ declare global{ } } +// editorconfig-checker-disable interface ddeHTMLAnchorElement extends HTMLAnchorElement{ append: ddeAppend; } interface ddeHTMLAreaElement extends HTMLAreaElement{ append: ddeAppend; } interface ddeHTMLAudioElement extends HTMLAudioElement{ append: ddeAppend; } @@ -477,3 +514,4 @@ interface ddeSVGTitleElement extends SVGTitleElement{ append: ddeAppend; } interface ddeSVGUseElement extends SVGUseElement{ append: ddeAppend; } interface ddeSVGViewElement extends SVGViewElement{ append: ddeAppend; } +// editorconfig-checker-enable diff --git a/jsdom.js b/jsdom.js index f71ab97..00d5a2d 100644 --- a/jsdom.js +++ b/jsdom.js @@ -1,6 +1,14 @@ //TODO: https://www.npmjs.com/package/html-element import { enviroment as env } from './src/dom-common.js'; env.ssr= " ssr"; + +const q_store= new Set(); +env.q= function(promise){ + if(promise) return ( q_store.add(promise), promise ); + return Promise.allSettled(Array.from(q_store)).then(()=> q_store.clear()); +}; +export const queue= env.q; + const { setDeleteAttr }= env; /** @param {HTMLElement} obj */ env.setDeleteAttr= function(obj, prop, value){ @@ -29,7 +37,7 @@ export function register(dom){ export function unregister(){ if(!dom_last) return false; - + Object.assign(env, env_bk); env_bk= {}; dom_last= undefined; diff --git a/package-lock.json b/package-lock.json index 022a4e9..4736753 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "devDependencies": { "@size-limit/preset-small-lib": "~11.0", "dts-bundler": "~0.1", + "editorconfig-checker": "^6.0.0", "esbuild": "~0.24", "jsdom": "~25.0", "jshint": "~2.13", @@ -1424,6 +1425,24 @@ "typescript": "^2.4.0" } }, + "node_modules/editorconfig-checker": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/editorconfig-checker/-/editorconfig-checker-6.0.0.tgz", + "integrity": "sha512-uyTOwLJzR/k7ugiu7ITjCzkLKBhXeirQZ8hGlUkt1u/hq2Qu1E8EslgFZDN+lxZoQc97eiI87sUFgVILK4P+YQ==", + "dev": true, + "license": "MIT", + "bin": { + "ec": "dist/index.js", + "editorconfig-checker": "dist/index.js" + }, + "engines": { + "node": ">=20.11.0" + }, + "funding": { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/mstruebing" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", diff --git a/package.json b/package.json index fa01659..9018174 100644 --- a/package.json +++ b/package.json @@ -83,9 +83,7 @@ "modifyEsbuildConfig": { "platform": "browser" }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, + "scripts": {}, "keywords": [ "dom", "javascript", @@ -95,6 +93,7 @@ "devDependencies": { "@size-limit/preset-small-lib": "~11.0", "dts-bundler": "~0.1", + "editorconfig-checker": "~6.0", "esbuild": "~0.24", "jsdom": "~25.0", "jshint": "~2.13", diff --git a/signals.d.ts b/signals.d.ts index ebae316..2cd5d19 100644 --- a/signals.d.ts +++ b/signals.d.ts @@ -55,7 +55,7 @@ interface signal{ * */ el(signal: Signal, el: (v: S)=> Element | Element[] | DocumentFragment): DocumentFragment; - observedAttributes(custom_element: HTMLElement): Record>; + observedAttributes(custom_element: HTMLElement): Record>; } export const signal: signal; export const S: signal; diff --git a/src/customElement.js b/src/customElement.js index 5c2e20f..c34b911 100644 --- a/src/customElement.js +++ b/src/customElement.js @@ -1,7 +1,8 @@ import { keyLTE, evc, evd, eva } from "./dom-common.js"; import { scope } from "./dom.js"; import { c_ch_o } from "./events-observer.js"; -export function customElementRender(custom_element, target, render, props= observedAttributes){ +export function customElementRender(target, render, props= observedAttributes){ + const custom_element= target.host || target; scope.push({ scope: custom_element, host: (...c)=> c.length ? c.forEach(c=> c(custom_element)) : custom_element diff --git a/src/dom-common.js b/src/dom-common.js index a2071fc..01eb83f 100644 --- a/src/dom-common.js +++ b/src/dom-common.js @@ -6,6 +6,7 @@ export const enviroment= { H: globalThis.HTMLElement, S: globalThis.SVGElement, M: globalThis.MutationObserver, + q: p=> p || Promise.resolve(), }; import { isUndef } from './helpers.js'; function setDeleteAttr(obj, prop, val){ @@ -13,11 +14,11 @@ function setDeleteAttr(obj, prop, val){ For some native attrs you can unset only to set empty string. This can be confusing as it is seen in inspector `<… id=""`. Options: - 1. Leave it, as it is native behaviour - 2. Sets as empty string and removes the corresponding attribute when also has empty string - *3. Sets as undefined and removes the corresponding attribute when "undefined" string discovered - 4. Point 2. with checks for coincidence (e.g. use special string) - */ + 1. Leave it, as it is native behaviour + 2. Sets as empty string and removes the corresponding attribute when also has empty string + 3. (*) Sets as undefined and removes the corresponding attribute when "undefined" string discovered + 4. Point 2. with checks for coincidence (e.g. use special string) + */ Reflect.set(obj, prop, val); if(!isUndef(val)) return; Reflect.deleteProperty(obj, prop); diff --git a/src/dom.js b/src/dom.js index 4c5daa5..0daccff 100644 --- a/src/dom.js +++ b/src/dom.js @@ -1,6 +1,8 @@ import { signals } from "./signals-common.js"; import { enviroment as env } from './dom-common.js'; +//TODO: add type, docs ≡ make it public +export function queue(promise){ return env.q(promise); } /** @type {{ scope: object, prevent: boolean, host: function }[]} */ const scopes= [ { get scope(){ return env.D.body; }, @@ -10,13 +12,13 @@ const scopes= [ { export const scope= { get current(){ return scopes[scopes.length-1]; }, get host(){ return this.current.host; }, - + preventDefault(){ const { current }= this; current.prevent= true; return current; }, - + get state(){ return [ ...scopes ]; }, push(s= {}){ return scopes.push(Object.assign({}, this.current, { prevent: false }, s)); }, pushRoot(){ return scopes.push(scopes[0]); }, @@ -25,22 +27,25 @@ export const scope= { return scopes.pop(); }, }; -// following chainableAppend implementation is OK as the ElementPrototype.append description already is { writable: true, enumerable: true, configurable: true } +//NOTE: following chainableAppend implementation is OK as the ElementPrototype.append description already is { writable: true, enumerable: true, configurable: true } // editorconfig-checker-disable-line function append(...els){ this.appendOriginal(...els); return this; } -export function chainableAppend(el){ if(el.append===append) return el; el.appendOriginal= el.append; el.append= append; return el; } +export function chainableAppend(el){ + if(el.append===append) return el; el.appendOriginal= el.append; el.append= append; return el; +} let namespace; export function createElement(tag, attributes, ...addons){ /* jshint maxcomplexity: 15 */ const s= signals(this); let scoped= 0; let el, el_host; - //TODO Array.isArray(tag) ⇒ set key (cache els) if(Object(attributes)!==attributes || s.isSignal(attributes)) attributes= { textContent: attributes }; switch(true){ case typeof tag==="function": { scoped= 1; - scope.push({ scope: tag, host: (...c)=> c.length ? (scoped===1 ? addons.unshift(...c) : c.forEach(c=> c(el_host)), undefined) : el_host }); + const host= (...c)=> !c.length ? el_host : + (scoped===1 ? addons.unshift(...c) : c.forEach(c=> c(el_host)), undefined); + scope.push({ scope: tag, host }); el= tag(attributes || undefined); const is_fragment= el instanceof env.F; if(el.nodeName==="#comment") break; @@ -65,52 +70,6 @@ export function createElement(tag, attributes, ...addons){ scoped= 2; return el; } -import { hasOwn } from "./helpers.js"; -/** @param {HTMLElement} element @param {HTMLElement} [root] */ -export function simulateSlots(element, root, mapper){ - if(typeof root!=="object"){ - mapper= root; - root= element; - } - const _default= Symbol.for("default"); - const slots= Array.from(root.querySelectorAll("slot")) - .reduce((out, curr)=> Reflect.set(out, curr.name || _default, curr) && out, {}); - const has_d= hasOwn(slots, _default); - element.append= new Proxy(element.append, { - apply(orig, _, els){ - if(els[0]===root) return orig.apply(element, els); - if(!els.length) return element; - - const d= env.D.createDocumentFragment(); - for(const el of els){ - if(!el || !el.slot){ if(has_d) d.append(el); continue; } - const name= el.slot; - const slot= slots[name]; - elementAttribute(el, "remove", "slot"); - if(!slot) continue; - simulateSlotReplace(slot, el, mapper); - Reflect.deleteProperty(slots, name); - } - if(has_d){ - slots[_default].replaceWith(d); - Reflect.deleteProperty(slots, _default); - } - element.append= orig; //TODO: better memory management, but non-native behavior! - return element; - } - }); - if(element!==root){ - const els= Array.from(element.childNodes); - els.forEach(el=> el.remove()); - element.append(...els); - } - return root; -} -function simulateSlotReplace(slot, element, mapper){ - if(mapper) mapper(slot, element); - try{ slot.replaceWith(assign(element, { className: [ element.className, slot.className ], dataset: { ...slot.dataset } })); } - catch(_){ slot.replaceWith(element); } -} /** * @param { { type: "component", name: string, host: "this" | "parentElement" } | { type: "reactive" | "later" } } attrs * @param {boolean} [is_open=false] @@ -123,8 +82,7 @@ createElement.mark= function(attrs, is_open= false){ return out; }; export { createElement as el }; - -//const namespaceHelper= ns=> ns==="svg" ? "http://www.w3.org/2000/svg" : ns; +//TODO?: const namespaceHelper= ns=> ns==="svg" ? "http://www.w3.org/2000/svg" : ns; export function createElementNS(ns){ const _this= this; return function createElementNSCurried(...rest){ @@ -136,12 +94,46 @@ export function createElementNS(ns){ } export { createElementNS as elNS }; +/** @param {HTMLElement} element @param {HTMLElement} [root] */ +export function simulateSlots(element, root= element){ + const mark_e= "¹⁰", mark_s= "✓"; //NOTE: Markers to identify slots processed by this function. Also “prevents” native behavior as it is unlikely to use these in names. // editorconfig-checker-disable-line + const slots= Object.fromEntries( + Array.from(root.querySelectorAll("slot")) + .filter(s => !s.name.endsWith(mark_e)) + .map(s => [(s.name += mark_e), s])); + element.append= new Proxy(element.append, { + apply(orig, _, els){ + if(els[0]===root) return orig.apply(element, els); + for(const el of els){ + const name= (el.slot||"")+mark_e; + try{ elementAttribute(el, "remove", "slot"); } catch(_error){} + const slot= slots[name]; + if(!slot) return; + if(!slot.name.startsWith(mark_s)){ + slot.childNodes.forEach(c=> c.remove()); + slot.name= mark_s+name; + } + slot.append(el); + //TODO?: el.dispatchEvent(new CustomEvent("dde:slotchange", { detail: slot })); + } + element.append= orig; //TODO?: better memory management, but non-native behavior! + return element; + } + }); + if(element!==root){ + const els= Array.from(element.childNodes); + //TODO?: els.forEach(el=> el.remove()); + element.append(...els); + } + return root; +} + const assign_context= new WeakMap(); const { setDeleteAttr }= env; export function assign(element, ...attributes){ if(!attributes.length) return element; assign_context.set(element, assignContext(element, this)); - + for(const [ key, value ] of Object.entries(Object.assign({}, ...attributes))) assignAttribute.call(this, element, key, value); assign_context.delete(element); @@ -150,7 +142,7 @@ export function assign(element, ...attributes){ export function assignAttribute(element, key, value){ const { setRemoveAttr, s }= assignContext(element, this); const _this= this; - + value= s.processReactiveAttribute(element, key, value, (key, value)=> assignAttribute.call(_this, element, key, value)); const [ k ]= key; @@ -160,11 +152,11 @@ export function assignAttribute(element, key, value){ key= key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); return setRemoveAttr(key, value); } - if("className"===key) key= "class";//just optimalization, `isPropSetter` returns false immediately + if("className"===key) key= "class";//NOTE: just optimalization, this makes `isPropSetter` returns false immediately // editorconfig-checker-disable-line switch(key){ case "xlink:href": return setRemoveAttr(key, value, "http://www.w3.org/1999/xlink"); - case "textContent": //just optimalization, its part of Node ⇒ deep for `isPropSetter` + case "textContent": //NOTE: just optimalization, this makes `isPropSetter` returns false immediately (as its part of Node ⇒ deep for `isPropSetter`) // editorconfig-checker-disable-line return setDeleteAttr(element, key, value); case "style": if(typeof value!=="object") break; @@ -192,17 +184,13 @@ export function classListDeclarative(element, toggle){ 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; -} export function elementAttribute(element, op, key, value){ if(element instanceof env.H) return element[op+"Attribute"](key, value); return element[op+"AttributeNS"](null, key, value); } import { isUndef } from "./helpers.js"; -//TODO add cache? `(Map/Set)` +//TODO: add cache? `(Map/Set)` function isPropSetter(el, key){ if(!(key in el)) return false; const des= getPropDescriptor(el, key); @@ -216,7 +204,9 @@ function getPropDescriptor(p, key){ return des; } -/** @template {Record} T @param {object} s @param {T} obj @param {(param: [ keyof T, T[keyof T] ])=> void} cb */ +/** + * @template {Record} T @param {object} s @param {T} obj @param {(param: [ keyof T, T[keyof T] ])=> void} cb + * */ function forEachEntries(s, obj, cb){ if(typeof obj !== "object" || obj===null) return; return Object.entries(obj).forEach(function process([ key, val ]){ @@ -226,7 +216,9 @@ function forEachEntries(s, obj, cb){ }); } -function attrArrToStr(attr){ return Array.isArray(attr) ? attr.filter(Boolean).join(" ") : attr; } -function setRemove(obj, prop, key, val){ return obj[ (isUndef(val) ? "remove" : "set") + prop ](key, attrArrToStr(val)); } -function setRemoveNS(obj, prop, key, val, ns= null){ return obj[ (isUndef(val) ? "remove" : "set") + prop + "NS" ](ns, key, attrArrToStr(val)); } -function setDelete(obj, key, val){ Reflect.set(obj, key, val); if(!isUndef(val)) return; return Reflect.deleteProperty(obj, key); } +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, key, val){ + Reflect.set(obj, key, val); if(!isUndef(val)) return; return Reflect.deleteProperty(obj, key); } diff --git a/src/events-observer.js b/src/events-observer.js index 67966e7..846340e 100644 --- a/src/events-observer.js +++ b/src/events-observer.js @@ -82,12 +82,12 @@ function connectionsChangesObserverConstructor(){ is_observing= false; observer.disconnect(); } - //TODO remount support? + //TODO: remount support? function requestIdle(){ return new Promise(function(resolve){ (requestIdleCallback || requestAnimationFrame)(resolve); }); } async function collectChildren(element){ - if(store.size > 30)//TODO limit? + if(store.size > 30)//TODO?: limit await requestIdle(); const out= []; if(!(element instanceof Node)) return out; @@ -103,10 +103,10 @@ function connectionsChangesObserverConstructor(){ for(const element of addedNodes){ if(is_root) collectChildren(element).then(observerAdded); if(!store.has(element)) continue; - + const ls= store.get(element); if(!ls.length_c) continue; - + element.dispatchEvent(new Event(evc)); ls.connected= new WeakSet(); ls.length_c= 0; @@ -120,7 +120,7 @@ function connectionsChangesObserverConstructor(){ for(const element of removedNodes){ if(is_root) collectChildren(element).then(observerRemoved); if(!store.has(element)) continue; - + const ls= store.get(element); if(!ls.length_d) continue; (globalThis.queueMicrotask || setTimeout)(dispatchRemove(element)); diff --git a/src/events.js b/src/events.js index 8f08d0e..282f13d 100644 --- a/src/events.js +++ b/src/events.js @@ -8,6 +8,7 @@ export function dispatchEvent(name, options, host){ d.unshift(element); element= typeof host==="function"? host() : host; } + //TODO: what about re-emmitting? const event= d.length ? new CustomEvent(name, Object.assign({ detail: d[0] }, options)) : new Event(name, options); return element.dispatchEvent(event); }; @@ -64,9 +65,9 @@ on.attributeChanged= function(listener, options){ element.addEventListener(eva, listener, options); if(element[keyLTE] || els_attribute_store.has(element)) return element; - + if(!env.M) return element; - + const observer= new env.M(function(mutations){ for(const { attributeName, target } of mutations) target.dispatchEvent( @@ -77,4 +78,4 @@ on.attributeChanged= function(listener, options){ //TODO: clean up when element disconnected return element; }; -}; +}; diff --git a/src/observables-lib.js b/src/observables-lib.js index c86e51a..1fbae47 100644 --- a/src/observables-lib.js +++ b/src/observables-lib.js @@ -21,7 +21,7 @@ export function signal(value, actions){ if(typeof value!=="function") return create(false, value, actions); if(isSignal(value)) return value; - + const out= create(true); const contextReWatch= function(){ const [ origin, ...deps_old ]= deps.get(contextReWatch); @@ -58,7 +58,7 @@ signal.on= function on(s, listener, options= {}){ if(Array.isArray(s)) return s.forEach(s=> on(s, listener, options)); addSignalListener(s, listener); if(as) as.addEventListener("abort", ()=> removeSignalListener(s, listener)); - //TODO cleanup when signal removed + //TODO: cleanup when signal removed }; signal.symbols= { //signal: mark, @@ -77,11 +77,11 @@ signal.clear= function(...signals){ o.listeners.forEach(l=> { o.listeners.delete(l); if(!deps.has(l)) return; - + const ls= deps.get(l); ls.delete(s); if(ls.size>1) return; - + s.clear(...ls); deps.delete(l); }); diff --git a/src/signals-lib.js b/src/signals-lib.js index c86e51a..1fbae47 100644 --- a/src/signals-lib.js +++ b/src/signals-lib.js @@ -21,7 +21,7 @@ export function signal(value, actions){ if(typeof value!=="function") return create(false, value, actions); if(isSignal(value)) return value; - + const out= create(true); const contextReWatch= function(){ const [ origin, ...deps_old ]= deps.get(contextReWatch); @@ -58,7 +58,7 @@ signal.on= function on(s, listener, options= {}){ if(Array.isArray(s)) return s.forEach(s=> on(s, listener, options)); addSignalListener(s, listener); if(as) as.addEventListener("abort", ()=> removeSignalListener(s, listener)); - //TODO cleanup when signal removed + //TODO: cleanup when signal removed }; signal.symbols= { //signal: mark, @@ -77,11 +77,11 @@ signal.clear= function(...signals){ o.listeners.forEach(l=> { o.listeners.delete(l); if(!deps.has(l)) return; - + const ls= deps.get(l); ls.delete(s); if(ls.size>1) return; - + s.clear(...ls); deps.delete(l); }); diff --git a/tsconfig.json b/tsconfig.json index 8ac44d2..ce2ec17 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { - "compilerOptions": { - "emitDeclarationOnly": true, - "declaration": true, - "declarationDir": "dist" - } + "compilerOptions": { + "emitDeclarationOnly": true, + "declaration": true, + "declarationDir": "dist" + } }