@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.
- package/.rush/temp/chunked-rush-logs/loadable-element.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/loadable-element.build_docs.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/loadable-element.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_docs/all.log +1 -0
- package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_docs/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +6 -0
- package/index.ts +118 -0
- package/package.json +44 -0
- package/rush-logs/loadable-element.apply-exports.cache.log +1 -0
- package/rush-logs/loadable-element.apply-exports.log +1 -0
- package/rush-logs/loadable-element.build_docs.cache.log +1 -0
- package/rush-logs/loadable-element.build_docs.log +1 -0
- package/rush-logs/loadable-element.build_package-metas.cache.log +1 -0
- package/rush-logs/loadable-element.build_package-metas.log +1 -0
- package/support/custom-elements.json +143 -0
- package/support/dist-docs/loadable-element.md +93 -0
- package/support/docs/README.md +51 -0
- package/support/package-meta.json +95 -0
- package/support/tests/loadable-element.test.ts +93 -0
- 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 @@
|
|
|
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 @@
|
|
|
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"}
|
package/config/rig.json
ADDED
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 }) => [\n { _setLoading: [] },\n { _promise: fetch(srcUrl).then((r) => r.json()) },\n ])\n .onPromiseResolved(\"_promise\", (_, { _promise }) => ({ _setSuccess: [_promise] }))\n .onPromiseRejected(\"_promise\", (_, { _promise }) => ({ _setError: [_promise] }));\n\nLoadJson.define();</template></include-content>\n<include-content data-language=\"html\"><template><load-json src-url=\"/api/user\"></load-json></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 }) => [\n { _setLoading: [] },\n { _promise: fetch(srcUrl).then((r) => r.json()) },\n ])\n .onPromiseResolved(\"_promise\", (_, { _promise }) => ({ _setSuccess: [_promise] }))\n .onPromiseRejected(\"_promise\", (_, { _promise }) => ({ _setError: [_promise] }));\n\nLoadJson.define();</template></include-content>\n<include-content data-language=\"html\"><template><load-json src-url=\"/api/user\"></load-json></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
|
+
});
|