@excom/loadable-element 0.1.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.
Files changed (28) hide show
  1. package/.rush/temp/chunked-rush-logs/loadable-element.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/loadable-element.build_docs.chunks.jsonl +1 -0
  3. package/.rush/temp/chunked-rush-logs/loadable-element.build_package-metas.chunks.jsonl +1 -0
  4. package/.rush/temp/operation/apply-exports/all.log +1 -0
  5. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  6. package/.rush/temp/operation/apply-exports/state.json +3 -0
  7. package/.rush/temp/operation/build_docs/all.log +1 -0
  8. package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
  9. package/.rush/temp/operation/build_docs/state.json +3 -0
  10. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  11. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  12. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  13. package/.rush/temp/shrinkwrap-deps.json +3 -0
  14. package/config/rig.json +6 -0
  15. package/index.ts +118 -0
  16. package/package.json +44 -0
  17. package/rush-logs/loadable-element.apply-exports.cache.log +1 -0
  18. package/rush-logs/loadable-element.apply-exports.log +1 -0
  19. package/rush-logs/loadable-element.build_docs.cache.log +1 -0
  20. package/rush-logs/loadable-element.build_docs.log +1 -0
  21. package/rush-logs/loadable-element.build_package-metas.cache.log +1 -0
  22. package/rush-logs/loadable-element.build_package-metas.log +1 -0
  23. package/support/custom-elements.json +143 -0
  24. package/support/dist-docs/loadable-element.md +93 -0
  25. package/support/docs/README.md +51 -0
  26. package/support/package-meta.json +95 -0
  27. package/support/tests/loadable-element.test.ts +93 -0
  28. package/tsconfig.json +5 -0
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
@@ -0,0 +1 @@
1
+ Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "nonCachedDurationMs": 34.70067400000016
3
+ }
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "nonCachedDurationMs": 42.185382000000004
3
+ }
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "nonCachedDurationMs": 123.15210000000002
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "../../packages/loadable-element": "../../packages/loadable-element:YAC8G8l1nkKknuTXH370JI0/DynCPkP81MNQcvbO6fA=:"
3
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
3
+ "rigPackageName": "@excom/heft-rig",
4
+ "rigProfile": "default"
5
+ }
6
+
package/index.ts ADDED
@@ -0,0 +1,118 @@
1
+ import { ConstructorType, Neutron, TEvent } from "@excom/neutron";
2
+
3
+ export type LoadableLoadingEvent = TEvent & {
4
+ type: "{tag}-loading";
5
+ detail: void;
6
+ };
7
+
8
+ export type LoadableSuccessEvent<P = unknown> = TEvent & {
9
+ type: "{tag}-success";
10
+ detail: P;
11
+ };
12
+
13
+ export type LoadableErrorEvent<E = unknown> = TEvent & {
14
+ type: "{tag}-error";
15
+ detail: E;
16
+ };
17
+
18
+ /**
19
+ * Composition base for the loading / success / error lifecycle every
20
+ * "do async work, then publish" element shares — fetches, sheet loads,
21
+ * template loads, third-party widget boots. Owns the three mutually
22
+ * exclusive state attributes, the `provision` payload and the three
23
+ * tag-prefixed events, so a concrete element only decides *when* work
24
+ * starts and what it yields. Not a registered element on its own (tag is
25
+ * intentionally `noop-tag`) — compose it via
26
+ * `Neutron.compose([LoadableElement, …])`. Consumers include
27
+ * `FetchableElement` (and through it `<provider-fetch>`, `<super-form>`,
28
+ * `<web-authn>`) and `<quark-sheet>`.
29
+ *
30
+ * Subclasses drive it with three effects: `_setLoading: []`,
31
+ * `_setSuccess: [provision]`, `_setError: [error]`; `_resetLoadState: []`
32
+ * clears all three states without an event (a cancelled request).
33
+ *
34
+ * @summary Loading / success / error state + provision + events, once.
35
+ *
36
+ * @fires {tag}-loading - After `is-loading` is set (work started).
37
+ * @type LoadableLoadingEvent
38
+ * @fires {tag}-success - After `is-success` is set. `event.detail` is the
39
+ * new `provision`.
40
+ * @type LoadableSuccessEvent
41
+ * @fires {tag}-error - After `is-error` is set. `event.detail` is the
42
+ * error payload (also stored as `provision`).
43
+ * @type LoadableErrorEvent
44
+ */
45
+ export const LoadableElement = Neutron({
46
+ tag: "noop-tag",
47
+ events: {
48
+ loading: {
49
+ prefixWithTag: true,
50
+ },
51
+ success: {
52
+ prefixWithTag: true,
53
+ },
54
+ error: {
55
+ prefixWithTag: true,
56
+ },
57
+ },
58
+ props: {
59
+ // public state
60
+ /**
61
+ * @state
62
+ * Work is in flight.
63
+ */
64
+ isLoading: Boolean,
65
+ /**
66
+ * @state
67
+ * The most recent work finished successfully. Mutually exclusive with
68
+ * `is-error`.
69
+ */
70
+ isSuccess: Boolean,
71
+ /**
72
+ * @state
73
+ * The most recent work failed. Mutually exclusive with `is-success`.
74
+ */
75
+ isError: Boolean,
76
+ /**
77
+ * @provision
78
+ * The result of the most recent work on success, or the error payload
79
+ * on failure. Shape is defined by the composing element. Not
80
+ * reflected as an attribute.
81
+ */
82
+ provision: Object as unknown as ConstructorType<unknown>,
83
+ },
84
+ }).defineMethods({
85
+ /** Enter the loading state and announce it. */
86
+ _setLoading: () => ({
87
+ isLoading: true,
88
+ isSuccess: false,
89
+ isError: false,
90
+ emit: ["loading"],
91
+ }),
92
+ /** Publish `provision`, enter the success state and announce it. */
93
+ _setSuccess: (_, provision: unknown) => [
94
+ {
95
+ isLoading: false,
96
+ isSuccess: true,
97
+ isError: false,
98
+ provision,
99
+ },
100
+ { emit: ["success", { detail: provision }] },
101
+ ],
102
+ /** Store the error as `provision`, enter the error state and announce it. */
103
+ _setError: (_, error: unknown) => [
104
+ {
105
+ isLoading: false,
106
+ isSuccess: false,
107
+ isError: true,
108
+ provision: error,
109
+ },
110
+ { emit: ["error", { detail: error }] },
111
+ ],
112
+ /** Clear every state without an event, cancelled / superseded work. */
113
+ _resetLoadState: () => ({
114
+ isLoading: false,
115
+ isSuccess: false,
116
+ isError: false,
117
+ }),
118
+ });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@excom/loadable-element",
3
+ "version": "0.1.0",
4
+ "description": "LoadableElement base for Neutron elements",
5
+ "license": "MIT",
6
+ "engines": {
7
+ "node": ">=24.13.0"
8
+ },
9
+ "type": "module",
10
+ "dependencies": {
11
+ "@excom/neutron": "^0.1.0"
12
+ },
13
+ "peerDependencies": {},
14
+ "devDependencies": {
15
+ "@excom/heft-rig": "^0.1.0"
16
+ },
17
+ "repository": {
18
+ "url": "excom-dev/nucleus",
19
+ "directory": "packages/loadable-element"
20
+ },
21
+ "homepage": "https://github.com/excom-dev/nucleus/tree/main/packages/loadable-element/support/docs/README.md",
22
+ "bugs": "https://github.com/excom-dev/nucleus/issues",
23
+ "keywords": [
24
+ "LoadableElement",
25
+ "loadable-element",
26
+ "neutron",
27
+ "kit-element-base",
28
+ "custom-elements"
29
+ ],
30
+ "excom": {
31
+ "packageType": "element-base"
32
+ },
33
+ "scripts": {
34
+ "build": "node node_modules/@excom/heft-rig/scripts/vite-build.mjs",
35
+ "build:watch": "node node_modules/@excom/heft-rig/scripts/vite-build-watch.mjs",
36
+ "format": "node node_modules/@excom/heft-rig/scripts/format.mjs",
37
+ "test": "node node_modules/@excom/heft-rig/scripts/vitest.mjs",
38
+ "coverage": "node node_modules/@excom/heft-rig/scripts/coverage.mjs",
39
+ "dev": "node node_modules/@excom/heft-rig/scripts/vite-dev.mjs",
40
+ "preview": "node node_modules/@excom/heft-rig/scripts/vite-preview.mjs",
41
+ "build:package-metas": "node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs",
42
+ "build:docs": "node node_modules/@excom/heft-rig/scripts/build-docs.mjs"
43
+ }
44
+ }
@@ -0,0 +1 @@
1
+ Caching has been disabled for this project's "apply-exports" command.
@@ -0,0 +1 @@
1
+ Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
@@ -0,0 +1 @@
1
+ This project does not define the caching behavior of the "build:docs" command, so caching has been disabled.
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
@@ -0,0 +1 @@
1
+ This project does not define the caching behavior of the "build:package-metas" command, so caching has been disabled.
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
@@ -0,0 +1,143 @@
1
+ {
2
+ "schemaVersion": "1.0.0",
3
+ "modules": [
4
+ {
5
+ "kind": "javascript-module",
6
+ "path": "index.ts",
7
+ "declarations": [
8
+ {
9
+ "kind": "mixin",
10
+ "name": "LoadableElement",
11
+ "summary": "Loading / success / error state + provision + events, once.",
12
+ "description": "Composition base for the loading / success / error lifecycle every \"do async work, then publish\" element shares — fetches, sheet loads, template loads, third-party widget boots. Owns the three mutually exclusive state attributes, the `provision` payload and the three tag-prefixed events, so a concrete element only decides *when* work starts and what it yields. Not a registered element on its own (tag is intentionally `noop-tag`) — compose it via `Neutron.compose([LoadableElement, …])`. Consumers include `FetchableElement` (and through it `<provider-fetch>`, `<super-form>`, `<web-authn>`) and `<quark-sheet>`. Subclasses drive it with three effects: `_setLoading: []`, `_setSuccess: [provision]`, `_setError: [error]`; `_resetLoadState: []` clears all three states without an event (a cancelled request).",
13
+ "attributes": [
14
+ {
15
+ "name": "is-loading",
16
+ "type": {
17
+ "text": "boolean"
18
+ },
19
+ "description": "Work is in flight.",
20
+ "fieldName": "isLoading"
21
+ },
22
+ {
23
+ "name": "is-success",
24
+ "type": {
25
+ "text": "boolean"
26
+ },
27
+ "description": "The most recent work finished successfully. Mutually exclusive with `is-error`.",
28
+ "fieldName": "isSuccess"
29
+ },
30
+ {
31
+ "name": "is-error",
32
+ "type": {
33
+ "text": "boolean"
34
+ },
35
+ "description": "The most recent work failed. Mutually exclusive with `is-success`.",
36
+ "fieldName": "isError"
37
+ }
38
+ ],
39
+ "members": [
40
+ {
41
+ "kind": "field",
42
+ "name": "isLoading",
43
+ "type": {
44
+ "text": "boolean"
45
+ },
46
+ "privacy": "public",
47
+ "readonly": true,
48
+ "description": "Work is in flight.",
49
+ "_neutron": {
50
+ "surface": "state"
51
+ }
52
+ },
53
+ {
54
+ "kind": "field",
55
+ "name": "isSuccess",
56
+ "type": {
57
+ "text": "boolean"
58
+ },
59
+ "privacy": "public",
60
+ "readonly": true,
61
+ "description": "The most recent work finished successfully. Mutually exclusive with `is-error`.",
62
+ "_neutron": {
63
+ "surface": "state"
64
+ }
65
+ },
66
+ {
67
+ "kind": "field",
68
+ "name": "isError",
69
+ "type": {
70
+ "text": "boolean"
71
+ },
72
+ "privacy": "public",
73
+ "readonly": true,
74
+ "description": "The most recent work failed. Mutually exclusive with `is-success`.",
75
+ "_neutron": {
76
+ "surface": "state"
77
+ }
78
+ },
79
+ {
80
+ "kind": "field",
81
+ "name": "provision",
82
+ "type": {
83
+ "text": "unknown"
84
+ },
85
+ "privacy": "public",
86
+ "readonly": false,
87
+ "description": "The result of the most recent work on success, or the error payload on failure. Shape is defined by the composing element. Not reflected as an attribute.",
88
+ "_neutron": {
89
+ "surface": "option"
90
+ }
91
+ }
92
+ ],
93
+ "events": [
94
+ {
95
+ "name": "{tag}-loading",
96
+ "description": "After `is-loading` is set (work started).",
97
+ "type": {
98
+ "text": "LoadableLoadingEvent",
99
+ "expanded": "CustomEvent & { type: \"{tag}-loading\"; detail: void; bubbles: true; cancelable: true; composed: true }"
100
+ }
101
+ },
102
+ {
103
+ "name": "{tag}-success",
104
+ "description": "After `is-success` is set. `event.detail` is the new `provision`.",
105
+ "type": {
106
+ "text": "LoadableSuccessEvent"
107
+ }
108
+ },
109
+ {
110
+ "name": "{tag}-error",
111
+ "description": "After `is-error` is set. `event.detail` is the error payload (also stored as `provision`).",
112
+ "type": {
113
+ "text": "LoadableErrorEvent"
114
+ }
115
+ }
116
+ ],
117
+ "_neutron": {
118
+ "provisions": [
119
+ {
120
+ "name": "provision",
121
+ "type": {
122
+ "text": "unknown"
123
+ },
124
+ "description": "The result of the most recent work on success, or the error payload on failure. Shape is defined by the composing element. Not reflected as an attribute.",
125
+ "fieldName": "provision"
126
+ }
127
+ ]
128
+ }
129
+ }
130
+ ],
131
+ "exports": [
132
+ {
133
+ "kind": "js",
134
+ "name": "LoadableElement",
135
+ "declaration": {
136
+ "name": "LoadableElement",
137
+ "module": "index.ts"
138
+ }
139
+ }
140
+ ]
141
+ }
142
+ ]
143
+ }
@@ -0,0 +1,93 @@
1
+ # loadable-element
2
+
3
+ Loading / success / error state, a `provision`, and the three events — once, for every element that does async work.
4
+
5
+ ## Features
6
+
7
+ - **Three states** `is-loading` / `is-success` / `is-error`, mutually exclusive
8
+ - **One payload** The result or the error lands on `provision`
9
+ - **Three events** `{tag}-loading` / `{tag}-success` / `{tag}-error`, tag-prefixed automatically
10
+ - **Four effects** `_setLoading`, `_setSuccess`, `_setError`, `_resetLoadState` — the element decides *when*, the base does the bookkeeping
11
+
12
+ ## Installation
13
+
14
+
15
+ `@excom/loadable-element` v0.1.0
16
+
17
+ ```bash
18
+ pnpm add @excom/loadable-element
19
+ ```
20
+
21
+ ```bash
22
+ npm install @excom/loadable-element
23
+ ```
24
+
25
+ ```bash
26
+ yarn add @excom/loadable-element
27
+ ```
28
+
29
+ ### Import
30
+
31
+ ```ts
32
+ import { /* … */ } from "@excom/loadable-element";
33
+ ```
34
+
35
+
36
+
37
+ ## Usage
38
+
39
+ Compose `LoadableElement` and call its effects from your own lifecycles. `FetchableElement` (and every element built on it) and `quark-sheet` compose it this way.
40
+
41
+ ```ts
42
+ import { LoadableElement } from "@excom/loadable-element";
43
+ import { Neutron } from "@excom/neutron";
44
+
45
+ export const LoadJson = Neutron.compose([
46
+ LoadableElement,
47
+ Neutron({ tag: "load-json", props: { srcUrl: String, _promise: Promise } }),
48
+ ])
49
+ .onPropSet("srcUrl", ({ srcUrl }) => [
50
+ { _setLoading: [] },
51
+ { _promise: fetch(srcUrl).then((r) => r.json()) },
52
+ ])
53
+ .onPromiseResolved("_promise", (_, { _promise }) => ({ _setSuccess: [_promise] }))
54
+ .onPromiseRejected("_promise", (_, { _promise }) => ({ _setError: [_promise] }));
55
+
56
+ LoadJson.define();
57
+ ```
58
+
59
+ ```html
60
+ <load-json src-url="/api/user"></load-json>
61
+ ```
62
+
63
+ ```quark
64
+ load-json[is-success] { $user: prop("provision"); }
65
+ load-json[is-error] [bind-message] { content: prop("provision").message; }
66
+ ```
67
+
68
+ Cancelled or superseded work calls `_resetLoadState` (no event) — pair with [abortable-element](/nucleus/packages/abortable-element) to abort the promise itself.
69
+
70
+ ### API Reference
71
+
72
+
73
+ #### Attributes
74
+
75
+ | Name | Surface | Type | Default | Values | Description |
76
+ | --- | --- | --- | --- | --- | --- |
77
+ | `is-loading` | state | `boolean` | | | Work is in flight. |
78
+ | `is-success` | state | `boolean` | | | The most recent work finished successfully. Mutually exclusive with `is-error`. |
79
+ | `is-error` | state | `boolean` | | | The most recent work failed. Mutually exclusive with `is-success`. |
80
+
81
+ #### Provision
82
+
83
+ | Name | Type | Description |
84
+ | --- | --- | --- |
85
+ | `provision` | `unknown` | The result of the most recent work on success, or the error payload on failure. Shape is defined by the composing element. Not reflected as an attribute. |
86
+
87
+ #### Fires
88
+
89
+ | Name | Type | Description |
90
+ | --- | --- | --- |
91
+ | `{tag}-loading` | `LoadableLoadingEvent` (`CustomEvent & { type: "{tag}-loading"; detail: void; bubbles: true; cancelable: true; composed: true }`) | After `is-loading` is set (work started). |
92
+ | `{tag}-success` | `LoadableSuccessEvent` | After `is-success` is set. `event.detail` is the new `provision`. |
93
+ | `{tag}-error` | `LoadableErrorEvent` | After `is-error` is set. `event.detail` is the error payload (also stored as `provision`). |
@@ -0,0 +1,51 @@
1
+ # loadable-element
2
+
3
+ Loading / success / error state, a `provision`, and the three events — once, for every element that does async work.
4
+
5
+ ## Features
6
+
7
+ - **Three states** `is-loading` / `is-success` / `is-error`, mutually exclusive
8
+ - **One payload** The result or the error lands on `provision`
9
+ - **Three events** `{tag}-loading` / `{tag}-success` / `{tag}-error`, tag-prefixed automatically
10
+ - **Four effects** `_setLoading`, `_setSuccess`, `_setError`, `_resetLoadState` — the element decides *when*, the base does the bookkeeping
11
+
12
+ ## Installation
13
+
14
+ <include-content is-active template-ref="/views/install-section/install-section.html"></include-content>
15
+
16
+ ## Usage
17
+
18
+ Compose `LoadableElement` and call its effects from your own lifecycles. `FetchableElement` (and every element built on it) and `quark-sheet` compose it this way.
19
+
20
+ ```ts
21
+ import { LoadableElement } from "@excom/loadable-element";
22
+ import { Neutron } from "@excom/neutron";
23
+
24
+ export const LoadJson = Neutron.compose([
25
+ LoadableElement,
26
+ Neutron({ tag: "load-json", props: { srcUrl: String, _promise: Promise } }),
27
+ ])
28
+ .onPropSet("srcUrl", ({ srcUrl }) => [
29
+ { _setLoading: [] },
30
+ { _promise: fetch(srcUrl).then((r) => r.json()) },
31
+ ])
32
+ .onPromiseResolved("_promise", (_, { _promise }) => ({ _setSuccess: [_promise] }))
33
+ .onPromiseRejected("_promise", (_, { _promise }) => ({ _setError: [_promise] }));
34
+
35
+ LoadJson.define();
36
+ ```
37
+
38
+ ```html
39
+ <load-json src-url="/api/user"></load-json>
40
+ ```
41
+
42
+ ```quark
43
+ load-json[is-success] { $user: prop("provision"); }
44
+ load-json[is-error] [bind-message] { content: prop("provision").message; }
45
+ ```
46
+
47
+ Cancelled or superseded work calls `_resetLoadState` (no event) — pair with [abortable-element](/nucleus/packages/abortable-element) to abort the promise itself.
48
+
49
+ ### API Reference
50
+
51
+ <include-content is-active template-ref="/views/api-reference/api-reference.html"></include-content>
@@ -0,0 +1,95 @@
1
+ {
2
+ "shortName": "loadable-element",
3
+ "package": {
4
+ "name": "@excom/loadable-element",
5
+ "version": "0.1.0",
6
+ "description": "LoadableElement base for Neutron elements",
7
+ "peerDependencies": {},
8
+ "excom": {
9
+ "packageType": "element-base"
10
+ }
11
+ },
12
+ "demos": {},
13
+ "readme": "<h1 id=\"md-loadable-element\">loadable-element</h1>\n<p>Loading / success / error state, a <code>provision</code>, and the three events — once, for every element that does async work.</p>\n<h2 id=\"md-features\">Features</h2>\n<ul>\n<li><strong>Three states</strong> <code>is-loading</code> / <code>is-success</code> / <code>is-error</code>, mutually exclusive</li>\n<li><strong>One payload</strong> The result or the error lands on <code>provision</code></li>\n<li><strong>Three events</strong> <code>{tag}-loading</code> / <code>{tag}-success</code> / <code>{tag}-error</code>, tag-prefixed automatically</li>\n<li><strong>Four effects</strong> <code>_setLoading</code>, <code>_setSuccess</code>, <code>_setError</code>, <code>_resetLoadState</code> — the element decides <em>when</em>, the base does the bookkeeping</li>\n</ul>\n<h2 id=\"md-installation\">Installation</h2>\n<p><include-content is-active template-ref=\"/views/install-section/install-section.html\"></include-content></p>\n<h2 id=\"md-usage\">Usage</h2>\n<p>Compose <code>LoadableElement</code> and call its effects from your own lifecycles. <code>FetchableElement</code> (and every element built on it) and <code>quark-sheet</code> compose it this way.</p>\n<include-content data-language=\"ts\"><template>import { LoadableElement } from \"@excom/loadable-element\";\nimport { Neutron } from \"@excom/neutron\";\n\nexport const LoadJson = Neutron.compose([\n LoadableElement,\n Neutron({ tag: \"load-json\", props: { srcUrl: String, _promise: Promise } }),\n])\n .onPropSet(\"srcUrl\", ({ srcUrl }) =&gt; [\n { _setLoading: [] },\n { _promise: fetch(srcUrl).then((r) =&gt; r.json()) },\n ])\n .onPromiseResolved(\"_promise\", (_, { _promise }) =&gt; ({ _setSuccess: [_promise] }))\n .onPromiseRejected(\"_promise\", (_, { _promise }) =&gt; ({ _setError: [_promise] }));\n\nLoadJson.define();</template></include-content>\n<include-content data-language=\"html\"><template>&lt;load-json src-url=\"/api/user\"&gt;&lt;/load-json&gt;</template></include-content>\n<include-content data-language=\"quark\"><template>load-json[is-success] { $user: prop(\"provision\"); }\nload-json[is-error] [bind-message] { content: prop(\"provision\").message; }</template></include-content>\n<p>Cancelled or superseded work calls <code>_resetLoadState</code> (no event) — pair with <spa-a route-href=\"/nucleus/packages/abortable-element\" role=\"link\">abortable-element</spa-a> to abort the promise itself.</p>\n<h3 id=\"md-api-reference\">API Reference</h3>\n<p><include-content is-active template-ref=\"/views/api-reference/api-reference.html\"></include-content></p>\n",
14
+ "docs": {
15
+ "readme": "<h1 id=\"md-loadable-element\">loadable-element</h1>\n<p>Loading / success / error state, a <code>provision</code>, and the three events — once, for every element that does async work.</p>\n<h2 id=\"md-features\">Features</h2>\n<ul>\n<li><strong>Three states</strong> <code>is-loading</code> / <code>is-success</code> / <code>is-error</code>, mutually exclusive</li>\n<li><strong>One payload</strong> The result or the error lands on <code>provision</code></li>\n<li><strong>Three events</strong> <code>{tag}-loading</code> / <code>{tag}-success</code> / <code>{tag}-error</code>, tag-prefixed automatically</li>\n<li><strong>Four effects</strong> <code>_setLoading</code>, <code>_setSuccess</code>, <code>_setError</code>, <code>_resetLoadState</code> — the element decides <em>when</em>, the base does the bookkeeping</li>\n</ul>\n<h2 id=\"md-installation\">Installation</h2>\n<p><include-content is-active template-ref=\"/views/install-section/install-section.html\"></include-content></p>\n<h2 id=\"md-usage\">Usage</h2>\n<p>Compose <code>LoadableElement</code> and call its effects from your own lifecycles. <code>FetchableElement</code> (and every element built on it) and <code>quark-sheet</code> compose it this way.</p>\n<include-content data-language=\"ts\"><template>import { LoadableElement } from \"@excom/loadable-element\";\nimport { Neutron } from \"@excom/neutron\";\n\nexport const LoadJson = Neutron.compose([\n LoadableElement,\n Neutron({ tag: \"load-json\", props: { srcUrl: String, _promise: Promise } }),\n])\n .onPropSet(\"srcUrl\", ({ srcUrl }) =&gt; [\n { _setLoading: [] },\n { _promise: fetch(srcUrl).then((r) =&gt; r.json()) },\n ])\n .onPromiseResolved(\"_promise\", (_, { _promise }) =&gt; ({ _setSuccess: [_promise] }))\n .onPromiseRejected(\"_promise\", (_, { _promise }) =&gt; ({ _setError: [_promise] }));\n\nLoadJson.define();</template></include-content>\n<include-content data-language=\"html\"><template>&lt;load-json src-url=\"/api/user\"&gt;&lt;/load-json&gt;</template></include-content>\n<include-content data-language=\"quark\"><template>load-json[is-success] { $user: prop(\"provision\"); }\nload-json[is-error] [bind-message] { content: prop(\"provision\").message; }</template></include-content>\n<p>Cancelled or superseded work calls <code>_resetLoadState</code> (no event) — pair with <spa-a route-href=\"/nucleus/packages/abortable-element\" role=\"link\">abortable-element</spa-a> to abort the promise itself.</p>\n<h3 id=\"md-api-reference\">API Reference</h3>\n<p><include-content is-active template-ref=\"/views/api-reference/api-reference.html\"></include-content></p>\n"
16
+ },
17
+ "installation": {
18
+ "name": "@excom/loadable-element",
19
+ "shortName": "loadable-element",
20
+ "version": "0.1.0",
21
+ "description": "LoadableElement base for Neutron elements",
22
+ "packageType": "element-base",
23
+ "install": {
24
+ "npm": "npm install @excom/loadable-element"
25
+ },
26
+ "imports": {
27
+ "js": "import { /* … */ } from \"@excom/loadable-element\";"
28
+ },
29
+ "peerDependencies": []
30
+ },
31
+ "elementApis": [
32
+ {
33
+ "summary": "Loading / success / error state + provision + events, once.",
34
+ "kind": "mixin",
35
+ "attributes": [
36
+ {
37
+ "name": "is-error",
38
+ "type": "boolean",
39
+ "description": "The most recent work failed. Mutually exclusive with <code>is-success</code>.",
40
+ "fieldName": "isError",
41
+ "surface": "state"
42
+ },
43
+ {
44
+ "name": "is-loading",
45
+ "type": "boolean",
46
+ "description": "Work is in flight.",
47
+ "fieldName": "isLoading",
48
+ "surface": "state"
49
+ },
50
+ {
51
+ "name": "is-success",
52
+ "type": "boolean",
53
+ "description": "The most recent work finished successfully. Mutually exclusive with <code>is-error</code>.",
54
+ "fieldName": "isSuccess",
55
+ "surface": "state"
56
+ }
57
+ ],
58
+ "events": [
59
+ {
60
+ "name": "{tag}-error",
61
+ "description": "After <code>is-error</code> is set. <code>event.detail</code> is the error payload (also stored as <code>provision</code>).",
62
+ "type": "LoadableErrorEvent"
63
+ },
64
+ {
65
+ "name": "{tag}-loading",
66
+ "description": "After <code>is-loading</code> is set (work started).",
67
+ "type": "LoadableLoadingEvent",
68
+ "typeExpanded": "CustomEvent & { type: \"{tag}-loading\"; detail: void; bubbles: true; cancelable: true; composed: true }"
69
+ },
70
+ {
71
+ "name": "{tag}-success",
72
+ "description": "After <code>is-success</code> is set. <code>event.detail</code> is the new <code>provision</code>.",
73
+ "type": "LoadableSuccessEvent"
74
+ }
75
+ ],
76
+ "slots": [],
77
+ "cssProperties": [],
78
+ "cssClasses": [],
79
+ "cssAliases": [],
80
+ "listens": [],
81
+ "commands": [],
82
+ "defaultActions": [],
83
+ "expectedChildren": [],
84
+ "provisions": [
85
+ {
86
+ "name": "provision",
87
+ "type": "unknown",
88
+ "description": "The result of the most recent work on success, or the error payload on failure. Shape is defined by the composing element. Not reflected as an attribute.",
89
+ "fieldName": "provision"
90
+ }
91
+ ]
92
+ }
93
+ ],
94
+ "exportedFiles": {}
95
+ }
@@ -0,0 +1,93 @@
1
+ import { LoadableElement } from "../../index";
2
+ import {
3
+ afterEach,
4
+ describe,
5
+ expect,
6
+ fixture,
7
+ it,
8
+ vi,
9
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
10
+
11
+ const TAG = "loadable-element-test";
12
+ if (!customElements.get(TAG)) {
13
+ LoadableElement.define(TAG);
14
+ }
15
+ // Events use the base's config tag (`noop-tag`), not the defined tag
16
+ // (same as FetchableElement).
17
+ const EVT = (name: string) => `noop-tag-${name}`;
18
+
19
+ const listen = (el: HTMLElement, type: string) => {
20
+ const spy = vi.fn();
21
+ el.addEventListener(type, spy);
22
+ return spy;
23
+ };
24
+
25
+ describe("LoadableElement", () => {
26
+ afterEach(() => {
27
+ document.body.innerHTML = "";
28
+ });
29
+
30
+ it("starts with no state and no provision", () => {
31
+ const el = fixture<any>(`<${TAG}></${TAG}>`);
32
+ expect(el.isLoading).toBeFalsy();
33
+ expect(el.isSuccess).toBeFalsy();
34
+ expect(el.isError).toBeFalsy();
35
+ expect(el.provision).toBeFalsy();
36
+ });
37
+
38
+ it("_setLoading reflects is-loading and emits the loading event", () => {
39
+ const el = fixture<any>(`<${TAG}></${TAG}>`);
40
+ const loading = listen(el, EVT("loading"));
41
+ el._setLoading();
42
+ expect(el).dom.to.equalTag(`<${TAG} is-loading></${TAG}>`);
43
+ expect(loading).toHaveBeenCalledTimes(1);
44
+ });
45
+
46
+ it("_setSuccess publishes the provision, flips to is-success and emits with detail", () => {
47
+ const el = fixture<any>(`<${TAG}></${TAG}>`);
48
+ const success = listen(el, EVT("success"));
49
+ const provisioned = listen(el, "neutron-provision");
50
+ el._setLoading();
51
+ el._setSuccess({ ok: true });
52
+ expect(el).dom.to.equalTag(`<${TAG} is-success></${TAG}>`);
53
+ expect(el.provision).toEqual({ ok: true });
54
+ expect(success).toHaveBeenCalledTimes(1);
55
+ expect(success.mock.calls[0][0].detail).toEqual({ ok: true });
56
+ expect(provisioned).toHaveBeenCalledTimes(1);
57
+ });
58
+
59
+ it("_setError stores the error as provision, flips to is-error and emits with detail", () => {
60
+ const el = fixture<any>(`<${TAG}></${TAG}>`);
61
+ const error = listen(el, EVT("error"));
62
+ el._setLoading();
63
+ el._setError({ message: "boom" });
64
+ expect(el).dom.to.equalTag(`<${TAG} is-error></${TAG}>`);
65
+ expect(el.provision).toEqual({ message: "boom" });
66
+ expect(error).toHaveBeenCalledTimes(1);
67
+ expect(error.mock.calls[0][0].detail).toEqual({ message: "boom" });
68
+ });
69
+
70
+ it("states are mutually exclusive across transitions", () => {
71
+ const el = fixture<any>(`<${TAG}></${TAG}>`);
72
+ el._setSuccess(1);
73
+ el._setLoading();
74
+ expect(el).dom.to.equalTag(`<${TAG} is-loading></${TAG}>`);
75
+ el._setError("x");
76
+ expect(el).dom.to.equalTag(`<${TAG} is-error></${TAG}>`);
77
+ el._setSuccess(2);
78
+ expect(el).dom.to.equalTag(`<${TAG} is-success></${TAG}>`);
79
+ });
80
+
81
+ it("_resetLoadState clears every state silently and keeps the provision", () => {
82
+ const el = fixture<any>(`<${TAG}></${TAG}>`);
83
+ const spies = ["loading", "success", "error"].map((t) =>
84
+ listen(el, EVT(t))
85
+ );
86
+ el._setSuccess("kept");
87
+ spies.forEach((s) => s.mockClear());
88
+ el._resetLoadState();
89
+ expect(el).dom.to.equalTag(`<${TAG}></${TAG}>`);
90
+ expect(el.provision).toBe("kept");
91
+ spies.forEach((s) => expect(s).not.toHaveBeenCalled());
92
+ });
93
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "@excom/heft-rig/profiles/default/config/tsconfig.json",
3
+ "include": ["./*.ts"],
4
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
5
+ }