@mhmo91/schmancy 0.10.10 → 0.10.12

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.
@@ -29,8 +29,8 @@ cart.replace({ items: [], total: 0 })
29
29
  cart.update(d => { d.items.push(item) }) // immer
30
30
  cart.delete('total')
31
31
 
32
- // Subscribe in a $LitElement — direct read auto-tracks via SignalWatcher
33
- class CartView extends $LitElement() {
32
+ // Subscribe in a SchmancyElement — direct read auto-tracks via SignalWatcher
33
+ class CartView extends SchmancyElement {
34
34
  render() { return html`Items: ${cart.value.items.length}` }
35
35
  }
36
36
 
@@ -207,18 +207,18 @@ with zero ceremony.
207
207
 
208
208
  ### (1) Default — direct read inside `render()`
209
209
 
210
- `$LitElement()` composes `SignalWatcher` from `@lit-labs/signals`.
210
+ `SchmancyElement` composes `SignalWatcher` from `@lit-labs/signals`.
211
211
  Every signal read inside `render()` auto-tracks; the host re-renders
212
212
  on change. No decorator, no field, no binding code.
213
213
 
214
214
  ```ts
215
- import { LitElement, html } from 'lit'
215
+ import { html } from 'lit'
216
216
  import { customElement } from 'lit/decorators.js'
217
- import { $LitElement } from '@mixins/index'
218
- import { cart } from './cart.context'
217
+ import { SchmancyElement } from '@mhmo91/schmancy/mixins'
218
+ import { cart } from './cart.state'
219
219
 
220
220
  @customElement('cart-view')
221
- export class CartView extends $LitElement() {
221
+ export class CartView extends SchmancyElement {
222
222
  render() {
223
223
  return html`<span>Items: ${cart.value.items.length}</span>`
224
224
  }
@@ -243,7 +243,7 @@ derived methods, DevTools inspection, or readability:
243
243
  import { observe } from '@mhmo91/schmancy/state'
244
244
 
245
245
  @customElement('cart-view')
246
- export class CartView extends $LitElement() {
246
+ export class CartView extends SchmancyElement {
247
247
  @observe(cart) cart!: CartState
248
248
 
249
249
  onClick() {
@@ -268,12 +268,12 @@ works under the existing tsconfig with no migration.
268
268
 
269
269
  ### (3) `bindState(host, source)` — imperative form
270
270
 
271
- For hosts that aren't `$LitElement` subclasses (rare):
271
+ For hosts that aren't `SchmancyElement` subclasses (rare):
272
272
 
273
273
  ```ts
274
274
  import { bindState } from '@mhmo91/schmancy/state'
275
275
 
276
- class CustomHost extends LitElement { // not a $LitElement subclass
276
+ class CustomHost extends LitElement { // not a SchmancyElement subclass
277
277
  cart = bindState(this, cart)
278
278
  render() {
279
279
  return html`<span>Items: ${this.cart.value.items.length}</span>`
@@ -376,7 +376,7 @@ two side-by-side checkout flows, an `<iframe>`-like wizard, an
376
376
  embedded preview — wrap the subtree in `<schmancy-context>`.
377
377
 
378
378
  ```ts
379
- class App extends $LitElement() {
379
+ class App extends SchmancyElement {
380
380
  render() {
381
381
  return html`
382
382
  <schmancy-context .provides=${[cart]}>
@@ -402,7 +402,7 @@ after an `await fetch(...)` — all of it auto-resolves to the right
402
402
  instance based on tree position.
403
403
 
404
404
  ```ts
405
- class CartView extends $LitElement() {
405
+ class CartView extends SchmancyElement {
406
406
  render() {
407
407
  return html`
408
408
  <button @click=${() => cart.set({ total: 0 })}>Clear</button>
@@ -418,22 +418,42 @@ class CartView extends $LitElement() {
418
418
  }
419
419
  ```
420
420
 
421
- Coverage of the call paths is provided by `SchmancyElement`:
421
+ Coverage of the call paths is provided by `SchmancyElement` and
422
+ `<schmancy-context>`:
422
423
 
423
- - `render()` and every Lit lifecycle hook — wrapped at construction.
424
+ - `render()` and every Lit lifecycle hook — wrapped at construction
425
+ via the `_activeHost.run(host, fn)` stack.
424
426
  - Class methods (`handleAdd`, `handleSubmit`) — wrapped at construction.
425
- - `await` continuations inside class methods — propagated via the
426
- `Promise.then` patch in `state/active-host.ts`.
427
427
  - `addEventListener(type, fn)` on the host — wrapped (and unwrapped on
428
428
  `removeEventListener`).
429
- - Inline arrow handlers in templates (`@click=${() => …}`) resolve
430
- via the `window.event.composedPath()` fallback in
431
- `resolveActiveHost()`.
429
+ - Explicit `.then(...)` continuations off a class-method Promise
430
+ propagated via the `Promise.prototype.then` patch in
431
+ `state/active-host.ts`, which captures the active host at chain time
432
+ and restores it inside the callback.
433
+ - Inline arrow handlers in templates (`@click=${() => …}`) and any
434
+ other DOM event handler attached inside a `<schmancy-context>`
435
+ subtree — resolved via the capture-phase event listener that
436
+ `<schmancy-context>` installs on itself for ~18 common event types.
437
+ The listener publishes the event's target as the active host through
438
+ the `_publishEventHost(node)` slot for the duration of the
439
+ synchronous handler chain (slot self-clears in the next microtask).
440
+
441
+ **Known limitation — native `await` on a native Promise.** V8's await
442
+ optimization (since 7.x) skips the spec-prescribed
443
+ `Promise.resolve(x).then(continuation)` step, so the `Promise.then`
444
+ patch does not see the resumption. Class methods that mutate state
445
+ across an `await` boundary fall back to the module-scoped global, not
446
+ the active-host's isolated copy. To preserve the host across awaits,
447
+ either keep the mutation in the synchronous prelude before the first
448
+ `await`, or chain explicitly with `.then(...)` (which still routes
449
+ through the patched method). A real fix requires either a build-time
450
+ async-function transform or native `AsyncContext.Variable` in the
451
+ runtime.
432
452
 
433
453
  Pure async callbacks with no DOM origin — a websocket `onmessage`,
434
- `setInterval` with no triggering user event — fall through to the
435
- module-scoped global. That is the correct semantic: those callbacks
436
- have no tree position to resolve to.
454
+ `setInterval` with no triggering user event — also fall through to
455
+ the module-scoped global. That is the correct semantic: those
456
+ callbacks have no tree position to resolve to.
437
457
 
438
458
  ### Lifecycle
439
459
 
@@ -29,8 +29,8 @@ cart.replace({ items: [], total: 0 })
29
29
  cart.update(d => { d.items.push(item) }) // immer
30
30
  cart.delete('total')
31
31
 
32
- // Subscribe in a $LitElement — direct read auto-tracks via SignalWatcher
33
- class CartView extends $LitElement() {
32
+ // Subscribe in a SchmancyElement — direct read auto-tracks via SignalWatcher
33
+ class CartView extends SchmancyElement {
34
34
  render() { return html`Items: ${cart.value.items.length}` }
35
35
  }
36
36
 
@@ -207,18 +207,18 @@ with zero ceremony.
207
207
 
208
208
  ### (1) Default — direct read inside `render()`
209
209
 
210
- `$LitElement()` composes `SignalWatcher` from `@lit-labs/signals`.
210
+ `SchmancyElement` composes `SignalWatcher` from `@lit-labs/signals`.
211
211
  Every signal read inside `render()` auto-tracks; the host re-renders
212
212
  on change. No decorator, no field, no binding code.
213
213
 
214
214
  ```ts
215
- import { LitElement, html } from 'lit'
215
+ import { html } from 'lit'
216
216
  import { customElement } from 'lit/decorators.js'
217
- import { $LitElement } from '@mixins/index'
218
- import { cart } from './cart.context'
217
+ import { SchmancyElement } from '@mhmo91/schmancy/mixins'
218
+ import { cart } from './cart.state'
219
219
 
220
220
  @customElement('cart-view')
221
- export class CartView extends $LitElement() {
221
+ export class CartView extends SchmancyElement {
222
222
  render() {
223
223
  return html`<span>Items: ${cart.value.items.length}</span>`
224
224
  }
@@ -243,7 +243,7 @@ derived methods, DevTools inspection, or readability:
243
243
  import { observe } from '@mhmo91/schmancy/state'
244
244
 
245
245
  @customElement('cart-view')
246
- export class CartView extends $LitElement() {
246
+ export class CartView extends SchmancyElement {
247
247
  @observe(cart) cart!: CartState
248
248
 
249
249
  onClick() {
@@ -268,12 +268,12 @@ works under the existing tsconfig with no migration.
268
268
 
269
269
  ### (3) `bindState(host, source)` — imperative form
270
270
 
271
- For hosts that aren't `$LitElement` subclasses (rare):
271
+ For hosts that aren't `SchmancyElement` subclasses (rare):
272
272
 
273
273
  ```ts
274
274
  import { bindState } from '@mhmo91/schmancy/state'
275
275
 
276
- class CustomHost extends LitElement { // not a $LitElement subclass
276
+ class CustomHost extends LitElement { // not a SchmancyElement subclass
277
277
  cart = bindState(this, cart)
278
278
  render() {
279
279
  return html`<span>Items: ${this.cart.value.items.length}</span>`
@@ -376,7 +376,7 @@ two side-by-side checkout flows, an `<iframe>`-like wizard, an
376
376
  embedded preview — wrap the subtree in `<schmancy-context>`.
377
377
 
378
378
  ```ts
379
- class App extends $LitElement() {
379
+ class App extends SchmancyElement {
380
380
  render() {
381
381
  return html`
382
382
  <schmancy-context .provides=${[cart]}>
@@ -402,7 +402,7 @@ after an `await fetch(...)` — all of it auto-resolves to the right
402
402
  instance based on tree position.
403
403
 
404
404
  ```ts
405
- class CartView extends $LitElement() {
405
+ class CartView extends SchmancyElement {
406
406
  render() {
407
407
  return html`
408
408
  <button @click=${() => cart.set({ total: 0 })}>Clear</button>
@@ -418,22 +418,42 @@ class CartView extends $LitElement() {
418
418
  }
419
419
  ```
420
420
 
421
- Coverage of the call paths is provided by `SchmancyElement`:
421
+ Coverage of the call paths is provided by `SchmancyElement` and
422
+ `<schmancy-context>`:
422
423
 
423
- - `render()` and every Lit lifecycle hook — wrapped at construction.
424
+ - `render()` and every Lit lifecycle hook — wrapped at construction
425
+ via the `_activeHost.run(host, fn)` stack.
424
426
  - Class methods (`handleAdd`, `handleSubmit`) — wrapped at construction.
425
- - `await` continuations inside class methods — propagated via the
426
- `Promise.then` patch in `state/active-host.ts`.
427
427
  - `addEventListener(type, fn)` on the host — wrapped (and unwrapped on
428
428
  `removeEventListener`).
429
- - Inline arrow handlers in templates (`@click=${() => …}`) resolve
430
- via the `window.event.composedPath()` fallback in
431
- `resolveActiveHost()`.
429
+ - Explicit `.then(...)` continuations off a class-method Promise
430
+ propagated via the `Promise.prototype.then` patch in
431
+ `state/active-host.ts`, which captures the active host at chain time
432
+ and restores it inside the callback.
433
+ - Inline arrow handlers in templates (`@click=${() => …}`) and any
434
+ other DOM event handler attached inside a `<schmancy-context>`
435
+ subtree — resolved via the capture-phase event listener that
436
+ `<schmancy-context>` installs on itself for ~18 common event types.
437
+ The listener publishes the event's target as the active host through
438
+ the `_publishEventHost(node)` slot for the duration of the
439
+ synchronous handler chain (slot self-clears in the next microtask).
440
+
441
+ **Known limitation — native `await` on a native Promise.** V8's await
442
+ optimization (since 7.x) skips the spec-prescribed
443
+ `Promise.resolve(x).then(continuation)` step, so the `Promise.then`
444
+ patch does not see the resumption. Class methods that mutate state
445
+ across an `await` boundary fall back to the module-scoped global, not
446
+ the active-host's isolated copy. To preserve the host across awaits,
447
+ either keep the mutation in the synchronous prelude before the first
448
+ `await`, or chain explicitly with `.then(...)` (which still routes
449
+ through the patched method). A real fix requires either a build-time
450
+ async-function transform or native `AsyncContext.Variable` in the
451
+ runtime.
432
452
 
433
453
  Pure async callbacks with no DOM origin — a websocket `onmessage`,
434
- `setInterval` with no triggering user event — fall through to the
435
- module-scoped global. That is the correct semantic: those callbacks
436
- have no tree position to resolve to.
454
+ `setInterval` with no triggering user event — also fall through to
455
+ the module-scoped global. That is the correct semantic: those
456
+ callbacks have no tree position to resolve to.
437
457
 
438
458
  ### Lifecycle
439
459