@johnhenry/definable 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 John Henry
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 is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ 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 IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # definable
2
+
3
+ [![npm version](https://img.shields.io/npm/v/%40johnhenry%2Fdefinable.svg)](https://www.npmjs.com/package/@johnhenry/definable)
4
+ [![CI](https://github.com/johnhenry/definable/actions/workflows/ci.yml/badge.svg)](https://github.com/johnhenry/definable/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/%40johnhenry%2Fdefinable.svg)](LICENSE)
6
+
7
+ Full documentation: [opensource.johnhenry.me/definable](https://opensource.johnhenry.me/definable/)
8
+
9
+ > **Provenance:** extracted from [`johnhenry/lib`](https://github.com/johnhenry/lib)'s
10
+ > `js/` directory. Briefly part of [`@johnhenry/domkit`](https://github.com/johnhenry/domkit)
11
+ > (a toolkit of ~40 independent DOM/HTML-component modules), then split out
12
+ > as its own package because this cluster has a real internal shape of its
13
+ > own -- four ways of getting behavior onto a page declaratively, from four
14
+ > different kinds of source.
15
+
16
+ Five small modules for declaratively loading or defining custom elements
17
+ and globals, without writing a `customElements.define(...)` call by hand:
18
+
19
+ | Module | What it does |
20
+ |---|---|
21
+ | [`definetag`](src/definetag/readme.md) | Curried `customElements.define` wrapper: `(elementClass) => (name) => ...`. The low-level primitive the others build on. |
22
+ | [`define-component.component`](src/define-component.component/readme.md) | `<define-component name="x" src="./x.mjs">` -- dynamically imports a module by URL and registers its export as a custom element. |
23
+ | [`define-component-by-content.component`](src/define-component-by-content.component/readme.md) | `<define-component-by-content name="x" content="<p>Hi</p>">` -- defines a custom element from an inline HTML string instead of a separate module file. Built on [`@johnhenry/domable`](https://github.com/johnhenry/domable)'s `simple-element`. |
24
+ | [`polyfill-window.component`](src/polyfill-window.component/readme.md) | `<polyfill-window name="x" src="./x.mjs">` -- the same URL-loading idea as `define-component`, but assigns the import to a `globalThis` key instead of registering a custom element. |
25
+ | [`until-window-load`](src/until-window-load/readme.md) | Declarative "remove this loading class once `window` has loaded" helper -- used by `polyfill-window.component`'s own demo to avoid a flash of unstyled content. |
26
+
27
+ ## Install
28
+
29
+ ```sh
30
+ npm install @johnhenry/definable
31
+ ```
32
+
33
+ ```js
34
+ import definetag from "@johnhenry/definable/definetag/index.mjs";
35
+ ```
36
+
37
+ or, in a browser with no build step, via a CDN:
38
+
39
+ ```html
40
+ <script
41
+ type="module"
42
+ src="https://esm.sh/@johnhenry/definable/define-component.component/global.mjs"
43
+ ></script>
44
+ <define-component name="my-widget" src="./my-widget.mjs"></define-component>
45
+ <my-widget></my-widget>
46
+ ```
47
+
48
+ ## Which one do I want?
49
+
50
+ | I have... | Use |
51
+ |---|---|
52
+ | A module file (default or named export is a custom-element class) | `define-component.component` |
53
+ | A small HTML string I want to become a tag | `define-component-by-content.component` |
54
+ | A module whose export should just be a global, not a tag | `polyfill-window.component` |
55
+ | My own class already, just need to register it under a name | `definetag` directly |
56
+
57
+ ## Family
58
+
59
+ - [`@johnhenry/domable`](https://github.com/johnhenry/domable) -- `define-component-by-content.component`
60
+ depends on its `simple-element` `constructSuperclass` for building the
61
+ actual custom-element class from an HTML string.
62
+ - [`@johnhenry/domkit`](https://github.com/johnhenry/domkit) -- the toolkit
63
+ this package was originally part of. Several of its still-resident
64
+ modules (`animate-paths.component`, `chernoff-face`, `xy-grapher`) depend
65
+ on `definable` (`definetag`, `define-component.component`) to register
66
+ themselves.
67
+ - [`@johnhenry/respondable`](https://github.com/johnhenry/respondable) --
68
+ `query-container.component`'s own demo bootstraps via this package's
69
+ `define-component.component`.
70
+
71
+ ## License
72
+
73
+ MIT
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@johnhenry/definable",
3
+ "version": "0.0.0",
4
+ "description": "Load or define custom elements declaratively -- from a URL, from inline HTML content, or as a plain global polyfill.",
5
+ "type": "module",
6
+ "exports": {
7
+ "./*": "./src/*"
8
+ },
9
+ "files": [
10
+ "src/"
11
+ ],
12
+ "scripts": {
13
+ "test": "node scripts/check-syntax.mjs"
14
+ },
15
+ "keywords": [
16
+ "html",
17
+ "dom",
18
+ "web-components",
19
+ "custom-elements",
20
+ "define"
21
+ ],
22
+ "author": "",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/johnhenry/definable.git"
27
+ },
28
+ "homepage": "https://opensource.johnhenry.me/definable/",
29
+ "engines": {
30
+ "node": ">=26.0.0"
31
+ },
32
+ "dependencies": {
33
+ "@johnhenry/domable": "^0.0.1"
34
+ }
35
+ }
@@ -0,0 +1,3 @@
1
+ import definetag from "../definetag/index.mjs";
2
+ import DefineComponentByContent from "./index.mjs";
3
+ definetag(DefineComponentByContent)("define-component-by-content");
@@ -0,0 +1,28 @@
1
+ import { constructSuperclass } from "@johnhenry/domable/simple-element";
2
+
3
+ export default class extends globalThis.HTMLElement {
4
+ constructor() {
5
+ super();
6
+ }
7
+ connectedCallback() {
8
+ const name = this.getAttribute("name");
9
+ const useShadow = this.getAttribute("shadow") || false;
10
+ const shadowMode = this.getAttribute("mode") || "open";
11
+ const content = this.getAttribute("content") || "";
12
+ const useDom = this.getAttribute("use-dom") || "";
13
+ // this.querySelector("slot").innerHTML = content;
14
+
15
+ globalThis.customElements.define(
16
+ name,
17
+ class extends constructSuperclass({
18
+ [useDom ? "HTML" : "shadowHTML"]: content,
19
+ useShadow,
20
+ shadowMode,
21
+ }) {
22
+ constructor() {
23
+ super();
24
+ }
25
+ }
26
+ );
27
+ }
28
+ }
@@ -0,0 +1,32 @@
1
+ # Define Component By Content
2
+
3
+ Defines a new custom element whose markup comes from an HTML string given
4
+ directly as an attribute, rather than from a separate module file (compare
5
+ [define-component.component](../define-component.component/readme.md),
6
+ which loads markup/behavior from a URL instead).
7
+
8
+ Built on [`@johnhenry/domable`](https://github.com/johnhenry/domable)'s
9
+ `simple-element` `constructSuperclass`.
10
+
11
+ ## Attributes
12
+
13
+ | Attribute | Description |
14
+ |---|---|
15
+ | `name` | Tag name to register (required) |
16
+ | `content` | HTML string to render |
17
+ | `use-dom` | If present, `content` renders as light DOM (`append`ed directly); if absent (default), `content` renders inside an open shadow root |
18
+ | `mode` | Shadow root mode when `use-dom` is absent. Defaults to `"open"` |
19
+
20
+ ## Usage
21
+
22
+ ```html
23
+ <script
24
+ type="module"
25
+ src="https://esm.sh/@johnhenry/definable/define-component-by-content.component/global.mjs"
26
+ ></script>
27
+ <define-component-by-content
28
+ name="my-greeting"
29
+ content="<p>Hello!</p>"
30
+ ></define-component-by-content>
31
+ <my-greeting></my-greeting>
32
+ ```
@@ -0,0 +1,5 @@
1
+ export default class extends HTMLElement {
2
+ connectedCallback() {
3
+ this.textContent = "Hello from a dynamically defined component!";
4
+ }
5
+ }
@@ -0,0 +1,3 @@
1
+ <script type="module" src="./global.mjs"></script>
2
+ <define-component name="my-widget" src="./demo-widget.mjs"></define-component>
3
+ <my-widget></my-widget>
@@ -0,0 +1,2 @@
1
+ import DefineComponent from "./index.mjs";
2
+ globalThis.customElements.define("define-component", DefineComponent);
@@ -0,0 +1,36 @@
1
+ // Dynamically imports a module and registers its default (or named) export
2
+ // as a custom element. Adapted from polyfill-window.component/index.mjs's
3
+ // URL-resolution logic, but ends in customElements.define(...) instead of a
4
+ // global assignment -- see readme.md.
5
+ const define = async (src, name, imp, force) => {
6
+ const { href } = globalThis.location;
7
+ const indexQM = href.lastIndexOf("?");
8
+ const withoutQuery = indexQM === -1 ? href : href.substring(0, indexQM);
9
+ const indexS = withoutQuery.lastIndexOf("/");
10
+ const dirname =
11
+ indexS === -1 ? withoutQuery : withoutQuery.substring(0, indexS);
12
+ const url = new URL(src, dirname + "/");
13
+ if (globalThis.customElements.get(name) && force === null) {
14
+ return;
15
+ }
16
+ const module = await import(url.href);
17
+ const ElementClass = module[imp ?? "default"];
18
+ globalThis.customElements.define(name, ElementClass);
19
+ };
20
+
21
+ export default class extends globalThis.HTMLElement {
22
+ #name = "";
23
+ #src = "";
24
+ #import = null;
25
+ #force = false;
26
+ constructor() {
27
+ super();
28
+ }
29
+ connectedCallback() {
30
+ this.#name = this.getAttribute("name");
31
+ this.#src = this.getAttribute("src");
32
+ this.#import = this.getAttribute("import");
33
+ this.#force = this.getAttribute("force");
34
+ define(this.#src, this.#name, this.#import, this.#force);
35
+ }
36
+ }
@@ -0,0 +1,32 @@
1
+ # Define Component
2
+
3
+ Dynamically loads a module by URL and registers its export as a custom
4
+ element — the generic "define a component from a URL" pattern referenced
5
+ by [polyfill-window.component](../polyfill-window.component/readme.md)'s
6
+ own demo, and by [`@johnhenry/respondable`](https://github.com/johnhenry/respondable)'s
7
+ `query-container.component` and [`@johnhenry/domkit`](https://github.com/johnhenry/domkit)'s
8
+ `animate-paths.component`, both of which bootstrap via this exact pattern.
9
+
10
+ Compare [define-component-by-content.component](../define-component-by-content.component/readme.md),
11
+ which defines a component from an inline HTML string instead of a separate
12
+ module file.
13
+
14
+ ## Attributes
15
+
16
+ | Attribute | Description |
17
+ |---|---|
18
+ | `name` | Tag name to register (required) |
19
+ | `src` | URL of the module to import, resolved relative to the current document (required) |
20
+ | `import` | Named export to use as the element class. Defaults to the module's default export |
21
+ | `force` | If present, re-imports and re-registers even if `name` is already a registered custom element |
22
+
23
+ ## Usage
24
+
25
+ ```html
26
+ <script
27
+ type="module"
28
+ src="https://esm.sh/@johnhenry/definable/define-component.component/global.mjs"
29
+ ></script>
30
+ <define-component name="my-widget" src="./my-widget.mjs"></define-component>
31
+ <my-widget></my-widget>
32
+ ```
@@ -0,0 +1,13 @@
1
+ export default (elementClass) => (name) =>
2
+ globalThis.customElements.define(name, elementClass);
3
+
4
+ // export default (elementClass) => (name) => {
5
+ // try {
6
+ // globalThis.customElements.define(name, elementClass);
7
+ // } catch (e) {
8
+ // console.error(e);
9
+ // } finally {
10
+ // console.log({ elementClass });
11
+ // console.log({ name });
12
+ // }
13
+ // };
@@ -0,0 +1,18 @@
1
+ # Define Tag
2
+
3
+ A curried `customElements.define` wrapper: `(elementClass) => (name) =>
4
+ customElements.define(name, elementClass)`. Useful for separating "what a
5
+ component's class is" from "what tag name it gets registered under" —
6
+ several `.component` modules' `define.mjs`/`global.mjs` files use this
7
+ instead of calling `customElements.define` directly. See
8
+ [define-component-by-content.component](../define-component-by-content.component/readme.md)
9
+ for a real consumer.
10
+
11
+ ## Usage
12
+
13
+ ```js
14
+ import definetag from "../definetag/index.mjs";
15
+ import MyComponent from "./my-component.mjs";
16
+
17
+ definetag(MyComponent)("my-component");
18
+ ```
@@ -0,0 +1,89 @@
1
+ // https://gist.github.com/sechel/e6aff22d9e56df02c5bd09c4afc516e6
2
+ (function (global) {
3
+ var channels = [];
4
+
5
+ function BroadcastFlannel(channel) {
6
+ var $this = this;
7
+ channel = String(channel);
8
+
9
+ var id = "$BroadcastFlannel$" + channel + "$";
10
+
11
+ channels[id] = channels[id] || [];
12
+ channels[id].push(this);
13
+
14
+ this._name = channel;
15
+ this._id = id;
16
+ this._closed = false;
17
+ this._mc = new MessageChannel();
18
+ this._mc.port1.start();
19
+ this._mc.port2.start();
20
+
21
+ global.addEventListener("storage", function (e) {
22
+ if (e.storageArea !== global.localStorage) return;
23
+ if (e.newValue === null) return;
24
+ if (e.key.substring(0, id.length) !== id) return;
25
+ var data = JSON.parse(e.newValue);
26
+ $this._mc.port2.postMessage(data);
27
+ });
28
+ }
29
+
30
+ BroadcastFlannel.prototype = {
31
+ // BroadcastFlannel API
32
+ get name() {
33
+ return this._name;
34
+ },
35
+ postMessage: function (message) {
36
+ var $this = this;
37
+ if (this._closed) {
38
+ var e = new Error();
39
+ e.name = "InvalidStateError";
40
+ throw e;
41
+ }
42
+ var value = JSON.stringify(message);
43
+
44
+ // Broadcast to other contexts via storage events...
45
+ var key = this._id + String(Date.now()) + "$" + String(Math.random());
46
+ global.localStorage.setItem(key, value);
47
+ setTimeout(function () {
48
+ global.localStorage.removeItem(key);
49
+ }, 500);
50
+
51
+ // Broadcast to current context via ports
52
+ channels[this._id].forEach(function (bc) {
53
+ if (bc === $this) return;
54
+ bc._mc.port2.postMessage(JSON.parse(value));
55
+ });
56
+ },
57
+ close: function () {
58
+ if (this._closed) return;
59
+ this._closed = true;
60
+ this._mc.port1.close();
61
+ this._mc.port2.close();
62
+
63
+ var index = channels[this._id].indexOf(this);
64
+ channels[this._id].splice(index, 1);
65
+ },
66
+
67
+ // EventTarget API
68
+ get onmessage() {
69
+ return this._mc.port1.onmessage;
70
+ },
71
+ set onmessage(value) {
72
+ this._mc.port1.onmessage = value;
73
+ },
74
+ addEventListener: function (type, listener /*, useCapture*/) {
75
+ return this._mc.port1.addEventListener.apply(this._mc.port1, arguments);
76
+ },
77
+ removeEventListener: function (type, listener /*, useCapture*/) {
78
+ return this._mc.port1.removeEventListener.apply(
79
+ this._mc.port1,
80
+ arguments
81
+ );
82
+ },
83
+ dispatchEvent: function (event) {
84
+ return this._mc.port1.dispatchEvent.apply(this._mc.port1, arguments);
85
+ },
86
+ };
87
+
88
+ global.BroadcastFlannel = global.BroadcastFlannel || BroadcastFlannel;
89
+ })(globalThis);
@@ -0,0 +1,21 @@
1
+ <style>
2
+ .until-window-load {
3
+ visibility: hidden;
4
+ }
5
+ </style>
6
+ <script type="module" src="../until-window-load/index.mjs"></script>
7
+ <script
8
+ type="module"
9
+ src="../define-component.component/global.mjs"
10
+ ></script>
11
+ <define-component
12
+ class="until-window-load"
13
+ name="polyfill-window"
14
+ src="./index.mjs"
15
+ ></define-component>
16
+ <polyfill-window name="shout" src="./shout-polyfill.mjs"></polyfill-window>
17
+ <polyfill-window
18
+ name="BroadcastFhannel"
19
+ src="./broadcast-channel-polyfill.mjs"
20
+ no-import
21
+ ></polyfill-window>
@@ -0,0 +1,2 @@
1
+ import DefineComponent from "./index.mjs";
2
+ globalThis.customElements.define("polyfill-window", DefineComponent);
@@ -0,0 +1,35 @@
1
+ const define = async (src, name, imp, force, noImport) => {
2
+ const { href } = globalThis.location;
3
+ const indexQM = href.lastIndexOf("?");
4
+ const withoutQuery = indexQM === -1 ? href : href.substring(0, indexQM);
5
+ const indexS = withoutQuery.lastIndexOf("/");
6
+ const dirname =
7
+ indexS === -1 ? withoutQuery : withoutQuery.substring(0, indexS);
8
+ const url = new URL(src, dirname + "/");
9
+ if (!globalThis[name] || force !== null) {
10
+ const module = await import(url.href);
11
+ if (!noImport) {
12
+ const E = module[imp ?? "default"];
13
+ globalThis[name] = E;
14
+ }
15
+ }
16
+ };
17
+
18
+ export default class extends globalThis.HTMLElement {
19
+ #name = "";
20
+ #src = "";
21
+ #import = null;
22
+ #force = false;
23
+ #noImport = false;
24
+ constructor() {
25
+ super();
26
+ }
27
+ connectedCallback() {
28
+ this.#name = this.getAttribute("name");
29
+ this.#src = this.getAttribute("src");
30
+ this.#import = this.getAttribute("import");
31
+ this.#force = this.getAttribute("force");
32
+ this.#noImport = this.getAttribute("no-import");
33
+ define(this.#src, this.#name, this.#import, this.#force, this.#noImport);
34
+ }
35
+ }
@@ -0,0 +1,30 @@
1
+ # Polyfill Window
2
+
3
+ Dynamically imports a module and assigns its export to a key on
4
+ `globalThis` -- for loading a global polyfill declaratively from HTML,
5
+ rather than registering a custom element (compare
6
+ [define-component.component](../define-component.component/readme.md),
7
+ which this module's own `demo.html` uses to bootstrap itself and which
8
+ solves the related "register a custom element from a URL" problem).
9
+
10
+ ## Attributes
11
+
12
+ | Attribute | Description |
13
+ |---|---|
14
+ | `name` | Global key to assign the import to (required) |
15
+ | `src` | URL of the module to import, resolved relative to the current document (required) |
16
+ | `import` | Named export to assign. Defaults to the module's default export |
17
+ | `force` | If present, re-imports and re-assigns even if `globalThis[name]` is already set |
18
+ | `no-import` | If present, the module is imported (for its side effects) but nothing is assigned to `globalThis` |
19
+
20
+ ## Usage
21
+
22
+ ```html
23
+ <script
24
+ type="module"
25
+ src="https://esm.sh/@johnhenry/definable/define-component.component/global.mjs"
26
+ ></script>
27
+ <define-component name="polyfill-window" src="./index.mjs"></define-component>
28
+
29
+ <polyfill-window name="shout" src="./shout-polyfill.mjs"></polyfill-window>
30
+ ```
@@ -0,0 +1 @@
1
+ export default (text) => alert(text.toUpperCase());
@@ -0,0 +1,2 @@
1
+ import removeLoadingClasses from "./index.mjs";
2
+ removeLoadingClasses("until-window-load");
@@ -0,0 +1,9 @@
1
+ export default (...classNames) => {
2
+ window.addEventListener("load", () => {
3
+ for (const className of classNames) {
4
+ window.document.querySelectorAll(`.${className}`).forEach((loading) => {
5
+ loading.classList.remove(className);
6
+ });
7
+ }
8
+ });
9
+ };
@@ -0,0 +1,49 @@
1
+ # Until Window Load
2
+
3
+ Declarative import to remove loading classes once the window has loaded.
4
+
5
+ Useful to avoid content flash for custom components that arrange content
6
+ after loading. Used by [polyfill-window.component](../polyfill-window.component/readme.md)'s
7
+ own demo.
8
+
9
+ ## Usage
10
+
11
+ ### Manual
12
+
13
+ Define the class to be removed:
14
+
15
+ ```js
16
+ import removeLoadingClasses from "https://esm.sh/@johnhenry/definable/until-window-load/index.mjs";
17
+ removeLoadingClasses("custom-loading-class");
18
+ ```
19
+
20
+ ```css
21
+ .custom-loading-class {
22
+ visibility: hidden;
23
+ }
24
+ ```
25
+
26
+ ```html
27
+ <custom-component class="custom-loading-class"
28
+ >Hide me until window load</custom-component
29
+ >
30
+ ```
31
+
32
+ ### Automatic
33
+
34
+ Use the "global" import to automatically use the class name `until-window-load`:
35
+
36
+ ```html
37
+ <script
38
+ type="module"
39
+ src="https://esm.sh/@johnhenry/definable/until-window-load/global.mjs"
40
+ ></script>
41
+ <style>
42
+ .until-window-load {
43
+ visibility: hidden;
44
+ }
45
+ </style>
46
+ <custom-component class="until-window-load"
47
+ >Hide me until window load</custom-component
48
+ >
49
+ ```