@dooboostore/simple-web-component 1.0.54 → 1.0.56
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/README.md +342 -56
- package/dist/cjs/decorators/addEventListener.js +37 -14
- package/dist/cjs/decorators/addEventListener.js.map +2 -2
- package/dist/cjs/decorators/applyAttribute.js +6 -3
- package/dist/cjs/decorators/applyAttribute.js.map +2 -2
- package/dist/cjs/decorators/applyClass.js +18 -2
- package/dist/cjs/decorators/applyClass.js.map +2 -2
- package/dist/cjs/decorators/applyNode.js +10 -3
- package/dist/cjs/decorators/applyNode.js.map +2 -2
- package/dist/cjs/decorators/changedAttribute.js +2 -5
- package/dist/cjs/decorators/changedAttribute.js.map +2 -2
- package/dist/cjs/decorators/elementDefine.js +13 -2
- package/dist/cjs/decorators/elementDefine.js.map +2 -2
- package/dist/cjs/decorators/eventMedia.js +2 -5
- package/dist/cjs/decorators/eventMedia.js.map +2 -2
- package/dist/cjs/decorators/fetch.js +216 -82
- package/dist/cjs/decorators/fetch.js.map +3 -3
- package/dist/cjs/decorators/index.js +1 -0
- package/dist/cjs/decorators/index.js.map +2 -2
- package/dist/cjs/decorators/intersectionObserver.js +3 -6
- package/dist/cjs/decorators/intersectionObserver.js.map +2 -2
- package/dist/cjs/decorators/mutationObserver.js +3 -6
- package/dist/cjs/decorators/mutationObserver.js.map +2 -2
- package/dist/cjs/decorators/parameter.js +197 -33
- package/dist/cjs/decorators/parameter.js.map +3 -3
- package/dist/cjs/decorators/persistState.js +474 -0
- package/dist/cjs/decorators/persistState.js.map +7 -0
- package/dist/cjs/decorators/resizeObserver.js +3 -6
- package/dist/cjs/decorators/resizeObserver.js.map +2 -2
- package/dist/cjs/decorators/setInterval.js +2 -5
- package/dist/cjs/decorators/setInterval.js.map +2 -2
- package/dist/cjs/decorators/setTimeout.js +2 -5
- package/dist/cjs/decorators/setTimeout.js.map +2 -2
- package/dist/cjs/decorators/subscribeSwcAppMessage.js.map +1 -1
- package/dist/cjs/elements/SwcAppMixin.js +50 -2
- package/dist/cjs/elements/SwcAppMixin.js.map +2 -2
- package/dist/esm/decorators/addEventListener.js +38 -15
- package/dist/esm/decorators/addEventListener.js.map +2 -2
- package/dist/esm/decorators/applyAttribute.js +6 -3
- package/dist/esm/decorators/applyAttribute.js.map +2 -2
- package/dist/esm/decorators/applyClass.js +18 -2
- package/dist/esm/decorators/applyClass.js.map +2 -2
- package/dist/esm/decorators/applyNode.js +10 -3
- package/dist/esm/decorators/applyNode.js.map +2 -2
- package/dist/esm/decorators/changedAttribute.js +3 -6
- package/dist/esm/decorators/changedAttribute.js.map +2 -2
- package/dist/esm/decorators/elementDefine.js +15 -4
- package/dist/esm/decorators/elementDefine.js.map +2 -2
- package/dist/esm/decorators/eventMedia.js +3 -6
- package/dist/esm/decorators/eventMedia.js.map +2 -2
- package/dist/esm/decorators/fetch.js +216 -82
- package/dist/esm/decorators/fetch.js.map +3 -3
- package/dist/esm/decorators/index.js +1 -0
- package/dist/esm/decorators/index.js.map +2 -2
- package/dist/esm/decorators/intersectionObserver.js +4 -7
- package/dist/esm/decorators/intersectionObserver.js.map +2 -2
- package/dist/esm/decorators/mutationObserver.js +4 -7
- package/dist/esm/decorators/mutationObserver.js.map +2 -2
- package/dist/esm/decorators/parameter.js +197 -33
- package/dist/esm/decorators/parameter.js.map +3 -3
- package/dist/esm/decorators/persistState.js +455 -0
- package/dist/esm/decorators/persistState.js.map +7 -0
- package/dist/esm/decorators/resizeObserver.js +4 -7
- package/dist/esm/decorators/resizeObserver.js.map +2 -2
- package/dist/esm/decorators/setInterval.js +3 -6
- package/dist/esm/decorators/setInterval.js.map +2 -2
- package/dist/esm/decorators/setTimeout.js +3 -6
- package/dist/esm/decorators/setTimeout.js.map +2 -2
- package/dist/esm/decorators/subscribeSwcAppMessage.js.map +1 -1
- package/dist/esm/elements/SwcAppMixin.js +51 -3
- package/dist/esm/elements/SwcAppMixin.js.map +2 -2
- package/dist/esm-bundle/dooboostore-simple-web-component.esm.js +4491 -3637
- package/dist/esm-bundle/dooboostore-simple-web-component.esm.js.map +4 -4
- package/dist/types/decorators/addEventListener.d.ts +15 -0
- package/dist/types/decorators/addEventListener.d.ts.map +1 -1
- package/dist/types/decorators/applyAttribute.d.ts +1 -0
- package/dist/types/decorators/applyAttribute.d.ts.map +1 -1
- package/dist/types/decorators/applyClass.d.ts.map +1 -1
- package/dist/types/decorators/applyNode.d.ts +4 -2
- package/dist/types/decorators/applyNode.d.ts.map +1 -1
- package/dist/types/decorators/changedAttribute.d.ts.map +1 -1
- package/dist/types/decorators/elementDefine.d.ts.map +1 -1
- package/dist/types/decorators/eventMedia.d.ts.map +1 -1
- package/dist/types/decorators/fetch.d.ts +52 -31
- package/dist/types/decorators/fetch.d.ts.map +1 -1
- package/dist/types/decorators/index.d.ts +1 -0
- package/dist/types/decorators/index.d.ts.map +1 -1
- package/dist/types/decorators/intersectionObserver.d.ts.map +1 -1
- package/dist/types/decorators/mutationObserver.d.ts.map +1 -1
- package/dist/types/decorators/parameter.d.ts +169 -31
- package/dist/types/decorators/parameter.d.ts.map +1 -1
- package/dist/types/decorators/persistState.d.ts +66 -0
- package/dist/types/decorators/persistState.d.ts.map +1 -0
- package/dist/types/decorators/resizeObserver.d.ts.map +1 -1
- package/dist/types/decorators/setInterval.d.ts.map +1 -1
- package/dist/types/decorators/setTimeout.d.ts.map +1 -1
- package/dist/types/decorators/subscribeSwcAppMessage.d.ts +1 -1
- package/dist/types/decorators/subscribeSwcAppMessage.d.ts.map +1 -1
- package/dist/types/elements/SwcAppMixin.d.ts +12 -0
- package/dist/types/elements/SwcAppMixin.d.ts.map +1 -1
- package/dist/umd-bundle/dooboostore-simple-web-component.umd.js +4491 -3637
- package/dist/umd-bundle/dooboostore-simple-web-component.umd.js.map +4 -4
- package/lsp/bin/swc-lsp.js +3 -0
- package/lsp/out/server/server.js +9960 -0
- package/lsp/package.json +56 -0
- package/package.json +24 -9
- package/src/decorators/addEventListener.ts +23 -15
- package/src/decorators/applyAttribute.ts +8 -3
- package/src/decorators/applyClass.ts +22 -2
- package/src/decorators/applyNode.ts +18 -6
- package/src/decorators/changedAttribute.ts +3 -4
- package/src/decorators/elementDefine.ts +19 -7
- package/src/decorators/eventMedia.ts +3 -6
- package/src/decorators/fetch.ts +270 -140
- package/src/decorators/index.ts +1 -0
- package/src/decorators/intersectionObserver.ts +4 -5
- package/src/decorators/mutationObserver.ts +4 -5
- package/src/decorators/parameter.ts +326 -42
- package/src/decorators/persistState.ts +408 -0
- package/src/decorators/resizeObserver.ts +4 -5
- package/src/decorators/setInterval.ts +3 -4
- package/src/decorators/setTimeout.ts +3 -4
- package/src/decorators/subscribeSwcAppMessage.ts +1 -1
- package/src/elements/SwcAppMixin.ts +64 -5
package/README.md
CHANGED
|
@@ -399,16 +399,20 @@ class ProductList extends HTMLElement {
|
|
|
399
399
|
- `@hostSet` - host-ancestor-tree info only (`$host`, `$parentHost`, `$hosts`, `$appHost`, `$appHosts`, `$firstHost`, `$lastHost`, `$firstAppHost`, `$lastAppHost`)
|
|
400
400
|
- `@helperHostSet` - `@hostSet` + DOM/window helpers (`$d`, `$w`, `$q`, `$qa`, `$qi`) + `$this`
|
|
401
401
|
- `@helperSet` - DOM/window helpers only (`$d`, `$w`, `$q`, `$qa`, `$qi`), no host-tree info
|
|
402
|
+
- `@swcAppRouter` - the `Router` instance (`$appHost.router`) directly, no `@inject`/DI container needed; `undefined` outside a `SwcApp` tree
|
|
403
|
+
- `@swcAppSimpleApplication` - the `SimpleApplication` (DI container) instance (`$appHost.simpleApplication`) directly; `undefined` outside a `SwcApp` tree or without a DI container. `@inject` is usually what you want instead — reach for this only when you need the container itself (e.g. `simstanceManager`).
|
|
404
|
+
- `@swcAppHost` - the `SwcApp` host element itself (`$appHost`) directly, when you just need the host and not the whole `HostSet`; `null` outside a `SwcApp` tree (matches `HostSet.$appHost`'s own type, unlike the two above which use optional chaining down to `undefined`)
|
|
402
405
|
|
|
403
406
|
`$w`/`$d` (and the `helper` passed to every decorator's `filter`/`before`/`finally`) come from the element's window, resolved the same way everywhere: its own `@elementDefine({ window })` → the nearest ancestor SWC host's → the global `window`. Route subscribers (`@subscribeSwcAppRouteChange`) and app messages use the SwcApp's runtime `window` instead (the per-request window under SSR).
|
|
404
407
|
|
|
405
|
-
The same mechanism (and the same
|
|
406
|
-
- **`@onInitialize`/`@onConnectedBefore`/`@onConnectedAfter`/... lifecycle methods** — can freely mix `@hostSet`/`@helperHostSet`/`@helperSet` with `@inject(...)` on the same method.
|
|
408
|
+
The same mechanism (and the same 8 decorators, plus two more) also applies to:
|
|
409
|
+
- **`@onInitialize`/`@onConnectedBefore`/`@onConnectedAfter`/... lifecycle methods** — can freely mix `@hostSet`/`@helperHostSet`/`@helperSet`/`@swcAppRouter`/`@swcAppSimpleApplication`/`@swcAppHost` with `@inject(...)` on the same method.
|
|
407
410
|
- **`@subscribeSwcAppRouteChange`** — add `@swcAppRouterEvent` to receive the `RouterEventType` regardless of position.
|
|
408
|
-
- **`@subscribeSwcAppMessage`** — add `@
|
|
409
|
-
- **`before` hook results** — `@eventBeforeReturn`, `@routeChangeBeforeReturn`, `@
|
|
411
|
+
- **`@subscribeSwcAppMessage`** — add `@swcAppMessage` to receive the `SwcAppMessage` regardless of position.
|
|
412
|
+
- **`before` hook results** — `@eventBeforeReturn`, `@routeChangeBeforeReturn`, `@swcAppMessageBeforeReturn`, `@changedAttributeBeforeReturn`, `@mutationObserverBeforeReturn`, `@resizeObserverBeforeReturn`, `@intersectionObserverBeforeReturn`, `@eventMediaBeforeReturn`, `@setIntervalBeforeReturn`, `@setTimeoutBeforeReturn` inject the value returned by that decorator's `before` option.
|
|
413
|
+
- **DOM/storage/fetch values** — `@querySelectorParam`, `@attributeParam`, `@localStorageParam`/`@sessionStorageParam`/`@cookieParam`/`@indexedDbParam`, `@fetchParam` go a step further and actively compute/fetch a value instead of just handing you context — see "Value-Injecting Parameter Decorators" below.
|
|
410
414
|
|
|
411
|
-
Note: once any parameter of a method is decorated,
|
|
415
|
+
Note: once any parameter of a method is decorated, every parameter still gets a value — decorated ones receive their injected value, and any parameter left undecorated falls back to its legacy positional argument. So mixing is safe: `onChanged(nv, @querySelectorParam('#x') el, name)` still gets `nv`/`name` from the usual positional arguments.
|
|
412
416
|
|
|
413
417
|
```typescript
|
|
414
418
|
@onConnectedAfter
|
|
@@ -418,7 +422,7 @@ onconstructor(@inject(UserService.SYMBOL) userService: UserService, @hostSet hs:
|
|
|
418
422
|
onRouteChanged(@helperSet helpers: HelperSet, @swcAppRouterEvent re: RouterEventType) { ... }
|
|
419
423
|
|
|
420
424
|
@subscribeSwcAppMessage
|
|
421
|
-
onMessage(@hostSet hs: HostSet, @
|
|
425
|
+
onMessage(@hostSet hs: HostSet, @swcAppMessage msg: SwcAppMessage) { ... }
|
|
422
426
|
```
|
|
423
427
|
|
|
424
428
|
#### @emitCustomEvent
|
|
@@ -618,7 +622,8 @@ class ProductCard extends HTMLElement {
|
|
|
618
622
|
- A field getter evaluates a `{{= expr }}` attribute value (see "Attribute Expressions")
|
|
619
623
|
|
|
620
624
|
#### @removeAttribute (Method Decorator)
|
|
621
|
-
Always removes the attribute when the method runs
|
|
625
|
+
Always removes the attribute when the method runs — the name already says what to do, so the return value doesn't matter.
|
|
626
|
+
With `valueKey`, it removes only when that key is truthy. The return value passes through unchanged,
|
|
622
627
|
so it is safe on subscriber handlers that must not return a value (e.g. route changes).
|
|
623
628
|
|
|
624
629
|
```typescript
|
|
@@ -891,6 +896,20 @@ class ContentUpdater extends HTMLElement {
|
|
|
891
896
|
- `@innerHtml` / `@innerHtmlLight` / `@innerHtmlShadow` - Set innerHTML
|
|
892
897
|
- `@innerText` / `@innerTextLight` / `@innerTextShadow` - Set innerText
|
|
893
898
|
- `@clearChildrenNode` / `@clearChildrenLight` - Remove all children
|
|
899
|
+
- `@removeNode` - Remove the target element itself (bare / `'$this'` removes the component)
|
|
900
|
+
|
|
901
|
+
**Declaration = action** (`@removeNode`, `@clearChildrenNode`, `@removeAttribute`, and `@addClass` / `@removeClass` / `@toggleClass` with the class name written in the decorator):
|
|
902
|
+
they run whatever the method returns. Give a `valueKey` to decide per call — then they run only when that key is truthy
|
|
903
|
+
(`@toggleClass('.x', 'open')`: no return toggles, `true` / `false` forces on / off).
|
|
904
|
+
|
|
905
|
+
```typescript
|
|
906
|
+
@eventClick('.close')
|
|
907
|
+
@removeNode('.toast') // always removes
|
|
908
|
+
close() {}
|
|
909
|
+
|
|
910
|
+
@removeNode('.editor', { valueKey: 'close' }) // removes only when close is truthy
|
|
911
|
+
save() { return { close: this.saved }; }
|
|
912
|
+
```
|
|
894
913
|
- Options: `position`, `root`, `filter`, `fallback`, `valueKey`. Ready-made `filter` helpers: `skipIfSameTagPresent`, `skipIfExists(selector)`, `skipIfEmpty`, `applyIfChanged` (e.g. `@innerHtmlLight({ filter: skipIfExists('my-page') })`)
|
|
895
914
|
|
|
896
915
|
**Position Options:**
|
|
@@ -957,7 +976,7 @@ class StyledComponent extends HTMLElement {
|
|
|
957
976
|
**@applyClass Variants:** (each takes `(selector?, classMapOrOptions?, options?)`; bare form targets `$this`)
|
|
958
977
|
- `setClass` - Replace all classes
|
|
959
978
|
- `updateClass` - Toggle classes by a `{ className: boolean }` map
|
|
960
|
-
- `addClass` / `removeClass` / `toggleClass`
|
|
979
|
+
- `addClass` / `removeClass` / `toggleClass` — with the class name in the decorator (`@addClass('.x', 'on')`) the declaration is the action (see "Declaration = action" above)
|
|
961
980
|
- `applyClass(selector, action?)` / `cls` - action `'set' | 'update' | 'add' | 'remove' | 'toggle'` (default `'update'`); `clsAppHost` / `clsLight` / `clsShadow` / `clsAll` (for $this use bare `@setClass` / `@updateClass` / `@addClass` / `@removeClass` / `@toggleClass`)
|
|
962
981
|
- Note: for the class decorators, an options object in the 2nd position must contain `root` (e.g. `{ root: 'auto', valueKey: 'k' }`); an object without `root` is read as a class map
|
|
963
982
|
|
|
@@ -1158,7 +1177,11 @@ class NotificationPanel extends HTMLElement {
|
|
|
1158
1177
|
}
|
|
1159
1178
|
```
|
|
1160
1179
|
|
|
1161
|
-
**@subscribeSwcAppMessage Options:** `filter(message, currentThis)`, `before(message, currentThis)` (result → `@
|
|
1180
|
+
**@subscribeSwcAppMessage Options:** `filter(message, currentThis)`, `before(message, currentThis)` (result → `@swcAppMessageBeforeReturn`), `finally(message, currentThis, { args, result, error })`, `subject`, `trigger` (see "Message Replay"). Every matching subscriber runs (fire-and-forget); return values do not stop other subscribers.
|
|
1181
|
+
|
|
1182
|
+
**Shortcuts for `subject`:** `@subscribeSwcAppMessageBehavior(type, options?)` = `{ subject: 'behavior' }` (a late subscriber gets the last message),
|
|
1183
|
+
`@subscribeSwcAppMessageReplay(type, options?)` = `{ subject: 'replay' }` (gets the whole buffer). Same forms as `@subscribeSwcAppMessage`
|
|
1184
|
+
(bare, type, options, type + options); the other options pass through.
|
|
1162
1185
|
|
|
1163
1186
|
#### @publishSwcAppMessage
|
|
1164
1187
|
Publish a message from a method's return value.
|
|
@@ -1485,9 +1508,25 @@ No prefix means *first*; `First`/`Last` pick the first/last value when a key rep
|
|
|
1485
1508
|
load(@swcAppRoutePathVariable('releaseSeq') seq: string, @swcAppRouteQueryParam('tab') tab: string | null) { ... }
|
|
1486
1509
|
```
|
|
1487
1510
|
|
|
1488
|
-
-
|
|
1511
|
+
- Add `@swcAppRouterEvent` for the whole event — any parameter left undecorated still gets the positional route argument even after others are decorated (see the parameter-decorator note earlier in this doc).
|
|
1489
1512
|
- `route.pathData` keeps its old behavior: a repeated variable name holds the **last** value. Use the decorators above to pick first/last explicitly.
|
|
1490
1513
|
|
|
1514
|
+
**On fields.** The same decorators work on fields. The SwcApp fills them on every route change and, for an element attached later, from the current route before its first render. Inside a route handler of the same element, the field already holds this route's value.
|
|
1515
|
+
|
|
1516
|
+
```typescript
|
|
1517
|
+
class UserPage extends HTMLElement {
|
|
1518
|
+
@swcAppRouteQueryParam('tab') tab?: string | null; // query: no pattern needed
|
|
1519
|
+
@swcAppRoutePathVariable('id', { path: '/user/{id}' }) userId?: string; // path variables need { path }
|
|
1520
|
+
@swcAppRoutePathVariableObject({ path: ['/user/{id}', '/member/{id}'] }) vars?: { id?: string };
|
|
1521
|
+
|
|
1522
|
+
@onConnectedBodyShadow render() { return `<h1>${this.userId} · ${this.tab}</h1>`; }
|
|
1523
|
+
}
|
|
1524
|
+
```
|
|
1525
|
+
|
|
1526
|
+
- `path` takes the same forms as `@subscribeSwcAppRouteChange` (string, array, or `(currentThis) => ...`). When it doesn't match the current route, path fields are empty (`undefined`, `[]`, `{}`); query fields keep updating. A path-variable field without `{ path }` warns once at definition.
|
|
1527
|
+
- A field is reassigned only when its value changes (object fields keep their reference otherwise), so a setter on it doesn't fire for unrelated route changes.
|
|
1528
|
+
- Don't name such fields after built-in `HTMLElement` properties (`id`, `lang`, `title`, `prefix`, ...): assigning to them changes the element instead.
|
|
1529
|
+
|
|
1491
1530
|
**6. Navigating: `@swcAppRoute` / `@swcAppRouteGo`**
|
|
1492
1531
|
|
|
1493
1532
|
The method returns where to go; the decorator calls the element's SwcApp router — no `Router` injection needed. `@swcAppRoute({ type })` picks the Router method (default `'go'`); `@swcAppRouteGo`, `@swcAppRoutePush`, ... are aliases with the type fixed.
|
|
@@ -1953,6 +1992,40 @@ constructor(private service: Service) { super(); } // Web Components can't have
|
|
|
1953
1992
|
|
|
1954
1993
|
---
|
|
1955
1994
|
|
|
1995
|
+
## ⚡ Benchmarks
|
|
1996
|
+
|
|
1997
|
+
Measured in real Chrome against a hand-written `innerHTML` renderer and Lit, rendering the same 1,000-row table (state in fields, a decorated method returns the HTML, `@replaceChildren*` applies it):
|
|
1998
|
+
|
|
1999
|
+
- **Bulk rendering keeps up with Lit.** Creating 10,000 rows takes 244 ms, versus 232 ms for Lit and 222 ms for a hand-written `innerHTML` loop.
|
|
2000
|
+
- **Low memory.** 1,000 rows keep about 94 KB of JS heap, 6x less than Lit (558 KB); at 10,000 rows it is 1.6 MB versus 4.9 MB (3x less). Only the data is retained, no per-row template bookkeeping.
|
|
2001
|
+
- **Thin layer.** Rendering through the decorators costs within about 5% to 25% of the equivalent hand-written `innerHTML` code.
|
|
2002
|
+
|
|
2003
|
+
```text
|
|
2004
|
+
create 10,000 rows (lower is better)
|
|
2005
|
+
vanilla (innerHTML) ██████████████████████████ 222 ▲
|
|
2006
|
+
lit ████████████████████████████ 232
|
|
2007
|
+
simple-web-component █████████████████████████████ 244
|
|
2008
|
+
vanilla (DOM calls) ████████████████████████████████ 269
|
|
2009
|
+
|
|
2010
|
+
JS heap, 1,000 rows (KB) (lower is better)
|
|
2011
|
+
vanilla (innerHTML) ████ 73.4 ▲
|
|
2012
|
+
simple-web-component █████ 93.5
|
|
2013
|
+
vanilla (DOM calls) ███████ 119
|
|
2014
|
+
lit ████████████████████████████████ 558
|
|
2015
|
+
```
|
|
2016
|
+
|
|
2017
|
+
Full results, including the operations where other approaches are faster, methodology and caveats: [bench/RESULTS.md](./bench/RESULTS.md). Reproduce with `pnpm bench` inside `bench/`.
|
|
2018
|
+
|
|
2019
|
+
---
|
|
2020
|
+
|
|
2021
|
+
## 🧰 Editor Support (LSP)
|
|
2022
|
+
|
|
2023
|
+
The package ships a language server for SWC templates — `npx swc-lsp --stdio`:
|
|
2024
|
+
tag / attribute / `@state@` / `$host.` completion, hover, go to definition, parameter hints, and type errors inside `swc-on-*`, `{{= }}` and `@x@`
|
|
2025
|
+
(`swc-on-click="$host.tt(4)"` → *number is not assignable to string*).
|
|
2026
|
+
|
|
2027
|
+
Setup for IntelliJ, VS Code and Neovim: [lsp/README.md](https://github.com/dooboostore-develop/packages/tree/main/@dooboostore/simple-web-component/lsp).
|
|
2028
|
+
|
|
1956
2029
|
## 🧪 Testing
|
|
1957
2030
|
|
|
1958
2031
|
`pnpm test` type-checks the tests and runs them in a real browser (Chromium via Playwright, falling back to the installed Chrome) — no jsdom. Each test gets its own same-origin iframe window, so custom elements, `history` and `location` never leak between tests.
|
|
@@ -1999,8 +2072,8 @@ Late subscribers can receive past messages. The buffer keeps every typed message
|
|
|
1999
2072
|
```typescript
|
|
2000
2073
|
// 'behavior' = last message only, 'replay' = whole buffer in order.
|
|
2001
2074
|
// Unset (or 'subject') = live only.
|
|
2002
|
-
@subscribeSwcAppMessage('auth-changed', { subject: 'behavior' })
|
|
2003
|
-
onAuth(@
|
|
2075
|
+
@subscribeSwcAppMessageBehavior('auth-changed') // = @subscribeSwcAppMessage('auth-changed', { subject: 'behavior' })
|
|
2076
|
+
onAuth(@swcAppMessage msg: SwcAppMessage<User | null>) { ... }
|
|
2004
2077
|
```
|
|
2005
2078
|
|
|
2006
2079
|
`trigger` controls when replay happens (default `'connected'`).
|
|
@@ -2014,12 +2087,26 @@ onAuth(@appMessage msg: SwcAppMessage<User | null>) { ... }
|
|
|
2014
2087
|
```typescript
|
|
2015
2088
|
@subscribeSwcAppMessage('auth-changed', { subject: 'behavior', trigger: 'connectedDone' })
|
|
2016
2089
|
@innerHtml('.wrap')
|
|
2017
|
-
async load(@
|
|
2090
|
+
async load(@swcAppMessage msg: SwcAppMessage<User | null>) { ... }
|
|
2018
2091
|
```
|
|
2019
2092
|
|
|
2020
2093
|
Route subscribers (`@subscribeSwcAppRouteChange`) accept the same `trigger`.
|
|
2021
2094
|
Live broadcasts always reach everyone regardless of trigger.
|
|
2022
2095
|
|
|
2096
|
+
### `@swcAppMessage` on fields
|
|
2097
|
+
|
|
2098
|
+
On a field, `@swcAppMessage` holds the latest `SwcAppMessage` — set on every publish, before the same element's subscribers run. It takes the same forms and `subject` rule as `@subscribeSwcAppMessage`:
|
|
2099
|
+
|
|
2100
|
+
```typescript
|
|
2101
|
+
@swcAppMessage last?: SwcAppMessage; // every type, live only
|
|
2102
|
+
@swcAppMessage('auth-changed') auth?: SwcAppMessage<User | null>; // live only (default 'subject')
|
|
2103
|
+
@swcAppMessage('auth-changed', { subject: 'behavior' }) me?: SwcAppMessage<User | null>; // late element: last one
|
|
2104
|
+
@swcAppMessage({ messageType: 'auth-changed', subject: 'replay' }) all?: SwcAppMessage; // late element: buffer in order
|
|
2105
|
+
```
|
|
2106
|
+
|
|
2107
|
+
- With `'behavior'`/`'replay'` (and a type), an element attached after the publish gets the field filled from the buffer before its first render. Without a type nothing is replayed.
|
|
2108
|
+
- `@swcAppMessageBeforeReturn` stays parameter-only: it is the `before` hook's result for one handler call.
|
|
2109
|
+
|
|
2023
2110
|
## 🔌 App Host Hooks (onConnected / onSwcAppConnected / onDisconnected)
|
|
2024
2111
|
|
|
2025
2112
|
The three host hooks of `SwcAppMixin` are **abstract**: a subclass must implement them (use an empty method if there is nothing to do).
|
|
@@ -2064,37 +2151,119 @@ Use `connectedElements()` to get the currently connected child elements.
|
|
|
2064
2151
|
|
|
2065
2152
|
## 🌐 @fetch — Declarative HTTP
|
|
2066
2153
|
|
|
2154
|
+
`@fetch` and its aliases (`@fetchGet`, `@fetchPost`, `@fetchLatest`, `@fetchManual`, …) go in one of three places.
|
|
2155
|
+
Pick by **who calls it** and **whether the result should be remembered**:
|
|
2156
|
+
|
|
2157
|
+
| Where | Declare | Use when | Remembers the result? |
|
|
2158
|
+
|---|---|---|---|
|
|
2159
|
+
| **Method** | `load(@fetchPromise p: Promise<T>) { … }` | the framework calls it — a trigger handler (`@eventClick`, `@onConnectedAfter`, a route…) | no |
|
|
2160
|
+
| **Function field** | `declare getPost: (id: number) => Promise<T>;` | you call it yourself, anywhere, maybe with arguments (`this.getPost(1)`) | no — every call is a new request |
|
|
2161
|
+
| **Value field** | `declare me?: Promise<T>;` | data you load once and keep reading (`await this.me`) | **yes** — until you clear it |
|
|
2162
|
+
|
|
2163
|
+
### 1. On a method — trigger handlers
|
|
2164
|
+
|
|
2165
|
+
Stack the trigger, the outputs and `@fetch` on one method. The request starts first; its `Promise` is handed to the
|
|
2166
|
+
`@fetchPromise` parameter and the method runs **right away**. The method's own return value is returned unchanged, so the
|
|
2167
|
+
signature you wrote stays true (a sync method stays sync).
|
|
2168
|
+
|
|
2067
2169
|
```typescript
|
|
2068
|
-
|
|
2069
|
-
@
|
|
2070
|
-
|
|
2071
|
-
|
|
2170
|
+
@eventClick('.load')
|
|
2171
|
+
@innerHtml('.out', { fallback: () => 'Loading…' }) // shown until the returned Promise settles
|
|
2172
|
+
@fetchLatest('/api/post/1') // click again → the previous request is aborted
|
|
2173
|
+
load(@fetchPromise p: Promise<Post>) {
|
|
2174
|
+
return p.then(post => post.title, () => 'Failed'); // success / failure, no try/catch
|
|
2072
2175
|
}
|
|
2176
|
+
```
|
|
2073
2177
|
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
@fetch({ url: '/api/post/1', request: 'GET' })
|
|
2077
|
-
async loadBefore(@fetchSettled settled: PromiseSettledResult<any>) {
|
|
2078
|
-
return settled.status === 'fulfilled' ? settled.value : null;
|
|
2079
|
-
}
|
|
2178
|
+
If you also call such a method yourself, declare the parameter optional (`@fetchPromise p?: Promise<T>`) — the
|
|
2179
|
+
framework fills it, callers don't. For a request you call yourself, a field (below) is the better fit.
|
|
2080
2180
|
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
}
|
|
2181
|
+
### 2. Function field — a request function you call
|
|
2182
|
+
|
|
2183
|
+
The framework writes the function body; the declared type is the real signature (no injected parameter, no changed
|
|
2184
|
+
return type).
|
|
2086
2185
|
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2186
|
+
```typescript
|
|
2187
|
+
// GET / DELETE: the arguments feed url / request
|
|
2188
|
+
@fetchGet((_self, _helper, [id]) => `/api/posts/${id}`)
|
|
2189
|
+
declare getPost: (id: number) => Promise<Post>;
|
|
2190
|
+
|
|
2191
|
+
// POST / PUT / PATCH: the last argument is the body
|
|
2192
|
+
@fetchPost('/api/posts', { process: 'json' })
|
|
2193
|
+
declare createPost: (body: NewPost) => Promise<Post>;
|
|
2194
|
+
|
|
2195
|
+
@fetchLatest({ url: (_s, _h, [q]) => `/api/search?q=${q}` }) // only the latest call settles
|
|
2196
|
+
declare search: (q: string) => Promise<Item[]>;
|
|
2197
|
+
|
|
2198
|
+
// use it from a trigger handler (or anywhere)
|
|
2199
|
+
@eventClick('.load')
|
|
2200
|
+
@innerHtml('.out', { fallback: () => 'Loading…' })
|
|
2201
|
+
show() {
|
|
2202
|
+
return this.getPost(1).then(p => p.title);
|
|
2091
2203
|
}
|
|
2092
2204
|
```
|
|
2093
2205
|
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2206
|
+
Every call is a new request. Success resolves, failure rejects with `HttpResponseError` (see below).
|
|
2207
|
+
|
|
2208
|
+
### 3. Value field — data that is remembered
|
|
2209
|
+
|
|
2210
|
+
```typescript
|
|
2211
|
+
@fetch('/api/me')
|
|
2212
|
+
declare me?: Promise<User>; // `?` so that `this.me = undefined` type-checks under `strict`
|
|
2213
|
+
|
|
2214
|
+
await this.me; // first read: request → remembered on success
|
|
2215
|
+
await this.me; // remembered result (no request)
|
|
2216
|
+
|
|
2217
|
+
this.me = undefined; // forget it (null works too) …
|
|
2218
|
+
await this.me; // … so this read requests again — this is how you reload
|
|
2219
|
+
|
|
2220
|
+
this.me = savedUser; // or seed a value yourself (e.g. data that came with SSR) — no request
|
|
2221
|
+
```
|
|
2222
|
+
|
|
2223
|
+
- Declare it optional (`declare me?: Promise<T>`) if you ever clear it: under `strict`, assigning `undefined` to a
|
|
2224
|
+
plain `Promise<T>` is a type error. Reading it is still `await this.me` (the result type is `T | undefined`).
|
|
2225
|
+
If you never clear it, `declare me: Promise<T>` is fine.
|
|
2226
|
+
- Clear it by assigning, not with `delete this.me` — the memory lives behind the field's setter.
|
|
2227
|
+
|
|
2228
|
+
- **Remembered**: success, per element. **Not remembered**: failure — the next read requests again.
|
|
2229
|
+
- **Forgotten**: when you assign `undefined` / `null`, and when the element disconnects (the request is aborted; a read
|
|
2230
|
+
after reconnecting requests again).
|
|
2231
|
+
- There are no arguments — build the body in the `request` factory if the request needs one.
|
|
2232
|
+
- If the data must be fresh on every use, declare a function field instead (`declare loadMe: () => Promise<User>`).
|
|
2233
|
+
- The value is a real Promise to your code (`instanceof Promise` is true), so it can be returned straight to an output
|
|
2234
|
+
decorator: `@innerHtml('.name') showName() { return this.me.then(u => u.name); }`.
|
|
2235
|
+
|
|
2236
|
+
### Field rules (function and value fields)
|
|
2237
|
+
|
|
2238
|
+
- **Always write `declare`.** Without it, builds with `useDefineForClassFields: true` (the default for target ES2022+)
|
|
2239
|
+
create an instance field that hides what the framework put there:
|
|
2240
|
+
|
|
2241
|
+
| Build setting | `declare me: Promise<T>` | `me!: Promise<T>` (no `declare`) |
|
|
2242
|
+
|---|---|---|
|
|
2243
|
+
| `useDefineForClassFields: false` | works | works (but `strict` needs the `!`) |
|
|
2244
|
+
| `useDefineForClassFields: true` / ES2022+ default | works | **broken** — reported on connect: *must be declared with `declare`* |
|
|
2245
|
+
|
|
2246
|
+
- **Declared type is checked.** At compile time the field must be `(...args) => Promise<T>` or `Promise<T>`
|
|
2247
|
+
(`string`, `() => T`, … are errors; optional `?` fields are fine; `private` / `protected` fields can't be seen by
|
|
2248
|
+
TypeScript and are not checked).
|
|
2249
|
+
Builds that emit `design:type` (`emitDecoratorMetadata` with tsc / ts-loader) also reject a non-function, non-Promise
|
|
2250
|
+
type at runtime.
|
|
2251
|
+
- **Nothing else on the field.** Triggers and outputs go on a method that calls the field. Method decorators on a field
|
|
2252
|
+
are already compile errors; any other SWC decorator on the same field (`@attribute`, `@query`, …, or anything via
|
|
2253
|
+
`as any`) throws when the class is defined.
|
|
2254
|
+
- **Same in every build.** Function vs. value is decided by how you declared it, not by build metadata: the field holds
|
|
2255
|
+
one object that is callable and a Promise, and TypeScript lets you use it only the way you declared it.
|
|
2256
|
+
- **Quiet ends.** A call skipped by `filter` or made while the element is detached, and a call aborted by the framework
|
|
2257
|
+
(disconnect, `abortPrevious`), return a Promise that **never settles** — nothing draws on a detached DOM, nothing throws.
|
|
2258
|
+
|
|
2259
|
+
### Method modes
|
|
2260
|
+
|
|
2261
|
+
- **before** (default, or `trigger: 'before'`): start the request → inject its `Promise` via
|
|
2262
|
+
`@fetchPromise` → run the method **right away**. Returns the method's own return value, so the
|
|
2263
|
+
signature you wrote stays true (sync stays sync). Success resolves, failure rejects — handle both with
|
|
2264
|
+
`then` / `catch` / `finally` on the Promise.
|
|
2265
|
+
If you also call the method yourself, declare the parameter optional (`@fetchPromise p?: Promise<T>`) —
|
|
2266
|
+
the framework fills it, callers don't.
|
|
2098
2267
|
- **after** (`trigger: 'after'`, or just `valueKey`): run the method → its return value
|
|
2099
2268
|
(or `returnValue[valueKey]`) becomes the body → fetch. Returns the fetch result; a failed fetch throws.
|
|
2100
2269
|
`valueKey` is after-only — combining it with `trigger: 'before'` is a type error.
|
|
@@ -2107,7 +2276,7 @@ async saveForm() {
|
|
|
2107
2276
|
```typescript
|
|
2108
2277
|
// service call — only the result matters
|
|
2109
2278
|
@fetch({ manual: (self) => self.myService.getProfile({}) })
|
|
2110
|
-
|
|
2279
|
+
load(@fetchPromise p: Promise<MyService.ProfileResponse>) { ... }
|
|
2111
2280
|
|
|
2112
2281
|
// your own fetch — pass the signal so an abort cancels the network request too
|
|
2113
2282
|
@fetch({ trigger: 'after', manual: (_self, _helper, _params, signal, returnValue) =>
|
|
@@ -2136,38 +2305,37 @@ Its return value is the fetch result.
|
|
|
2136
2305
|
|
|
2137
2306
|
```typescript
|
|
2138
2307
|
@fetch('/api/post/1')
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
return settled.reason.response?.status; // e.g. 404
|
|
2142
|
-
}
|
|
2308
|
+
load(@fetchPromise p: Promise<Post>) {
|
|
2309
|
+
return p.catch(e => isHttpResponseError(e) ? e.response?.status : undefined); // e.g. 404
|
|
2143
2310
|
}
|
|
2144
2311
|
```
|
|
2145
2312
|
|
|
2146
|
-
**
|
|
2147
|
-
- `filter(
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
-
|
|
2151
|
-
`
|
|
2313
|
+
**filter** — the only hook
|
|
2314
|
+
- `filter({ self, helper, args })` → `false` skips the whole call: no request, no method. A method returns `undefined`;
|
|
2315
|
+
a field returns a Promise that never settles.
|
|
2316
|
+
It is synchronous: the request starts before the method runs, so "send or not" can only be decided here.
|
|
2317
|
+
- There is no `before` / `after` / `finally` on `@fetch`: result handling is plain Promise code
|
|
2318
|
+
(`then` / `catch` / `finally`), and gating or cleanup around the call lives on the trigger you stack it with
|
|
2319
|
+
(`@eventClick`, `@onConnectedAfter`, …), which already has `filter → before → handler → finally`.
|
|
2152
2320
|
|
|
2153
2321
|
**Cancellation**
|
|
2154
2322
|
- Every call gets its own `AbortController`. Declarative requests get its signal as `init.signal`
|
|
2155
2323
|
(merged with a `signal` you return from `request`, via `AbortSignal.any` or a fallback); `manual` gets it as
|
|
2156
2324
|
the `signal` argument.
|
|
2157
|
-
- When the element disconnects, all its in-flight `@fetch` requests are aborted.
|
|
2158
|
-
|
|
2159
|
-
|
|
2325
|
+
- When the element disconnects, all its in-flight `@fetch` requests are aborted. The injected Promise then
|
|
2326
|
+
**never settles** (before mode), so nothing draws on a detached DOM and nothing throws; an after-mode call
|
|
2327
|
+
ends quietly with `undefined`.
|
|
2160
2328
|
- A call that *starts* while the element is detached (e.g. a `setTimeout` scheduled just before the user
|
|
2161
2329
|
navigated away) is skipped the same quiet way. Elements that were never connected are not affected.
|
|
2162
2330
|
- `manual` should pass `signal` on to its own client (`fetch`, axios, ...) so the network
|
|
2163
2331
|
request is actually cancelled. If it ignores the signal, the call still ends as soon as it's aborted
|
|
2164
2332
|
(the result is raced against the abort) — the request just keeps running in the background and its
|
|
2165
2333
|
late result is discarded.
|
|
2166
|
-
- `abortPrevious: true` aborts the previous in-flight call of the same method when it's called again
|
|
2167
|
-
(search-as-you-type) — the superseded
|
|
2334
|
+
- `abortPrevious: true` aborts the previous in-flight call of the same method (per element) when it's called again
|
|
2335
|
+
(search-as-you-type) — the superseded Promise never settles, so a late old response can't
|
|
2168
2336
|
overwrite the newest result.
|
|
2169
|
-
- Aborting through your own signal is not quiet: it
|
|
2170
|
-
or
|
|
2337
|
+
- Aborting through your own signal is not quiet: it rejects the Promise (before mode)
|
|
2338
|
+
or throws (after mode), like any other failure.
|
|
2171
2339
|
|
|
2172
2340
|
**Aliases**
|
|
2173
2341
|
|
|
@@ -2183,13 +2351,13 @@ For the method aliases, `request` may only be a factory returning `RequestInit`
|
|
|
2183
2351
|
|
|
2184
2352
|
```typescript
|
|
2185
2353
|
@fetchManual((self) => self.myService.getProfile({}))
|
|
2186
|
-
|
|
2354
|
+
load(@fetchPromise p: Promise<Profile>) { ... }
|
|
2187
2355
|
|
|
2188
2356
|
@fetchPost('/api/posts', { process: 'json' })
|
|
2189
2357
|
async save() { return { title: 'hi' }; }
|
|
2190
2358
|
|
|
2191
2359
|
@fetchDelete((_self, _helper, params) => `/api/posts/${params[0]}`)
|
|
2192
|
-
|
|
2360
|
+
remove(id: number, @fetchPromise p?: Promise<unknown>) { ... }
|
|
2193
2361
|
```
|
|
2194
2362
|
|
|
2195
2363
|
**Composition** — e.g. form submit:
|
|
@@ -2197,6 +2365,124 @@ async remove(id: number, @fetchSettled settled?: PromiseSettledResult<unknown>)
|
|
|
2197
2365
|
stacked on `@fetch({ url: '/api/echo', trigger: 'after', process: 'form' })`, with the method taking
|
|
2198
2366
|
`@eventBeforeReturn formData: FormData` and returning it.
|
|
2199
2367
|
|
|
2368
|
+
## 🧩 Value-Injecting Parameter Decorators (@querySelectorParam, @attributeParam, @local/session/cookie/indexedDbParam, @fetchParam, @swcAppRouter)
|
|
2369
|
+
|
|
2370
|
+
Unlike `@hostSet`/`@helperSet`/`@helperHostSet` (which just hand you context objects), these decorators actively **go compute or fetch a value** and inject it — once, at the moment the method's trigger fires (`@onConnectedAfter` and friends, `@addEventListener`, `@setInterval`/`@setTimeout`, the observer decorators, `@eventMedia`, route/message subscribers, ...). They only work inside such auto-triggered methods — a plain method you call yourself gets `undefined` for these parameters, since nothing builds the lookup for it.
|
|
2371
|
+
|
|
2372
|
+
> Unlike `@query`/`@queryAll` (lazy — re-queries the DOM on every read), these resolve **once per call**, as a plain captured value, not a live reference. If the DOM/storage changes after the method runs, the parameter you already received does not change.
|
|
2373
|
+
|
|
2374
|
+
### @querySelectorParam / @querySelectorAllParam
|
|
2375
|
+
Same engine as the field decorators `@query`/`@queryAll` (`root`, `pick`, `filter`, function selectors all supported) — named "querySelector" instead of "query" so it doesn't read like a URL query parameter.
|
|
2376
|
+
|
|
2377
|
+
```typescript
|
|
2378
|
+
@onConnectedAfter
|
|
2379
|
+
onReady(
|
|
2380
|
+
@querySelectorParam('#name') input: HTMLInputElement,
|
|
2381
|
+
@querySelectorAllParam('.item', { root: 'all' }) items: HTMLElement[]
|
|
2382
|
+
) {
|
|
2383
|
+
input.focus();
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
@addEventListener('#save', 'click')
|
|
2387
|
+
onSave(@querySelectorShadowParam('.form') form: HTMLFormElement) { ... }
|
|
2388
|
+
```
|
|
2389
|
+
- Root/pick shorthands mirror `@query`'s own family: `querySelectorShadowParam` / `querySelectorLightParam` / `querySelectorAllRootsParam` (single), `querySelectorAllShadowParam` / `querySelectorAllLightParam` / `querySelectorAllAllParam` (arrays).
|
|
2390
|
+
|
|
2391
|
+
### @attributeParam
|
|
2392
|
+
Same engine as the field/method `@attribute`.
|
|
2393
|
+
|
|
2394
|
+
```typescript
|
|
2395
|
+
onReady(
|
|
2396
|
+
@attributeParam('data-x') ownAttr: string,
|
|
2397
|
+
@attributeParam('#child', 'data-y', { root: 'shadow' }) childAttr: string
|
|
2398
|
+
) { ... }
|
|
2399
|
+
```
|
|
2400
|
+
- One string = this element's own attribute; `(selector, name, options?)` = another element's attribute (same two-form shape as the field decorator).
|
|
2401
|
+
- `options.type` converts to `Number`/`Boolean`, same as the field decorator; missing attribute/target → `null`.
|
|
2402
|
+
|
|
2403
|
+
### @localStorageParam / @sessionStorageParam / @cookieParam / @indexedDbParam
|
|
2404
|
+
Inject a persisted value by key (see "Persisted State" below for the backing storage). `@indexedDbParam` is async, so it injects a **Promise** rather than the resolved value — the surrounding resolution has to stay synchronous:
|
|
2405
|
+
|
|
2406
|
+
```typescript
|
|
2407
|
+
onReady(
|
|
2408
|
+
@localStorageParam('theme') theme: string,
|
|
2409
|
+
@indexedDbParam('draft') draftPromise: Promise<string>
|
|
2410
|
+
) {
|
|
2411
|
+
this.applyTheme(theme);
|
|
2412
|
+
draftPromise.then(d => this.restoreDraft(d));
|
|
2413
|
+
}
|
|
2414
|
+
```
|
|
2415
|
+
|
|
2416
|
+
### @fetchParam
|
|
2417
|
+
A minimal fetch — no abort-on-disconnect, no `abortPrevious`, no `filter` (reach for `@fetch`/`@fetchManual` + `@fetchPromise` when you need those). It injects a **Promise** that resolves to the parsed JSON/text body, or rejects with an `Error` on a non-OK status.
|
|
2418
|
+
|
|
2419
|
+
```typescript
|
|
2420
|
+
@onConnectedAfter
|
|
2421
|
+
async onReady(@fetchParam('/api/products/1') p: Promise<Product>) {
|
|
2422
|
+
this.product = await p;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
// url/init accept (currentThis, helper) => ... callbacks too, same shape as @fetch's factories —
|
|
2426
|
+
// handy for referencing instance fields to build a dynamic URL or body.
|
|
2427
|
+
@addEventListener('#save', 'click')
|
|
2428
|
+
async onSave(@fetchParam(
|
|
2429
|
+
(self) => `/api/products/${self.productId}`,
|
|
2430
|
+
(self) => ({ method: 'POST', body: JSON.stringify({ qty: self.qty }) })
|
|
2431
|
+
) p: Promise<any>) {
|
|
2432
|
+
await p;
|
|
2433
|
+
}
|
|
2434
|
+
```
|
|
2435
|
+
|
|
2436
|
+
### @swcAppRouter / @swcAppSimpleApplication / @swcAppHost
|
|
2437
|
+
Documented above alongside `@hostSet`/`@helperSet` — these just hand you a context reference (the `Router`, the DI container, or the `SwcApp` host element itself), they don't compute anything the way the rest of this section's decorators do.
|
|
2438
|
+
|
|
2439
|
+
## 💾 Persisted State (@localStorage, @sessionStorage, @cookie, @indexedDb, @persistState)
|
|
2440
|
+
|
|
2441
|
+
Field or method decorator that mirrors a value to a storage backend. Survives disconnect/reconnect and page reloads — the whole point is that **element lifecycle never clears it**; only you do (see "Removing a key" below).
|
|
2442
|
+
|
|
2443
|
+
```typescript
|
|
2444
|
+
class Settings extends HTMLElement {
|
|
2445
|
+
// Field — bare key defaults to the field name; the stored value wins over the initializer on reconnect
|
|
2446
|
+
@localStorage theme: string = 'dark';
|
|
2447
|
+
|
|
2448
|
+
// Explicit key
|
|
2449
|
+
@sessionStorage('draft-id') draftId?: string;
|
|
2450
|
+
|
|
2451
|
+
@cookie('session-token', { maxAge: 3600, sameSite: 'Strict' })
|
|
2452
|
+
token?: string;
|
|
2453
|
+
|
|
2454
|
+
// Method — persists the return value (JSON-serialized) each time it runs
|
|
2455
|
+
@localStorage('last-search')
|
|
2456
|
+
search(q: string) { return q; }
|
|
2457
|
+
}
|
|
2458
|
+
```
|
|
2459
|
+
|
|
2460
|
+
- **Field**: getter/setter. Reading re-hydrates from storage on first access; assigning writes through. Assigning `undefined` **removes** the key (and clears the in-memory value too).
|
|
2461
|
+
- **Method**: the return value is persisted after the method runs (`valueKey` picks one field out of an object return).
|
|
2462
|
+
- Backends: `local` / `session` / `cookie` (sync) and `indexeddb` (async). The generic form is `@persistState(key?, { storage, ... })`; `@localStorage`/`@sessionStorage`/`@cookie`/`@indexedDb` are storage-fixed aliases.
|
|
2463
|
+
- `@indexedDb` on a field: reading can't `await`, so the first read returns the initializer immediately and the stored value lands right after (a console warning flags this once per class) — prefer calling `persistReadAsync` directly, or a `Promise`-typed field, for async backends.
|
|
2464
|
+
- Options: `valueKey`, `serialize`/`deserialize` (default `JSON.stringify`/`JSON.parse`); `cookie` also takes `path`/`domain`/`maxAge`/`expires`/`secure`/`sameSite`.
|
|
2465
|
+
|
|
2466
|
+
**Removing a key**
|
|
2467
|
+
```typescript
|
|
2468
|
+
class Settings extends HTMLElement {
|
|
2469
|
+
@localStorage theme: string = 'dark';
|
|
2470
|
+
|
|
2471
|
+
@removeLocalStorage('theme')
|
|
2472
|
+
clearTheme() {}
|
|
2473
|
+
}
|
|
2474
|
+
```
|
|
2475
|
+
- `@removeLocalStorage` / `@removeSessionStorage` / `@removeCookie` / `@removeIndexedDb` (method-only) — call them, or just assign `undefined` to the field directly (`this.theme = undefined`), which does the same thing and also clears the live value.
|
|
2476
|
+
- Generic form: `@removePersistState(key?, options?)`.
|
|
2477
|
+
|
|
2478
|
+
**Direct function access** (no decorator needed) — for a plain method body, or the async backend:
|
|
2479
|
+
```typescript
|
|
2480
|
+
import { persistRead, persistWrite, persistRemove, persistReadAsync, persistWriteAsync, persistRemoveAsync } from '@dooboostore/simple-web-component';
|
|
2481
|
+
|
|
2482
|
+
const cached = persistRead(this, 'theme', { storage: 'local' });
|
|
2483
|
+
const draft = await persistReadAsync(this, 'draft', { storage: 'indexeddb' });
|
|
2484
|
+
```
|
|
2485
|
+
|
|
2200
2486
|
## 💧 @property Hydration Rules
|
|
2201
2487
|
|
|
2202
2488
|
- Attach bare `@property` (no args) to a field to mark it as an SSR hydration target.
|
|
@@ -920,6 +920,21 @@ __export(addEventListener_exports, {
|
|
|
920
920
|
eventTransitionstartMutationShadow: () => eventTransitionstartMutationShadow,
|
|
921
921
|
eventTransitionstartShadow: () => eventTransitionstartShadow,
|
|
922
922
|
eventTransitionstartWindow: () => eventTransitionstartWindow,
|
|
923
|
+
eventVisibilitychange: () => eventVisibilitychange,
|
|
924
|
+
eventVisibilitychangeAll: () => eventVisibilitychangeAll,
|
|
925
|
+
eventVisibilitychangeAppHost: () => eventVisibilitychangeAppHost,
|
|
926
|
+
eventVisibilitychangeDelegate: () => eventVisibilitychangeDelegate,
|
|
927
|
+
eventVisibilitychangeDelegateAll: () => eventVisibilitychangeDelegateAll,
|
|
928
|
+
eventVisibilitychangeDelegateLight: () => eventVisibilitychangeDelegateLight,
|
|
929
|
+
eventVisibilitychangeDelegateShadow: () => eventVisibilitychangeDelegateShadow,
|
|
930
|
+
eventVisibilitychangeDocument: () => eventVisibilitychangeDocument,
|
|
931
|
+
eventVisibilitychangeLight: () => eventVisibilitychangeLight,
|
|
932
|
+
eventVisibilitychangeMutation: () => eventVisibilitychangeMutation,
|
|
933
|
+
eventVisibilitychangeMutationAll: () => eventVisibilitychangeMutationAll,
|
|
934
|
+
eventVisibilitychangeMutationLight: () => eventVisibilitychangeMutationLight,
|
|
935
|
+
eventVisibilitychangeMutationShadow: () => eventVisibilitychangeMutationShadow,
|
|
936
|
+
eventVisibilitychangeShadow: () => eventVisibilitychangeShadow,
|
|
937
|
+
eventVisibilitychangeWindow: () => eventVisibilitychangeWindow,
|
|
923
938
|
eventWheel: () => eventWheel,
|
|
924
939
|
eventWheelAll: () => eventWheelAll,
|
|
925
940
|
eventWheelAppHost: () => eventWheelAppHost,
|
|
@@ -2005,6 +2020,22 @@ const eventToggleAll = toggleAliases.all;
|
|
|
2005
2020
|
const eventToggleAppHost = toggleAliases.appHost;
|
|
2006
2021
|
const eventToggleWindow = toggleAliases.window;
|
|
2007
2022
|
const eventToggleDocument = toggleAliases.document;
|
|
2023
|
+
const visibilitychangeAliases = makeTypedEventAliases("visibilitychange");
|
|
2024
|
+
const eventVisibilitychange = visibilitychangeAliases.base;
|
|
2025
|
+
const eventVisibilitychangeDelegateLight = visibilitychangeAliases.delegateLight;
|
|
2026
|
+
const eventVisibilitychangeDelegateShadow = visibilitychangeAliases.delegateShadow;
|
|
2027
|
+
const eventVisibilitychangeDelegateAll = visibilitychangeAliases.delegateAll;
|
|
2028
|
+
const eventVisibilitychangeDelegate = visibilitychangeAliases.delegate;
|
|
2029
|
+
const eventVisibilitychangeMutationLight = visibilitychangeAliases.mutationLight;
|
|
2030
|
+
const eventVisibilitychangeMutationShadow = visibilitychangeAliases.mutationShadow;
|
|
2031
|
+
const eventVisibilitychangeMutationAll = visibilitychangeAliases.mutationAll;
|
|
2032
|
+
const eventVisibilitychangeMutation = visibilitychangeAliases.mutation;
|
|
2033
|
+
const eventVisibilitychangeLight = visibilitychangeAliases.light;
|
|
2034
|
+
const eventVisibilitychangeShadow = visibilitychangeAliases.shadow;
|
|
2035
|
+
const eventVisibilitychangeAll = visibilitychangeAliases.all;
|
|
2036
|
+
const eventVisibilitychangeAppHost = visibilitychangeAliases.appHost;
|
|
2037
|
+
const eventVisibilitychangeWindow = visibilitychangeAliases.window;
|
|
2038
|
+
const eventVisibilitychangeDocument = visibilitychangeAliases.document;
|
|
2008
2039
|
const getAddEventListenerMetadata = (target) => {
|
|
2009
2040
|
const constructor = typeof target === "function" ? target : target.constructor;
|
|
2010
2041
|
return import_core.ReflectUtils.findAllMetadata(ADD_EVENT_LISTENER_METADATA_KEY, constructor).flat();
|
|
@@ -2054,16 +2085,12 @@ class EventListenerLifeCycler {
|
|
|
2054
2085
|
if (options.stopPropagation) event2.stopPropagation();
|
|
2055
2086
|
if (options.stopImmediatePropagation) event2.stopImmediatePropagation();
|
|
2056
2087
|
if (options.preventDefault) event2.preventDefault();
|
|
2057
|
-
const
|
|
2058
|
-
const legacyArgs = [event2, { currentHostSet, $matchedElement: matchedElement }, { event: event2, ...
|
|
2059
|
-
const currentHelperSet = import_Utils.SwcUtils.getHelperSet(currentWin);
|
|
2060
|
-
const currentHelperHostSet = { ...currentHelperSet, ...currentHostSet, $this: inst };
|
|
2088
|
+
const common = (0, import_parameter.buildCommonKindValues)(inst, currentWin);
|
|
2089
|
+
const legacyArgs = [event2, { currentHostSet: common.hostSet, $matchedElement: matchedElement }, { event: event2, ...common.hostSet, $el: target, $root: target }];
|
|
2061
2090
|
const buildArgs = (beforeReturn) => (0, import_parameter.buildSwcParameterArgs)(inst, meta.propertyKey, {
|
|
2091
|
+
...common,
|
|
2062
2092
|
eventObject: event2,
|
|
2063
2093
|
matchedElement,
|
|
2064
|
-
hostSet: currentHostSet,
|
|
2065
|
-
helperHostSet: currentHelperHostSet,
|
|
2066
|
-
helperSet: currentHelperSet,
|
|
2067
2094
|
eventBeforeReturn: beforeReturn
|
|
2068
2095
|
}, legacyArgs);
|
|
2069
2096
|
let args = buildArgs(void 0);
|
|
@@ -2199,16 +2226,12 @@ class EventListenerLifeCycler {
|
|
|
2199
2226
|
if (m.options.stopPropagation) event2.stopPropagation();
|
|
2200
2227
|
if (m.options.stopImmediatePropagation) event2.stopImmediatePropagation();
|
|
2201
2228
|
if (m.options.preventDefault) event2.preventDefault();
|
|
2202
|
-
const
|
|
2203
|
-
const legacyArgs = [event2, { ...
|
|
2204
|
-
const helperSetForMatch = import_Utils.SwcUtils.getHelperSet(currentWin);
|
|
2205
|
-
const helperHostSetForMatch = { ...helperSetForMatch, ...hs, $this: inst };
|
|
2229
|
+
const common = (0, import_parameter.buildCommonKindValues)(inst, currentWin);
|
|
2230
|
+
const legacyArgs = [event2, { ...common.hostSet, $matchedElement: matchedEl }, { event: event2, ...common.hostSet, $el: matchedEl, $root: br }];
|
|
2206
2231
|
const buildArgs = (beforeReturn) => (0, import_parameter.buildSwcParameterArgs)(inst, m.propertyKey, {
|
|
2232
|
+
...common,
|
|
2207
2233
|
eventObject: event2,
|
|
2208
2234
|
matchedElement: matchedEl,
|
|
2209
|
-
hostSet: hs,
|
|
2210
|
-
helperHostSet: helperHostSetForMatch,
|
|
2211
|
-
helperSet: helperSetForMatch,
|
|
2212
2235
|
eventBeforeReturn: beforeReturn
|
|
2213
2236
|
}, legacyArgs);
|
|
2214
2237
|
let args = buildArgs(void 0);
|