@excom/provider-fetch 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 (35) hide show
  1. package/.rush/temp/chunked-rush-logs/provider-fetch.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/provider-fetch.build_docs.chunks.jsonl +1 -0
  3. package/.rush/temp/chunked-rush-logs/provider-fetch.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 +5 -0
  15. package/index.ts +17 -0
  16. package/package.json +44 -0
  17. package/provider-fetch.ts +59 -0
  18. package/rush-logs/provider-fetch.apply-exports.cache.log +1 -0
  19. package/rush-logs/provider-fetch.apply-exports.log +1 -0
  20. package/rush-logs/provider-fetch.build_docs.cache.log +1 -0
  21. package/rush-logs/provider-fetch.build_docs.log +1 -0
  22. package/rush-logs/provider-fetch.build_package-metas.cache.log +1 -0
  23. package/rush-logs/provider-fetch.build_package-metas.log +1 -0
  24. package/support/custom-elements.json +91 -0
  25. package/support/demos/refetch.html +35 -0
  26. package/support/demos/simple.html +13 -0
  27. package/support/dist-docs/provider-fetch.md +172 -0
  28. package/support/docs/README.md +52 -0
  29. package/support/package-meta.json +214 -0
  30. package/support/tests/__snapshots__/refetch.view.test.ts.snap +23 -0
  31. package/support/tests/__snapshots__/simple.view.test.ts.snap +23 -0
  32. package/support/tests/provider-fetch.test.ts +215 -0
  33. package/support/tests/refetch.view.test.ts +56 -0
  34. package/support/tests/simple.view.test.ts +43 -0
  35. 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": 37.503224000000046
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": 57.958483
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": 99.9778339999998
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "../../packages/provider-fetch": "../../packages/provider-fetch:mswbYMnQ4eUa/jTGfZ0ZyQVSsYEX0r/6hq/NA32kH5A=:"
3
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
3
+ "rigPackageName": "@excom/heft-rig",
4
+ "rigProfile": "default"
5
+ }
package/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { ProviderFetch } from "./provider-fetch";
2
+
3
+ ProviderFetch.define();
4
+
5
+ export { ProviderFetch };
6
+
7
+ type T_HTMLProviderFetchElement = typeof ProviderFetch.CustomElement;
8
+ declare global {
9
+ interface HTMLProviderFetchElement extends T_HTMLProviderFetchElement {}
10
+ interface Window {
11
+ HTMLProviderFetchElement: HTMLProviderFetchElement;
12
+ }
13
+ interface HTMLElementTagNameMap {
14
+ "provider-fetch": HTMLProviderFetchElement;
15
+ }
16
+ }
17
+ export type { HTMLProviderFetchElement };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@excom/provider-fetch",
3
+ "version": "0.1.0",
4
+ "description": "<provider-fetch> custom element",
5
+ "license": "MIT",
6
+ "engines": {
7
+ "node": ">=24.13.0"
8
+ },
9
+ "type": "module",
10
+ "dependencies": {
11
+ "@excom/fetchable-element": "^0.1.0",
12
+ "@excom/neutron": "^0.1.0"
13
+ },
14
+ "peerDependencies": {},
15
+ "devDependencies": {
16
+ "@excom/heft-rig": "^0.1.0",
17
+ "@excom/kit-logger": "^0.1.0"
18
+ },
19
+ "repository": {
20
+ "url": "excom-dev/nucleus",
21
+ "directory": "packages/provider-fetch"
22
+ },
23
+ "homepage": "https://github.com/excom-dev/nucleus/tree/main/packages/provider-fetch/support/docs/README.md",
24
+ "bugs": "https://github.com/excom-dev/nucleus/issues",
25
+ "keywords": [
26
+ "provider-fetch",
27
+ "neutron",
28
+ "custom-elements"
29
+ ],
30
+ "excom": {
31
+ "packageType": "kit-element"
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,59 @@
1
+ import { FetchableElement } from "@excom/fetchable-element";
2
+ import { Neutron, TEvent } from "@excom/neutron";
3
+
4
+ export type FetchArgs = [url: string, requestInit: RequestInit];
5
+
6
+ export type ProviderFetchSubmitEvent = TEvent & {
7
+ type: "provider-fetch-submit";
8
+ detail: FetchArgs;
9
+ };
10
+
11
+ /**
12
+ * Zero-JS `fetch()` — composes `FetchableElement` and auto-fetches
13
+ * whenever `api-url` is set (or changes) while not `is-paused`. Re-fetch
14
+ * on demand with the `--fetch` command: a `<button command="--fetch"
15
+ * commandfor="…">`, or `<event-handler command-name="--fetch"
16
+ * target-ref="…">` to relay any event.
17
+ *
18
+ * All request-building attributes (`api-method`, headers, `form-ref`,
19
+ * …) and lifecycle state (`is-loading` / `is-success` / `is-error` /
20
+ * `provision`) are inherited from `FetchableElement` — see that package's
21
+ * docs for the full attribute list.
22
+ *
23
+ * @fires provider-fetch-submit - Internal — dispatched whenever a fetch
24
+ * is about to run (auto-fetch or `--fetch`). Built by
25
+ * `getFetchArgs()`. Cancelable; default action calls `doFetch()`.
26
+ * @type ProviderFetchSubmitEvent
27
+ * @command --fetch - Re-runs the request with the current attributes,
28
+ * even while `is-paused`.
29
+ * @default-action provider-fetch-submit - Calls `doFetch(url,
30
+ * requestInit)` with the event's detail.
31
+ */
32
+ export const ProviderFetch = Neutron.compose([
33
+ FetchableElement,
34
+ Neutron({
35
+ tag: "provider-fetch",
36
+ props: {
37
+ /**
38
+ * @option
39
+ * Pause auto-fetch. While set, `api-url` changes are ignored;
40
+ * the `--fetch` command still works.
41
+ */
42
+ isPaused: Boolean,
43
+ },
44
+ }),
45
+ ])
46
+ .onPropChanged(
47
+ ["apiUrl", "isPaused"],
48
+ ({ apiUrl, isPaused, getFetchArgs }) =>
49
+ apiUrl &&
50
+ !isPaused && {
51
+ emit: ["provider-fetch-submit", { detail: [getFetchArgs()] }],
52
+ }
53
+ )
54
+ .onCommand("--fetch", ({ getFetchArgs }) => ({
55
+ emit: ["provider-fetch-submit", { detail: [getFetchArgs()] }],
56
+ }))
57
+ .onEventDefault("provider-fetch-submit", (_, { detail }) => ({
58
+ doFetch: [detail[0], detail[1]],
59
+ }));
@@ -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,91 @@
1
+ {
2
+ "schemaVersion": "1.0.0",
3
+ "modules": [
4
+ {
5
+ "kind": "javascript-module",
6
+ "path": "provider-fetch.ts",
7
+ "declarations": [
8
+ {
9
+ "kind": "class",
10
+ "name": "ProviderFetch",
11
+ "customElement": true,
12
+ "tagName": "provider-fetch",
13
+ "mixins": [
14
+ {
15
+ "name": "FetchableElement",
16
+ "package": "@excom/fetchable-element"
17
+ }
18
+ ],
19
+ "description": "Zero-JS `fetch()` — composes `FetchableElement` and auto-fetches whenever `api-url` is set (or changes) while not `is-paused`. Re-fetch on demand with the `--fetch` command: a `<button command=\"--fetch\" commandfor=\"…\">`, or `<event-handler command-name=\"--fetch\" target-ref=\"…\">` to relay any event. All request-building attributes (`api-method`, headers, `form-ref`, …) and lifecycle state (`is-loading` / `is-success` / `is-error` / `provision`) are inherited from `FetchableElement` — see that package's docs for the full attribute list.",
20
+ "attributes": [
21
+ {
22
+ "name": "is-paused",
23
+ "type": {
24
+ "text": "boolean"
25
+ },
26
+ "description": "Pause auto-fetch. While set, `api-url` changes are ignored; the `--fetch` command still works.",
27
+ "fieldName": "isPaused"
28
+ }
29
+ ],
30
+ "members": [
31
+ {
32
+ "kind": "field",
33
+ "name": "isPaused",
34
+ "type": {
35
+ "text": "boolean"
36
+ },
37
+ "privacy": "public",
38
+ "readonly": false,
39
+ "description": "Pause auto-fetch. While set, `api-url` changes are ignored; the `--fetch` command still works.",
40
+ "_neutron": {
41
+ "surface": "option"
42
+ }
43
+ }
44
+ ],
45
+ "events": [
46
+ {
47
+ "name": "provider-fetch-submit",
48
+ "description": "Internal — dispatched whenever a fetch is about to run (auto-fetch or `--fetch`). Built by `getFetchArgs()`. Cancelable; default action calls `doFetch()`.",
49
+ "type": {
50
+ "text": "ProviderFetchSubmitEvent",
51
+ "expanded": "CustomEvent & { type: \"provider-fetch-submit\"; detail: [url: string, requestInit: RequestInit]; bubbles: true; cancelable: true; composed: true }"
52
+ }
53
+ }
54
+ ],
55
+ "_neutron": {
56
+ "commands": [
57
+ {
58
+ "name": "--fetch",
59
+ "description": "Re-runs the request with the current attributes, even while `is-paused`."
60
+ }
61
+ ],
62
+ "defaultActions": [
63
+ {
64
+ "name": "provider-fetch-submit",
65
+ "description": "Calls `doFetch(url, requestInit)` with the event's detail."
66
+ }
67
+ ]
68
+ }
69
+ }
70
+ ],
71
+ "exports": [
72
+ {
73
+ "kind": "js",
74
+ "name": "ProviderFetch",
75
+ "declaration": {
76
+ "name": "ProviderFetch",
77
+ "module": "provider-fetch.ts"
78
+ }
79
+ },
80
+ {
81
+ "kind": "custom-element-definition",
82
+ "name": "provider-fetch",
83
+ "declaration": {
84
+ "name": "ProviderFetch",
85
+ "module": "provider-fetch.ts"
86
+ }
87
+ }
88
+ ]
89
+ }
90
+ ]
91
+ }
@@ -0,0 +1,35 @@
1
+ <section>
2
+ <provider-fetch id="demo-refetch-provider" api-url="/api/todos/1" class="tag-article grid">
3
+ <ul></ul>
4
+ <ul></ul>
5
+ <template id="demo-refetch-item">
6
+ <li class="tag-code"></li>
7
+ </template>
8
+ </provider-fetch>
9
+ <button type="button" command="--fetch" commandfor="demo-refetch-provider">Refetch</button>
10
+ <style>
11
+ #demo-provider-fetch-refetch { max-width: none; }
12
+ #demo-provider-fetch-refetch provider-fetch {
13
+ &[is-loading] {
14
+ opacity: 0.5;
15
+ border: 1px dashed yellow;
16
+ }
17
+ &[is-success] { border: 1px solid green; }
18
+ &[is-error] {
19
+ border: 1px solid red;
20
+ &::before { content: "An error occurred."; }
21
+ }
22
+ ul:first-of-type::before { content: "Todo data:"; }
23
+ ul:last-of-type::before { content: "Response data:"; }
24
+ li { display: block; }
25
+ }
26
+ </style>
27
+ <quark-sheet>
28
+ provider-fetch[is-success] {
29
+ $res: prop("provision");
30
+ li { content: "#{index}: #{item}"; }
31
+ ul:first-of-type { content: iterate($res.body, "#demo-refetch-item"); }
32
+ ul:last-of-type { content: iterate($res, "#demo-refetch-item"); }
33
+ }
34
+ </quark-sheet>
35
+ </section>
@@ -0,0 +1,13 @@
1
+ <section>
2
+ <provider-fetch api-url="/api/todos/1">
3
+ <h4>Fetching todo...</h4>
4
+ <span></span>
5
+ </provider-fetch>
6
+ <quark-sheet>
7
+ provider-fetch[is-success] {
8
+ $todo: prop("provision").body;
9
+ h4 { content: "Todo ##{$todo.id}: #{$todo.title}"; }
10
+ span { content: "Completed: #{$todo.completed}"; }
11
+ }
12
+ </quark-sheet>
13
+ </section>
@@ -0,0 +1,172 @@
1
+ # provider-fetch
2
+
3
+ Fetch data with HTML. Pair it with Quark to render the data. Zero app JS.
4
+
5
+
6
+ ```html
7
+ <section>
8
+ <provider-fetch api-url="/api/todos/1">
9
+ <h4>Fetching todo...</h4>
10
+ <span></span>
11
+ </provider-fetch>
12
+ <quark-sheet>
13
+ provider-fetch[is-success] {
14
+ $todo: prop("provision").body;
15
+ h4 { content: "Todo ##{$todo.id}: #{$todo.title}"; }
16
+ span { content: "Completed: #{$todo.completed}"; }
17
+ }
18
+ </quark-sheet>
19
+ </section>
20
+ ```
21
+
22
+
23
+ ## Features
24
+
25
+ - **Provides data** Use Quark to render that data
26
+ - **Auto-fetch** Fetches whenever `api-url` is set or changes
27
+ - **Re-fetch on demand** The `--fetch` command (`<button command="--fetch" commandfor="…">`) forces a re-fetch
28
+ - **Pausable** `is-paused` holds off auto-fetch without removing state
29
+ - **Highly configurable** Headers, method, `form-ref`,
30
+ credentials, redirect, etc
31
+
32
+ ## Installation
33
+
34
+
35
+ `@excom/provider-fetch` v0.1.0
36
+
37
+ ```bash
38
+ pnpm add @excom/provider-fetch
39
+ ```
40
+
41
+ ```bash
42
+ npm install @excom/provider-fetch
43
+ ```
44
+
45
+ ```bash
46
+ yarn add @excom/provider-fetch
47
+ ```
48
+
49
+ ### Import
50
+
51
+ ```ts
52
+ import "@excom/provider-fetch";
53
+ ```
54
+
55
+
56
+
57
+ ## Usage
58
+
59
+ ```html
60
+ <provider-fetch api-url="/api/todos"></provider-fetch>
61
+ ```
62
+
63
+ Hook the lifecycle state with CSS:
64
+
65
+ ```css
66
+ provider-fetch[is-loading] { /* show loading spinner */ }
67
+ provider-fetch[is-error]::before { content: "An error occurred." }
68
+ ```
69
+
70
+ Or Quark:
71
+
72
+ ```quark
73
+ provider-fetch[is-success] {
74
+ $todo: prop("provision").body;
75
+ span { content: $todo.title; }
76
+ }
77
+ ```
78
+
79
+ ### API Reference
80
+
81
+
82
+ #### Attributes
83
+
84
+ | Name | Surface | Type | Default | Values | Description | Inherited from |
85
+ | --- | --- | --- | --- | --- | --- | --- |
86
+ | `is-paused` | option | `boolean` | | | Pause auto-fetch. While set, `api-url` changes are ignored; the `--fetch` command still works. | |
87
+ | `form-ref` | option | `string` | | `<CSS Selector>` | CSS selector for a `<form>` to source the request from — its `action` (URL), `method`, `enctype` (Content-Type), and field values (as the JSON payload) all take priority over the matching attributes below. Omit to build the request entirely from attributes / custom `doFetch()` args. | `@excom/fetchable-element` |
88
+ | `has-body` | option | `boolean` | | | Force a request body even for methods that don't imply one (`GET` / `HEAD`). Already implied for `POST` / `PUT` / `PATCH`. | `@excom/fetchable-element` |
89
+ | `api-url` | option | `string` | `""` | | Endpoint URL. When the request has no body, the JSON payload (from `form-ref` or custom `doFetch()` args) is merged in as query params instead. | `@excom/fetchable-element` |
90
+ | `api-method` | option | `string` | `"GET"` | | HTTP method. Always uppercased before the request is sent. | `@excom/fetchable-element` |
91
+ | `header-accept` | option | `string` | `"application/json"` | | `Accept` request header. | `@excom/fetchable-element` |
92
+ | `header-content-type` | option | `string` | `"application/json"` | | `Content-Type` request header. Dropped entirely when the request has no body. | `@excom/fetchable-element` |
93
+ | `header-cache-control` | option | `string` | | | `Cache-Control` request header. Unset by default (browser default caching applies). | `@excom/fetchable-element` |
94
+ | `fetch-redirect` | option | `string` | | `"follow"` \| `"error"` \| `"manual"` | `RequestInit.redirect` mode. Unset defers to the browser default (`follow`). | `@excom/fetchable-element` |
95
+ | `fetch-credentials` | option | `string` | `"include"` | `"omit"` \| `"same-origin"` \| `"include"` | `RequestInit.credentials` mode. | `@excom/fetchable-element` |
96
+ | `is-loading` | state | `boolean` | | | A request is currently in flight. | `@excom/fetchable-element` |
97
+ | `is-success` | state | `boolean` | | | The most recent request resolved successfully. Mutually exclusive with `is-error`. | `@excom/fetchable-element` |
98
+ | `is-error` | state | `boolean` | | | The most recent request failed (non-2xx status, network error, or a thrown error other than `AbortError`). Fires with the `error` event. | `@excom/fetchable-element` |
99
+
100
+ #### Provision
101
+
102
+ | Name | Type | Description | Inherited from |
103
+ | --- | --- | --- | --- |
104
+ | `provision` | `FetchResponse` (`{ bodyUsed: boolean; headers: [string, string][]; ok: boolean; redirected: boolean; status: number; statusText: string; type: ResponseType; url: string; body: unknown; }`) | Response payload on success, or error payload on failure. Success shape: `{ status, statusText, ok, headers, url, redirected, bodyUsed, type, body }`. Failure shape is either that same response shape (server responded with an error status) or `{ message, stack }` (request never completed). Not reflected as an attribute. | `@excom/fetchable-element` |
105
+
106
+ #### Fires
107
+
108
+ | Name | Type | Description | Inherited from |
109
+ | --- | --- | --- | --- |
110
+ | `provider-fetch-submit` | `ProviderFetchSubmitEvent` (`CustomEvent & { type: "provider-fetch-submit"; detail: [url: string, requestInit: RequestInit]; bubbles: true; cancelable: true; composed: true }`) | Internal — dispatched whenever a fetch is about to run (auto-fetch or `--fetch`). Built by `getFetchArgs()`. Cancelable; default action calls `doFetch()`. | |
111
+ | `provider-fetch-loading` | `FetchableLoadingEvent` (`CustomEvent & { type: "{tag}-loading"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Dispatched immediately before the request is sent. | `@excom/fetchable-element` |
112
+ | `provider-fetch-success` | `FetchableSuccessEvent` (`CustomEvent & { type: "{tag}-success"; detail: { bodyUsed: boolean; headers: [string, string][]; ok: boolean; redirected: boolean; status: number; statusText: string; type: ResponseType; url: string; body: unknown; }; bubbles: true; cancelable: true; composed: true }`) | Dispatched when the request resolves successfully. `event.detail` is the parsed response (see `provision`). | `@excom/fetchable-element` |
113
+ | `provider-fetch-error` | `FetchableErrorEvent` (`CustomEvent & { type: "{tag}-error"; detail: { bodyUsed: boolean; headers: [string, string][]; ok: boolean; redirected: boolean; status: number; statusText: string; type: ResponseType; url: string; body: unknown; } \| { message: string; stack?: string }; bubbles: true; cancelable: true; composed: true }`) | Dispatched when the request fails — non-2xx status, network error, or a thrown error. `event.detail` is the error payload (see `provision`). Not dispatched for aborted requests. | `@excom/fetchable-element` |
114
+
115
+ #### Commands
116
+
117
+ | Command | Action |
118
+ | --- | --- |
119
+ | `--fetch` | Re-runs the request with the current attributes, even while `is-paused`. |
120
+
121
+ #### Default actions
122
+
123
+ | Event | Default behavior (unless preventDefault() is called) |
124
+ | --- | --- |
125
+ | `provider-fetch-submit` | Calls `doFetch(url, requestInit)` with the event's detail. |
126
+
127
+
128
+
129
+ ### Examples
130
+
131
+ #### Comprehensive
132
+
133
+ This example shows loading state, error state, rendering, and refetching.
134
+
135
+
136
+ ```html
137
+ <section>
138
+ <provider-fetch id="demo-refetch-provider" api-url="/api/todos/1" class="tag-article grid">
139
+ <ul></ul>
140
+ <ul></ul>
141
+ <template id="demo-refetch-item">
142
+ <li class="tag-code"></li>
143
+ </template>
144
+ </provider-fetch>
145
+ <button type="button" command="--fetch" commandfor="demo-refetch-provider">Refetch</button>
146
+ <style>
147
+ #demo-provider-fetch-refetch { max-width: none; }
148
+ #demo-provider-fetch-refetch provider-fetch {
149
+ &[is-loading] {
150
+ opacity: 0.5;
151
+ border: 1px dashed yellow;
152
+ }
153
+ &[is-success] { border: 1px solid green; }
154
+ &[is-error] {
155
+ border: 1px solid red;
156
+ &::before { content: "An error occurred."; }
157
+ }
158
+ ul:first-of-type::before { content: "Todo data:"; }
159
+ ul:last-of-type::before { content: "Response data:"; }
160
+ li { display: block; }
161
+ }
162
+ </style>
163
+ <quark-sheet>
164
+ provider-fetch[is-success] {
165
+ $res: prop("provision");
166
+ li { content: "#{index}: #{item}"; }
167
+ ul:first-of-type { content: iterate($res.body, "#demo-refetch-item"); }
168
+ ul:last-of-type { content: iterate($res, "#demo-refetch-item"); }
169
+ }
170
+ </quark-sheet>
171
+ </section>
172
+ ```
@@ -0,0 +1,52 @@
1
+ # provider-fetch
2
+
3
+ Fetch data with HTML. Pair it with Quark to render the data. Zero app JS.
4
+
5
+ <include-content data-demo="simple"></include-content>
6
+
7
+ ## Features
8
+
9
+ - **Provides data** Use Quark to render that data
10
+ - **Auto-fetch** Fetches whenever `api-url` is set or changes
11
+ - **Re-fetch on demand** The `--fetch` command (`<button command="--fetch" commandfor="…">`) forces a re-fetch
12
+ - **Pausable** `is-paused` holds off auto-fetch without removing state
13
+ - **Highly configurable** Headers, method, `form-ref`,
14
+ credentials, redirect, etc
15
+
16
+ ## Installation
17
+
18
+ <include-content is-active template-ref="/views/install-section/install-section.html"></include-content>
19
+
20
+ ## Usage
21
+
22
+ ```html
23
+ <provider-fetch api-url="/api/todos"></provider-fetch>
24
+ ```
25
+
26
+ Hook the lifecycle state with CSS:
27
+
28
+ ```css
29
+ provider-fetch[is-loading] { /* show loading spinner */ }
30
+ provider-fetch[is-error]::before { content: "An error occurred." }
31
+ ```
32
+
33
+ Or Quark:
34
+
35
+ ```quark
36
+ provider-fetch[is-success] {
37
+ $todo: prop("provision").body;
38
+ span { content: $todo.title; }
39
+ }
40
+ ```
41
+
42
+ ### API Reference
43
+
44
+ <include-content is-active template-ref="/views/api-reference/api-reference.html"></include-content>
45
+
46
+ ### Examples
47
+
48
+ #### Comprehensive
49
+
50
+ This example shows loading state, error state, rendering, and refetching.
51
+
52
+ <include-content data-demo="refetch"></include-content>
@@ -0,0 +1,214 @@
1
+ {
2
+ "shortName": "provider-fetch",
3
+ "package": {
4
+ "name": "@excom/provider-fetch",
5
+ "version": "0.1.0",
6
+ "description": "<provider-fetch> custom element",
7
+ "peerDependencies": {},
8
+ "excom": {
9
+ "packageType": "kit-element"
10
+ }
11
+ },
12
+ "demos": {
13
+ "refetch": "<section>\n <provider-fetch id=\"demo-refetch-provider\" api-url=\"/api/todos/1\" class=\"tag-article grid\">\n <ul></ul>\n <ul></ul>\n <template id=\"demo-refetch-item\">\n <li class=\"tag-code\"></li>\n </template>\n </provider-fetch>\n <button type=\"button\" command=\"--fetch\" commandfor=\"demo-refetch-provider\">Refetch</button>\n <style>\n #demo-provider-fetch-refetch { max-width: none; }\n #demo-provider-fetch-refetch provider-fetch {\n &[is-loading] {\n opacity: 0.5;\n border: 1px dashed yellow;\n }\n &[is-success] { border: 1px solid green; }\n &[is-error] {\n border: 1px solid red;\n &::before { content: \"An error occurred.\"; }\n }\n ul:first-of-type::before { content: \"Todo data:\"; }\n ul:last-of-type::before { content: \"Response data:\"; }\n li { display: block; }\n }\n </style>\n <quark-sheet>\n provider-fetch[is-success] {\n $res: prop(\"provision\");\n li { content: \"#{index}: #{item}\"; }\n ul:first-of-type { content: iterate($res.body, \"#demo-refetch-item\"); }\n ul:last-of-type { content: iterate($res, \"#demo-refetch-item\"); }\n }\n </quark-sheet>\n</section>\n",
14
+ "simple": "<section>\n <provider-fetch api-url=\"/api/todos/1\">\n <h4>Fetching todo...</h4>\n <span></span>\n </provider-fetch>\n <quark-sheet>\n provider-fetch[is-success] {\n $todo: prop(\"provision\").body;\n h4 { content: \"Todo ##{$todo.id}: #{$todo.title}\"; }\n span { content: \"Completed: #{$todo.completed}\"; }\n }\n </quark-sheet>\n</section>\n"
15
+ },
16
+ "readme": "<h1 id=\"md-provider-fetch\">provider-fetch</h1>\n<p>Fetch data with HTML. Pair it with Quark to render the data. Zero app JS.</p>\n<p><include-content data-demo=\"simple\"></include-content></p>\n<h2 id=\"md-features\">Features</h2>\n<ul>\n<li><strong>Provides data</strong> Use Quark to render that data</li>\n<li><strong>Auto-fetch</strong> Fetches whenever <code>api-url</code> is set or changes</li>\n<li><strong>Re-fetch on demand</strong> The <code>--fetch</code> command (<code>&lt;button command=&quot;--fetch&quot; commandfor=&quot;…&quot;&gt;</code>) forces a re-fetch</li>\n<li><strong>Pausable</strong> <code>is-paused</code> holds off auto-fetch without removing state</li>\n<li><strong>Highly configurable</strong> Headers, method, <code>form-ref</code>,\ncredentials, redirect, etc</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<include-content data-language=\"html\"><template>&lt;provider-fetch api-url=\"/api/todos\"&gt;&lt;/provider-fetch&gt;</template></include-content>\n<p>Hook the lifecycle state with CSS:</p>\n<include-content data-language=\"css\"><template>provider-fetch[is-loading] { /* show loading spinner */ }\nprovider-fetch[is-error]::before { content: \"An error occurred.\" }</template></include-content>\n<p>Or Quark:</p>\n<include-content data-language=\"quark\"><template>provider-fetch[is-success] {\n $todo: prop(\"provision\").body;\n span { content: $todo.title; }\n}</template></include-content>\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<h3 id=\"md-examples\">Examples</h3>\n<h4 id=\"md-comprehensive\">Comprehensive</h4>\n<p>This example shows loading state, error state, rendering, and refetching.</p>\n<p><include-content data-demo=\"refetch\"></include-content></p>\n",
17
+ "docs": {
18
+ "readme": "<h1 id=\"md-provider-fetch\">provider-fetch</h1>\n<p>Fetch data with HTML. Pair it with Quark to render the data. Zero app JS.</p>\n<p><include-content data-demo=\"simple\"></include-content></p>\n<h2 id=\"md-features\">Features</h2>\n<ul>\n<li><strong>Provides data</strong> Use Quark to render that data</li>\n<li><strong>Auto-fetch</strong> Fetches whenever <code>api-url</code> is set or changes</li>\n<li><strong>Re-fetch on demand</strong> The <code>--fetch</code> command (<code>&lt;button command=&quot;--fetch&quot; commandfor=&quot;…&quot;&gt;</code>) forces a re-fetch</li>\n<li><strong>Pausable</strong> <code>is-paused</code> holds off auto-fetch without removing state</li>\n<li><strong>Highly configurable</strong> Headers, method, <code>form-ref</code>,\ncredentials, redirect, etc</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<include-content data-language=\"html\"><template>&lt;provider-fetch api-url=\"/api/todos\"&gt;&lt;/provider-fetch&gt;</template></include-content>\n<p>Hook the lifecycle state with CSS:</p>\n<include-content data-language=\"css\"><template>provider-fetch[is-loading] { /* show loading spinner */ }\nprovider-fetch[is-error]::before { content: \"An error occurred.\" }</template></include-content>\n<p>Or Quark:</p>\n<include-content data-language=\"quark\"><template>provider-fetch[is-success] {\n $todo: prop(\"provision\").body;\n span { content: $todo.title; }\n}</template></include-content>\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<h3 id=\"md-examples\">Examples</h3>\n<h4 id=\"md-comprehensive\">Comprehensive</h4>\n<p>This example shows loading state, error state, rendering, and refetching.</p>\n<p><include-content data-demo=\"refetch\"></include-content></p>\n"
19
+ },
20
+ "installation": {
21
+ "name": "@excom/provider-fetch",
22
+ "shortName": "provider-fetch",
23
+ "version": "0.1.0",
24
+ "description": "<provider-fetch> custom element",
25
+ "packageType": "kit-element",
26
+ "cdn": "<script src=\"https://unpkg.com/@excom/kit-utils/dist/index.umd.min.js\"></script>\n<script src=\"https://unpkg.com/@excom/neutron/dist/index.umd.min.js\"></script>\n<script src=\"https://unpkg.com/@excom/provider-fetch@0.1.0/dist/index.umd.min.js\"></script>",
27
+ "install": {
28
+ "npm": "npm install @excom/provider-fetch"
29
+ },
30
+ "imports": {
31
+ "js": "import \"@excom/provider-fetch\";",
32
+ "html": "<!-- import path to `node_modules` will depend on your build setup -->\n<script type=\"module\" src=\"/node_modules/@excom/provider-fetch\"></script>\n<link rel=\"stylesheet\" href=\"/node_modules/@excom/provider-fetch\">"
33
+ },
34
+ "peerDependencies": []
35
+ },
36
+ "elementApis": [
37
+ {
38
+ "tag": "provider-fetch",
39
+ "kind": "class",
40
+ "attributes": [
41
+ {
42
+ "name": "api-method",
43
+ "type": "string",
44
+ "description": "HTTP method. Always uppercased before the request is sent.",
45
+ "fieldName": "apiMethod",
46
+ "surface": "option",
47
+ "default": "\"GET\"",
48
+ "inheritedFrom": "@excom/fetchable-element"
49
+ },
50
+ {
51
+ "name": "api-url",
52
+ "type": "string",
53
+ "description": "Endpoint URL. When the request has no body, the JSON payload (from <code>form-ref</code> or custom <code>doFetch()</code> args) is merged in as query params instead.",
54
+ "fieldName": "apiUrl",
55
+ "surface": "option",
56
+ "default": "\"\"",
57
+ "inheritedFrom": "@excom/fetchable-element"
58
+ },
59
+ {
60
+ "name": "fetch-credentials",
61
+ "type": "string",
62
+ "description": "<code>RequestInit.credentials</code> mode.",
63
+ "fieldName": "fetchCredentials",
64
+ "surface": "option",
65
+ "default": "\"include\"",
66
+ "values": "\"omit\" | \"same-origin\" | \"include\"",
67
+ "inheritedFrom": "@excom/fetchable-element"
68
+ },
69
+ {
70
+ "name": "fetch-redirect",
71
+ "type": "string",
72
+ "description": "<code>RequestInit.redirect</code> mode. Unset defers to the browser default (<code>follow</code>).",
73
+ "fieldName": "fetchRedirect",
74
+ "surface": "option",
75
+ "values": "\"follow\" | \"error\" | \"manual\"",
76
+ "inheritedFrom": "@excom/fetchable-element"
77
+ },
78
+ {
79
+ "name": "form-ref",
80
+ "type": "string",
81
+ "description": "CSS selector for a <code>&lt;form&gt;</code> to source the request from — its <code>action</code> (URL), <code>method</code>, <code>enctype</code> (Content-Type), and field values (as the JSON payload) all take priority over the matching attributes below. Omit to build the request entirely from attributes / custom <code>doFetch()</code> args.",
82
+ "fieldName": "formRef",
83
+ "surface": "option",
84
+ "values": "<CSS Selector>",
85
+ "inheritedFrom": "@excom/fetchable-element"
86
+ },
87
+ {
88
+ "name": "has-body",
89
+ "type": "boolean",
90
+ "description": "Force a request body even for methods that don&#39;t imply one (<code>GET</code> / <code>HEAD</code>). Already implied for <code>POST</code> / <code>PUT</code> / <code>PATCH</code>.",
91
+ "fieldName": "hasBody",
92
+ "surface": "option",
93
+ "inheritedFrom": "@excom/fetchable-element"
94
+ },
95
+ {
96
+ "name": "header-accept",
97
+ "type": "string",
98
+ "description": "<code>Accept</code> request header.",
99
+ "fieldName": "headerAccept",
100
+ "surface": "option",
101
+ "default": "\"application/json\"",
102
+ "inheritedFrom": "@excom/fetchable-element"
103
+ },
104
+ {
105
+ "name": "header-cache-control",
106
+ "type": "string",
107
+ "description": "<code>Cache-Control</code> request header. Unset by default (browser default caching applies).",
108
+ "fieldName": "headerCacheControl",
109
+ "surface": "option",
110
+ "inheritedFrom": "@excom/fetchable-element"
111
+ },
112
+ {
113
+ "name": "header-content-type",
114
+ "type": "string",
115
+ "description": "<code>Content-Type</code> request header. Dropped entirely when the request has no body.",
116
+ "fieldName": "headerContentType",
117
+ "surface": "option",
118
+ "default": "\"application/json\"",
119
+ "inheritedFrom": "@excom/fetchable-element"
120
+ },
121
+ {
122
+ "name": "is-paused",
123
+ "type": "boolean",
124
+ "description": "Pause auto-fetch. While set, <code>api-url</code> changes are ignored; the <code>--fetch</code> command still works.",
125
+ "fieldName": "isPaused",
126
+ "surface": "option"
127
+ },
128
+ {
129
+ "name": "is-error",
130
+ "type": "boolean",
131
+ "description": "The most recent request failed (non-2xx status, network error, or a thrown error other than <code>AbortError</code>). Fires with the <code>error</code> event.",
132
+ "fieldName": "isError",
133
+ "surface": "state",
134
+ "inheritedFrom": "@excom/fetchable-element"
135
+ },
136
+ {
137
+ "name": "is-loading",
138
+ "type": "boolean",
139
+ "description": "A request is currently in flight.",
140
+ "fieldName": "isLoading",
141
+ "surface": "state",
142
+ "inheritedFrom": "@excom/fetchable-element"
143
+ },
144
+ {
145
+ "name": "is-success",
146
+ "type": "boolean",
147
+ "description": "The most recent request resolved successfully. Mutually exclusive with <code>is-error</code>.",
148
+ "fieldName": "isSuccess",
149
+ "surface": "state",
150
+ "inheritedFrom": "@excom/fetchable-element"
151
+ }
152
+ ],
153
+ "events": [
154
+ {
155
+ "name": "provider-fetch-error",
156
+ "description": "Dispatched when the request fails — non-2xx status, network error, or a thrown error. <code>event.detail</code> is the error payload (see <code>provision</code>). Not dispatched for aborted requests.",
157
+ "type": "FetchableErrorEvent",
158
+ "typeExpanded": "CustomEvent & { type: \"{tag}-error\"; detail: { bodyUsed: boolean; headers: [string, string][]; ok: boolean; redirected: boolean; status: number; statusText: string; type: ResponseType; url: string; body: unknown; } | { message: string; stack?: string }; bubbles: true; cancelable: true; composed: true }",
159
+ "inheritedFrom": "@excom/fetchable-element"
160
+ },
161
+ {
162
+ "name": "provider-fetch-loading",
163
+ "description": "Dispatched immediately before the request is sent.",
164
+ "type": "FetchableLoadingEvent",
165
+ "typeExpanded": "CustomEvent & { type: \"{tag}-loading\"; detail: void; bubbles: true; cancelable: true; composed: true }",
166
+ "inheritedFrom": "@excom/fetchable-element"
167
+ },
168
+ {
169
+ "name": "provider-fetch-submit",
170
+ "description": "Internal — dispatched whenever a fetch is about to run (auto-fetch or <code>--fetch</code>). Built by <code>getFetchArgs()</code>. Cancelable; default action calls <code>doFetch()</code>.",
171
+ "type": "ProviderFetchSubmitEvent",
172
+ "typeExpanded": "CustomEvent & { type: \"provider-fetch-submit\"; detail: [url: string, requestInit: RequestInit]; bubbles: true; cancelable: true; composed: true }",
173
+ "defaultAction": "Calls <code>doFetch(url, requestInit)</code> with the event&#39;s detail."
174
+ },
175
+ {
176
+ "name": "provider-fetch-success",
177
+ "description": "Dispatched when the request resolves successfully. <code>event.detail</code> is the parsed response (see <code>provision</code>).",
178
+ "type": "FetchableSuccessEvent",
179
+ "typeExpanded": "CustomEvent & { type: \"{tag}-success\"; detail: { bodyUsed: boolean; headers: [string, string][]; ok: boolean; redirected: boolean; status: number; statusText: string; type: ResponseType; url: string; body: unknown; }; bubbles: true; cancelable: true; composed: true }",
180
+ "inheritedFrom": "@excom/fetchable-element"
181
+ }
182
+ ],
183
+ "slots": [],
184
+ "cssProperties": [],
185
+ "cssClasses": [],
186
+ "cssAliases": [],
187
+ "listens": [],
188
+ "commands": [
189
+ {
190
+ "name": "--fetch",
191
+ "description": "Re-runs the request with the current attributes, even while <code>is-paused</code>."
192
+ }
193
+ ],
194
+ "defaultActions": [
195
+ {
196
+ "name": "provider-fetch-submit",
197
+ "description": "Calls <code>doFetch(url, requestInit)</code> with the event&#39;s detail."
198
+ }
199
+ ],
200
+ "expectedChildren": [],
201
+ "provisions": [
202
+ {
203
+ "name": "provision",
204
+ "type": "FetchResponse",
205
+ "typeExpanded": "{ bodyUsed: boolean; headers: [string, string][]; ok: boolean; redirected: boolean; status: number; statusText: string; type: ResponseType; url: string; body: unknown; }",
206
+ "description": "Response payload on success, or error payload on failure. Success shape: <code>{ status, statusText, ok, headers, url, redirected, bodyUsed, type, body }</code>. Failure shape is either that same response shape (server responded with an error status) or <code>{ message, stack }</code> (request never completed). Not reflected as an attribute.",
207
+ "fieldName": "provision",
208
+ "inheritedFrom": "@excom/fetchable-element"
209
+ }
210
+ ]
211
+ }
212
+ ],
213
+ "exportedFiles": {}
214
+ }
@@ -0,0 +1,23 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`refetch view > loads then refetches > complexity 1`] = `
4
+ {
5
+ "attributeRuns": 11,
6
+ "closest": 22,
7
+ "getVar": 0,
8
+ "importNode": 0,
9
+ "listenerRuns": 0,
10
+ "matches": 5,
11
+ "parentElement": 12,
12
+ "quarkRuns": 2,
13
+ "queryScopeCost": 42,
14
+ "querySelectorAll": 3,
15
+ "removeAttribute": 2,
16
+ "ruleRuns": 5,
17
+ "schedulePaint": 11,
18
+ "setAttribute": 2,
19
+ "setVar": 1,
20
+ "textContent": 0,
21
+ "variableRuns": 1,
22
+ }
23
+ `;
@@ -0,0 +1,23 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`simple view > renders the fetched todo > complexity 1`] = `
4
+ {
5
+ "attributeRuns": 0,
6
+ "closest": 0,
7
+ "getVar": 0,
8
+ "importNode": 0,
9
+ "listenerRuns": 0,
10
+ "matches": 0,
11
+ "parentElement": 0,
12
+ "quarkRuns": 0,
13
+ "queryScopeCost": 0,
14
+ "querySelectorAll": 0,
15
+ "removeAttribute": 0,
16
+ "ruleRuns": 0,
17
+ "schedulePaint": 0,
18
+ "setAttribute": 0,
19
+ "setVar": 0,
20
+ "textContent": 0,
21
+ "variableRuns": 0,
22
+ }
23
+ `;
@@ -0,0 +1,215 @@
1
+ import { invokeCommand } from "@excom/neutron";
2
+ import {
3
+ afterEach,
4
+ describe,
5
+ expect,
6
+ fixture,
7
+ it,
8
+ spyFetch,
9
+ vi,
10
+ waitForEvent,
11
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
12
+ import { KitLogger } from "@excom/kit-logger";
13
+ import "../../index";
14
+
15
+ const fetchStubs = {
16
+ success: () => ({
17
+ status: 200,
18
+ body: JSON.stringify({ foo: "bar" }),
19
+ }),
20
+ error: () => ({
21
+ status: 500,
22
+ body: JSON.stringify({ error: "bar" }),
23
+ }),
24
+ };
25
+
26
+ describe("provider-fetch", () => {
27
+ afterEach(() => {
28
+ document.body.innerHTML = "";
29
+ vi.restoreAllMocks();
30
+ });
31
+
32
+ it("sets state attributes and fetches data", async () => {
33
+ const providerFetch = fixture<HTMLProviderFetchElement>(
34
+ `<provider-fetch></provider-fetch>`,
35
+ );
36
+ const fetchSpy = spyFetch(fetchStubs.success() as any);
37
+
38
+ providerFetch.addEventListener("provider-fetch-loading", () => {
39
+ expect(providerFetch).dom.to.equalTag(
40
+ `<provider-fetch api-url="/api/test" is-loading></provider-fetch>`,
41
+ );
42
+ expect(providerFetch.provision).toEqual(null);
43
+ expect(fetchSpy).toHaveBeenCalledWith(
44
+ "http://localhost:3000/api/test",
45
+ expect.objectContaining({
46
+ method: "GET",
47
+ credentials: "include",
48
+ headers: {
49
+ Accept: "application/json",
50
+ },
51
+ }),
52
+ );
53
+ });
54
+
55
+ providerFetch.addEventListener("provider-fetch-success", () => {
56
+ expect(providerFetch).dom.to.equalTag(
57
+ `<provider-fetch api-url="/api/test" is-success></provider-fetch>`,
58
+ );
59
+ expect(providerFetch.provision?.body).toEqual({ foo: "bar" });
60
+ });
61
+
62
+ await waitForEvent(providerFetch, "provider-fetch-success", () => {
63
+ providerFetch.setAttribute("api-url", "/api/test");
64
+ });
65
+ expect.assertions(5);
66
+ });
67
+
68
+ it("sets state attributes and fetches data with formRef", async () => {
69
+ const providerFetch = fixture<HTMLProviderFetchElement>(
70
+ `<provider-fetch form-ref="form" api-method="post">
71
+ <form>
72
+ <input type="text" name="name" value="John Doe">
73
+ <input type="email" name="email" value="j@doe.com">
74
+ <input type="number" name="age" value="50">
75
+ <input type="range" name="volume" min="0" max="100" value="20">
76
+ <input type="tel" name="phone" value="123-456-7890">
77
+ <input type="url" name="website" value="https://example.com">
78
+ </form>
79
+ </provider-fetch>`,
80
+ );
81
+ const fetchSpy = spyFetch(fetchStubs.success() as any);
82
+
83
+ providerFetch.addEventListener("provider-fetch-loading", () => {
84
+ expect(providerFetch).dom.to.equalTag(
85
+ `<provider-fetch is-loading form-ref="form" api-method="post" api-url="/api/test"></provider-fetch>`,
86
+ );
87
+ expect(providerFetch.provision).toEqual(null);
88
+ expect(fetchSpy).toHaveBeenCalledWith(
89
+ "http://localhost:3000/api/test",
90
+ expect.objectContaining({
91
+ method: "POST",
92
+ credentials: "include",
93
+ headers: {
94
+ Accept: "application/json",
95
+ "Content-Type": "application/json",
96
+ },
97
+ body: expect.toSatisfy((body: string) => {
98
+ expect(JSON.parse(body)).toEqual(
99
+ expect.objectContaining({
100
+ name: "John Doe",
101
+ email: "j@doe.com",
102
+ age: 50,
103
+ volume: 20,
104
+ phone: "123-456-7890",
105
+ website: "https://example.com",
106
+ }),
107
+ );
108
+ return true;
109
+ }),
110
+ }),
111
+ );
112
+ });
113
+
114
+ providerFetch.addEventListener("provider-fetch-success", () => {
115
+ expect(providerFetch).dom.to.equalTag(
116
+ `<provider-fetch is-success form-ref="form" api-method="post" api-url="/api/test"></provider-fetch>`,
117
+ );
118
+ expect(providerFetch.provision?.body).toEqual({ foo: "bar" });
119
+ });
120
+
121
+ await waitForEvent(providerFetch, "provider-fetch-success", () => {
122
+ providerFetch.setAttribute("api-url", "/api/test");
123
+ });
124
+ expect.assertions(6);
125
+ });
126
+
127
+ it("handles fetch error", async () => {
128
+ const providerFetch = fixture<HTMLProviderFetchElement>(
129
+ `<provider-fetch></provider-fetch>`,
130
+ );
131
+ const fetchSpy = spyFetch(fetchStubs.error as any);
132
+
133
+ KitLogger.suppress();
134
+ providerFetch.addEventListener("provider-fetch-loading", () => {
135
+ expect(providerFetch).dom.to.equalTag(
136
+ `<provider-fetch api-url="/api/test" is-loading></provider-fetch>`,
137
+ );
138
+ expect(providerFetch.provision).toEqual(null);
139
+ expect(fetchSpy).toHaveBeenCalledWith(
140
+ "http://localhost:3000/api/test",
141
+ expect.objectContaining({
142
+ method: "GET",
143
+ credentials: "include",
144
+ headers: {
145
+ Accept: "application/json",
146
+ },
147
+ }),
148
+ );
149
+ });
150
+
151
+ providerFetch.addEventListener("provider-fetch-error", () => {
152
+ KitLogger.unsuppress();
153
+ expect(providerFetch).dom.to.equalTag(
154
+ `<provider-fetch api-url="/api/test" is-error></provider-fetch>`,
155
+ );
156
+ expect(providerFetch.provision?.body).toEqual({ error: "bar" });
157
+ });
158
+
159
+ await waitForEvent(providerFetch, "provider-fetch-error", () => {
160
+ providerFetch.setAttribute("api-url", "/api/test");
161
+ });
162
+ expect.assertions(5);
163
+ });
164
+
165
+ it("interpolates params from its own attributes", async () => {
166
+ const providerFetch = fixture<HTMLProviderFetchElement>(
167
+ `<provider-fetch></provider-fetch>`,
168
+ );
169
+ const fetchSpy = spyFetch(fetchStubs.success as any);
170
+
171
+ providerFetch.addEventListener("provider-fetch-loading", () => {
172
+ expect(providerFetch).dom.to.equalTag(
173
+ `<provider-fetch api-url="/api/users/123" is-loading></provider-fetch>`,
174
+ );
175
+ expect(providerFetch.provision).toEqual(null);
176
+ });
177
+
178
+ providerFetch.addEventListener("provider-fetch-success", () => {
179
+ expect(providerFetch).dom.to.equalTag(
180
+ `<provider-fetch api-url="/api/users/123" is-success></provider-fetch>`,
181
+ );
182
+ expect(providerFetch.provision?.body).toEqual({ foo: "bar" });
183
+ const calledUrl = fetchSpy.mock.calls[0][0] as string;
184
+ expect(calledUrl).toBe("http://localhost:3000/api/users/123");
185
+ });
186
+
187
+ await waitForEvent(providerFetch, "provider-fetch-success", () => {
188
+ providerFetch.setAttribute("api-url", "/api/users/123");
189
+ });
190
+ expect.assertions(5);
191
+ });
192
+
193
+ it("aborts existing fetch when new one is triggered", async () => {
194
+ const providerFetch = fixture<HTMLProviderFetchElement>(
195
+ `<provider-fetch></provider-fetch>`,
196
+ );
197
+ spyFetch(fetchStubs.success as any);
198
+
199
+ providerFetch.addEventListener("provider-fetch-loading", () => {
200
+ expect(providerFetch).dom.to.equalTag(
201
+ `<provider-fetch api-url="/api/test" is-loading></provider-fetch>`,
202
+ );
203
+ });
204
+ let secondFetch = false;
205
+ providerFetch.addEventListener("provider-fetch-success", () => {
206
+ expect(secondFetch).toBe(true);
207
+ });
208
+ await waitForEvent(providerFetch, "provider-fetch-success", () => {
209
+ providerFetch.setAttribute("api-url", "/api/test");
210
+ secondFetch = true;
211
+ invokeCommand(providerFetch, "--fetch");
212
+ });
213
+ expect.assertions(3);
214
+ });
215
+ });
@@ -0,0 +1,56 @@
1
+ import { invokeCommand } from "@excom/neutron";
2
+ import "@excom/event-handler";
3
+ import "@excom/quark-sheet";
4
+ import "../../index";
5
+ import {
6
+ afterEach,
7
+ describe,
8
+ expect,
9
+ it,
10
+ spyFetch,
11
+ vi,
12
+ waitForEvent,
13
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
14
+ import {
15
+ expectComplexity,
16
+ flush,
17
+ measureComplexity,
18
+ mountView,
19
+ readDemo,
20
+ } from "@excom/quark/support/tests/view-helpers";
21
+
22
+ describe("refetch view", () => {
23
+ afterEach(() => {
24
+ document.body.innerHTML = "";
25
+ vi.restoreAllMocks();
26
+ });
27
+
28
+ it("loads then refetches", async () => {
29
+ spyFetch({
30
+ status: 200,
31
+ body: JSON.stringify({ id: 1, title: "Once" }),
32
+ });
33
+ const { root, quark } = await mountView(readDemo(import.meta.url, "refetch"));
34
+ const provider = root.querySelector("provider-fetch")!;
35
+ if (!provider.hasAttribute("is-success")) {
36
+ await waitForEvent(provider, "provider-fetch-success");
37
+ }
38
+ await flush();
39
+ expect([...root.querySelectorAll("li")].some((li) => /Once/.test(li.textContent || ""))).toBe(
40
+ true,
41
+ );
42
+ const button = root.querySelector<HTMLButtonElement>("button")!;
43
+ expect(button.getAttribute("command")).toBe("--fetch");
44
+ expect(button.getAttribute("commandfor")).toBe(provider.id);
45
+ const meter = measureComplexity(quark!);
46
+ // happy-dom has no Command API: dispatch what the button would have
47
+ await waitForEvent(provider, "provider-fetch-success", () => {
48
+ invokeCommand(provider, "--fetch", button);
49
+ });
50
+ await flush();
51
+ const budget = meter.take();
52
+ meter.stop();
53
+ expect(provider.hasAttribute("is-success")).toBe(true);
54
+ expectComplexity(budget);
55
+ });
56
+ });
@@ -0,0 +1,43 @@
1
+ import "@excom/quark-sheet";
2
+ import "../../index";
3
+ import {
4
+ afterEach,
5
+ describe,
6
+ expect,
7
+ it,
8
+ spyFetch,
9
+ waitForEvent,
10
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
11
+ import {
12
+ expectComplexity,
13
+ flush,
14
+ measureComplexity,
15
+ mountView,
16
+ readDemo,
17
+ } from "@excom/quark/support/tests/view-helpers";
18
+
19
+ describe("simple view", () => {
20
+ afterEach(() => {
21
+ document.body.innerHTML = "";
22
+ });
23
+
24
+ it("renders the fetched todo", async () => {
25
+ spyFetch({
26
+ status: 200,
27
+ body: JSON.stringify({ id: 7, title: "Ship it", completed: false }),
28
+ });
29
+ const { root, quark } = await mountView(readDemo(import.meta.url, "simple"));
30
+ const provider = root.querySelector("provider-fetch")!;
31
+ if (!provider.hasAttribute("is-success")) {
32
+ await waitForEvent(provider, "provider-fetch-success");
33
+ await flush();
34
+ }
35
+ const meter = measureComplexity(quark!);
36
+ await flush();
37
+ const budget = meter.take();
38
+ meter.stop();
39
+ expect(root.querySelector("h4")?.textContent).toMatch(/Todo #7: Ship it/);
40
+ expect(root.querySelector("span")?.textContent).toMatch(/Completed: false/);
41
+ expectComplexity(budget);
42
+ });
43
+ });
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
+ }