@statorjs/stator 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/package.json +99 -0
- package/src/build/build.ts +244 -0
- package/src/build/head.ts +43 -0
- package/src/build/index.ts +10 -0
- package/src/build/sync.ts +65 -0
- package/src/client/bind.ts +47 -0
- package/src/client/client-id.ts +10 -0
- package/src/client/dispatch.ts +62 -0
- package/src/client/element.ts +156 -0
- package/src/client/index.ts +13 -0
- package/src/client/inspector.ts +237 -0
- package/src/client/machine.ts +39 -0
- package/src/client/runtime.ts +246 -0
- package/src/client/use.ts +119 -0
- package/src/compiler/client-emit.ts +180 -0
- package/src/compiler/client-script.ts +256 -0
- package/src/compiler/compile.ts +459 -0
- package/src/compiler/diagnostics.ts +65 -0
- package/src/compiler/dts.ts +87 -0
- package/src/compiler/hash.ts +8 -0
- package/src/compiler/index.ts +48 -0
- package/src/compiler/lower.ts +0 -0
- package/src/compiler/regions.ts +79 -0
- package/src/compiler/split.ts +168 -0
- package/src/compiler/styles.ts +200 -0
- package/src/compiler/virtual-code.ts +184 -0
- package/src/components/index.ts +2 -0
- package/src/components/json-ld.ts +85 -0
- package/src/engine/actor.ts +248 -0
- package/src/engine/define-machine.ts +113 -0
- package/src/engine/index.ts +37 -0
- package/src/engine/types.ts +236 -0
- package/src/server/api-route.ts +149 -0
- package/src/server/app-dispatch.ts +52 -0
- package/src/server/app-store.ts +35 -0
- package/src/server/cached-store.ts +117 -0
- package/src/server/create-app.ts +83 -0
- package/src/server/define-machine.ts +10 -0
- package/src/server/dev.ts +255 -0
- package/src/server/discovery.ts +111 -0
- package/src/server/dispatch-context.ts +39 -0
- package/src/server/effects.ts +120 -0
- package/src/server/http.ts +475 -0
- package/src/server/index.ts +101 -0
- package/src/server/instance-proxy.ts +95 -0
- package/src/server/logger.ts +52 -0
- package/src/server/machine-store.ts +355 -0
- package/src/server/reads-helpers.ts +40 -0
- package/src/server/recompute.ts +287 -0
- package/src/server/redis-store.ts +116 -0
- package/src/server/render-context.ts +228 -0
- package/src/server/render.ts +111 -0
- package/src/server/route-discovery.ts +226 -0
- package/src/server/route-request.ts +36 -0
- package/src/server/routing.ts +175 -0
- package/src/server/session-lock.ts +33 -0
- package/src/server/session-runtime.ts +242 -0
- package/src/server/session.ts +29 -0
- package/src/server/sse.ts +175 -0
- package/src/server/store.ts +95 -0
- package/src/stator-modules.d.ts +12 -0
- package/src/template/client-shell.ts +65 -0
- package/src/template/conditional.ts +166 -0
- package/src/template/directives/core.ts +58 -0
- package/src/template/directives/list-attr.ts +183 -0
- package/src/template/directives/on.ts +22 -0
- package/src/template/each.ts +295 -0
- package/src/template/html.ts +180 -0
- package/src/template/index.ts +29 -0
- package/src/template/parser.ts +341 -0
- package/src/template/read.ts +50 -0
- package/src/template/types.ts +33 -0
- package/src/vite/index.ts +6 -0
- package/src/vite/plugin.ts +151 -0
- package/src/vite/stub.ts +79 -0
- package/src/wire/apply.ts +103 -0
- package/src/wire/index.ts +67 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
allocSlotId,
|
|
3
|
+
type ErasedSelector,
|
|
4
|
+
keyToken,
|
|
5
|
+
popListScope,
|
|
6
|
+
type RenderState,
|
|
7
|
+
registerBinding,
|
|
8
|
+
requireCurrentRenderState,
|
|
9
|
+
type SlotId,
|
|
10
|
+
unregisterBindingsForScope,
|
|
11
|
+
} from '../server/render-context.ts'
|
|
12
|
+
import { isReadResult, type ReadResult } from './read.ts'
|
|
13
|
+
import { type HtmlFragment, isHtmlFragment } from './types.ts'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A conditional fragment. Same wire shape as EachResult — a position-marker
|
|
17
|
+
* span whose innerHTML is swapped on key change. When inactive, the span has
|
|
18
|
+
* no inner content, so the conditional body's DOM is genuinely absent (not
|
|
19
|
+
* hidden via CSS).
|
|
20
|
+
*/
|
|
21
|
+
export interface BranchResult {
|
|
22
|
+
readonly __isBranchResult: true
|
|
23
|
+
readonly html: string
|
|
24
|
+
readonly slotId: SlotId
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isBranchResult(v: unknown): v is BranchResult {
|
|
28
|
+
return (
|
|
29
|
+
typeof v === 'object' && v !== null && (v as Record<string, unknown>).__isBranchResult === true
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Render the body for a branch result. Mirrors renderListBody: clears any
|
|
35
|
+
* existing descendant bindings under this slot, pushes a child scope, and
|
|
36
|
+
* runs the renderer. Returns the inner HTML (empty when renderer is null).
|
|
37
|
+
*
|
|
38
|
+
* Descendant slot ids are scoped by the ARM (`s2:btrue:s0`,
|
|
39
|
+
* `s2:bconfirmed:s0`) — the branch analogue of keyed rows' key scoping. Two
|
|
40
|
+
* arms of one branch never share slot ids, so a patch computed for one arm
|
|
41
|
+
* can never land inside the other's DOM (the stale-page hazard: a non-live
|
|
42
|
+
* page showing arm A while the server diffs arm B must skip, not miswrite).
|
|
43
|
+
*/
|
|
44
|
+
export function renderBranchBody(
|
|
45
|
+
state: RenderState,
|
|
46
|
+
slotId: SlotId,
|
|
47
|
+
armKey: unknown,
|
|
48
|
+
renderer: (() => HtmlFragment) | null,
|
|
49
|
+
): string {
|
|
50
|
+
unregisterBindingsForScope(state, slotId)
|
|
51
|
+
if (!renderer) return ''
|
|
52
|
+
state.scopeStack.push({ prefix: `${slotId}:b${keyToken(String(armKey))}`, counter: 0 })
|
|
53
|
+
try {
|
|
54
|
+
const fragment = renderer()
|
|
55
|
+
if (!isHtmlFragment(fragment)) {
|
|
56
|
+
throw new Error('stator: when()/match() renderer must return an html`...` result')
|
|
57
|
+
}
|
|
58
|
+
return fragment.html
|
|
59
|
+
} finally {
|
|
60
|
+
popListScope(state)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Render `fn()` when `cond` is truthy; otherwise render nothing. Re-renders
|
|
66
|
+
* only when the truthiness of `cond` flips — toggling between two truthy
|
|
67
|
+
* values does not cause a swap.
|
|
68
|
+
*/
|
|
69
|
+
export function when<T>(cond: T | ReadResult<T>, fn: () => HtmlFragment): BranchResult {
|
|
70
|
+
const state = requireCurrentRenderState()
|
|
71
|
+
|
|
72
|
+
let value: T
|
|
73
|
+
let slotId: SlotId
|
|
74
|
+
let machineName: string | null = null
|
|
75
|
+
let selector: ErasedSelector | null = null
|
|
76
|
+
|
|
77
|
+
if (isReadResult(cond)) {
|
|
78
|
+
value = cond.value as T
|
|
79
|
+
slotId = cond.slotId
|
|
80
|
+
machineName = cond.machineName
|
|
81
|
+
selector = cond.selector
|
|
82
|
+
} else {
|
|
83
|
+
value = cond
|
|
84
|
+
slotId = allocSlotId(state)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const keyFn = (v: unknown): boolean => !!v
|
|
88
|
+
const renderFn = (v: unknown): (() => HtmlFragment) | null => (v ? fn : null)
|
|
89
|
+
|
|
90
|
+
const activeKey = keyFn(value)
|
|
91
|
+
const innerHtml = renderBranchBody(state, slotId, activeKey, renderFn(value))
|
|
92
|
+
|
|
93
|
+
if (machineName && selector) {
|
|
94
|
+
registerBinding(state, {
|
|
95
|
+
slotId,
|
|
96
|
+
machineName,
|
|
97
|
+
selector,
|
|
98
|
+
lastValue: value,
|
|
99
|
+
kind: 'branch',
|
|
100
|
+
branchKeyFn: keyFn,
|
|
101
|
+
branchRenderFn: renderFn,
|
|
102
|
+
lastBranchKey: activeKey,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// `display: contents` so the wrapper span doesn't break the surrounding
|
|
107
|
+
// element's layout (grid/flex) — see each.ts for the same rationale.
|
|
108
|
+
const html = `<span data-slot="${slotId}" data-branch="true" style="display:contents">${innerHtml}</span>`
|
|
109
|
+
return { __isBranchResult: true, html, slotId }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Render the body for the matching case key, or nothing if no case matches.
|
|
114
|
+
* Re-renders only when the key changes — toggling other context fields
|
|
115
|
+
* doesn't cause a swap.
|
|
116
|
+
*
|
|
117
|
+
* Type-safe when `key` is a `ReadResult<TKey extends string>`: cases must be
|
|
118
|
+
* keyed by the same literal union.
|
|
119
|
+
*/
|
|
120
|
+
export function match<TKey extends string>(
|
|
121
|
+
key: TKey | ReadResult<TKey>,
|
|
122
|
+
cases: Partial<Record<TKey, () => HtmlFragment>>,
|
|
123
|
+
): BranchResult {
|
|
124
|
+
const state = requireCurrentRenderState()
|
|
125
|
+
|
|
126
|
+
let value: TKey
|
|
127
|
+
let slotId: SlotId
|
|
128
|
+
let machineName: string | null = null
|
|
129
|
+
let selector: ErasedSelector | null = null
|
|
130
|
+
|
|
131
|
+
if (isReadResult(key)) {
|
|
132
|
+
value = key.value as TKey
|
|
133
|
+
slotId = key.slotId
|
|
134
|
+
machineName = key.machineName
|
|
135
|
+
selector = key.selector
|
|
136
|
+
} else {
|
|
137
|
+
value = key
|
|
138
|
+
slotId = allocSlotId(state)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const keyFn = (v: unknown): unknown => v
|
|
142
|
+
const renderFn = (v: unknown): (() => HtmlFragment) | null => {
|
|
143
|
+
const fn = (cases as Record<string, (() => HtmlFragment) | undefined>)[String(v)]
|
|
144
|
+
return fn ?? null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const innerHtml = renderBranchBody(state, slotId, keyFn(value), renderFn(value))
|
|
148
|
+
|
|
149
|
+
if (machineName && selector) {
|
|
150
|
+
registerBinding(state, {
|
|
151
|
+
slotId,
|
|
152
|
+
machineName,
|
|
153
|
+
selector,
|
|
154
|
+
lastValue: value,
|
|
155
|
+
kind: 'branch',
|
|
156
|
+
branchKeyFn: keyFn,
|
|
157
|
+
branchRenderFn: renderFn,
|
|
158
|
+
lastBranchKey: keyFn(value),
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// `display: contents` so the wrapper span doesn't break the surrounding
|
|
163
|
+
// element's layout (grid/flex) — see each.ts for the same rationale.
|
|
164
|
+
const html = `<span data-slot="${slotId}" data-branch="true" style="display:contents">${innerHtml}</span>`
|
|
165
|
+
return { __isBranchResult: true, html, slotId }
|
|
166
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ElementId } from '../../server/render-context.ts'
|
|
2
|
+
|
|
3
|
+
export interface DirectiveContext<TArg> {
|
|
4
|
+
/** The synthetic id of the element this directive is attached to. */
|
|
5
|
+
elementId: ElementId
|
|
6
|
+
/** The modifier portion of the directive, e.g. 'click' in `on('click', ...)`. */
|
|
7
|
+
modifier: string
|
|
8
|
+
/** The argument passed to the directive, e.g. the handler function. */
|
|
9
|
+
arg: TArg
|
|
10
|
+
/** Adds an attribute to the element (e.g. `data-event-click="..."`). */
|
|
11
|
+
addAttribute(name: string, value: string): void
|
|
12
|
+
/**
|
|
13
|
+
* Records a cleanup function. Unused server-side in the POC; reserved
|
|
14
|
+
* for V1 directives that bind server-side timers / SSE.
|
|
15
|
+
*/
|
|
16
|
+
registerCleanup(fn: () => void): void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DirectiveDefinition<TArg> {
|
|
20
|
+
name: string
|
|
21
|
+
apply(ctx: DirectiveContext<TArg>): void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Directive<TArg> extends DirectiveDefinition<TArg> {
|
|
25
|
+
readonly __isStatorDirective: true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function defineDirective<TArg>(def: DirectiveDefinition<TArg>): Directive<TArg> {
|
|
29
|
+
return { ...def, __isStatorDirective: true }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DirectiveInvocation<TArg = unknown> {
|
|
33
|
+
readonly __isStatorDirectiveInvocation: true
|
|
34
|
+
directive: Directive<TArg>
|
|
35
|
+
modifier: string
|
|
36
|
+
arg: TArg
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isDirectiveInvocation(v: unknown): v is DirectiveInvocation {
|
|
40
|
+
return (
|
|
41
|
+
typeof v === 'object' &&
|
|
42
|
+
v !== null &&
|
|
43
|
+
(v as Record<string, unknown>).__isStatorDirectiveInvocation === true
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function invoke<TArg>(
|
|
48
|
+
directive: Directive<TArg>,
|
|
49
|
+
modifier: string,
|
|
50
|
+
arg: TArg,
|
|
51
|
+
): DirectiveInvocation<TArg> {
|
|
52
|
+
return {
|
|
53
|
+
__isStatorDirectiveInvocation: true,
|
|
54
|
+
directive,
|
|
55
|
+
modifier,
|
|
56
|
+
arg,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import {
|
|
2
|
+
allocSlotId,
|
|
3
|
+
type RenderState,
|
|
4
|
+
registerBinding,
|
|
5
|
+
requireCurrentRenderState,
|
|
6
|
+
} from '../../server/render-context.ts'
|
|
7
|
+
import { isReadResult, type ReadResult } from '../read.ts'
|
|
8
|
+
import { type DirectiveInvocation, defineDirective, invoke } from './core.ts'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Shared shape for compound-attribute directives (`class:list`, `style:list`).
|
|
12
|
+
* Each directive owns an entire attribute. Conditional reads appear *inside*
|
|
13
|
+
* the spec, never alongside literal text — that rule is enforced by html.ts.
|
|
14
|
+
*
|
|
15
|
+
* The directive registers one attr binding per unique machine appearing in
|
|
16
|
+
* the spec; each binding's selector recomposes the full attribute value, so
|
|
17
|
+
* any machine event that affects the spec emits exactly one patch for the
|
|
18
|
+
* attribute (and the value always includes every static entry).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
type ConditionalEntry<TValue> = TValue | ReadResult<TValue>
|
|
22
|
+
|
|
23
|
+
/** Recursive shape accepted by class:list. */
|
|
24
|
+
export type ClassListSpec =
|
|
25
|
+
| string
|
|
26
|
+
| false
|
|
27
|
+
| null
|
|
28
|
+
| undefined
|
|
29
|
+
| ReadResult<string | boolean | null | undefined>
|
|
30
|
+
| Record<string, ConditionalEntry<boolean | null | undefined>>
|
|
31
|
+
| ClassListSpec[]
|
|
32
|
+
|
|
33
|
+
/** Recursive shape accepted by style:list. */
|
|
34
|
+
export type StyleListSpec =
|
|
35
|
+
| string
|
|
36
|
+
| false
|
|
37
|
+
| null
|
|
38
|
+
| undefined
|
|
39
|
+
| ReadResult<string | null | undefined>
|
|
40
|
+
| Record<string, ConditionalEntry<string | number | null | undefined>>
|
|
41
|
+
| StyleListSpec[]
|
|
42
|
+
|
|
43
|
+
function evalRead<T>(value: ConditionalEntry<T>): T {
|
|
44
|
+
return isReadResult(value) ? (value.selector(value.instance) as T) : value
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function collectMachines(spec: unknown, out: Set<string>): void {
|
|
48
|
+
if (spec == null || spec === false) return
|
|
49
|
+
if (typeof spec === 'string') return
|
|
50
|
+
if (isReadResult(spec)) {
|
|
51
|
+
out.add(spec.machineName)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(spec)) {
|
|
55
|
+
for (const item of spec) collectMachines(item, out)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
if (typeof spec === 'object') {
|
|
59
|
+
for (const v of Object.values(spec)) {
|
|
60
|
+
if (isReadResult(v)) out.add(v.machineName)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function composeClass(spec: ClassListSpec): string {
|
|
66
|
+
const parts: string[] = []
|
|
67
|
+
walkClass(spec, parts)
|
|
68
|
+
return parts.join(' ')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function walkClass(spec: ClassListSpec, out: string[]): void {
|
|
72
|
+
if (spec == null || spec === false) return
|
|
73
|
+
if (typeof spec === 'string') {
|
|
74
|
+
if (spec.length > 0) out.push(spec)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
if (isReadResult(spec)) {
|
|
78
|
+
const v = spec.selector(spec.instance)
|
|
79
|
+
if (typeof v === 'string' && v.length > 0) out.push(v)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(spec)) {
|
|
83
|
+
for (const item of spec) walkClass(item, out)
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
if (typeof spec === 'object') {
|
|
87
|
+
for (const [name, cond] of Object.entries(spec)) {
|
|
88
|
+
const truthy = evalRead(cond)
|
|
89
|
+
if (truthy) out.push(name)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function composeStyle(spec: StyleListSpec): string {
|
|
95
|
+
const decls: string[] = []
|
|
96
|
+
walkStyle(spec, decls)
|
|
97
|
+
return decls.join('; ')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function walkStyle(spec: StyleListSpec, out: string[]): void {
|
|
101
|
+
if (spec == null || spec === false) return
|
|
102
|
+
if (typeof spec === 'string') {
|
|
103
|
+
const trimmed = spec.trim()
|
|
104
|
+
if (trimmed.length > 0) out.push(trimmed.replace(/;\s*$/, ''))
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
if (isReadResult(spec)) {
|
|
108
|
+
const v = spec.selector(spec.instance)
|
|
109
|
+
if (typeof v === 'string' && v.trim().length > 0) {
|
|
110
|
+
out.push(v.trim().replace(/;\s*$/, ''))
|
|
111
|
+
}
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
if (Array.isArray(spec)) {
|
|
115
|
+
for (const item of spec) walkStyle(item, out)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
if (typeof spec === 'object') {
|
|
119
|
+
for (const [prop, raw] of Object.entries(spec)) {
|
|
120
|
+
const value = evalRead(raw)
|
|
121
|
+
if (value == null || value === '') continue
|
|
122
|
+
out.push(`${prop}: ${value}`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Register one attr-binding per unique machine the spec depends on. Each
|
|
129
|
+
* binding's selector ignores its arg and recomputes the entire attribute
|
|
130
|
+
* by re-walking the spec — every ReadResult inside re-evaluates its
|
|
131
|
+
* `.selector(.instance)`, returning fresh state.
|
|
132
|
+
*/
|
|
133
|
+
function bindListAttr<TSpec>(
|
|
134
|
+
state: RenderState,
|
|
135
|
+
spec: TSpec,
|
|
136
|
+
attrName: string,
|
|
137
|
+
elementId: string,
|
|
138
|
+
compose: (spec: TSpec) => string,
|
|
139
|
+
): string {
|
|
140
|
+
const machines = new Set<string>()
|
|
141
|
+
collectMachines(spec, machines)
|
|
142
|
+
|
|
143
|
+
const initial = compose(spec)
|
|
144
|
+
for (const machineName of machines) {
|
|
145
|
+
const slotId = allocSlotId(state)
|
|
146
|
+
registerBinding(state, {
|
|
147
|
+
slotId,
|
|
148
|
+
machineName,
|
|
149
|
+
selector: () => compose(spec),
|
|
150
|
+
lastValue: initial,
|
|
151
|
+
kind: 'attr',
|
|
152
|
+
attrName,
|
|
153
|
+
parentId: elementId,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
return initial
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const classListDirective = defineDirective<ClassListSpec>({
|
|
160
|
+
name: 'class:list',
|
|
161
|
+
apply({ elementId, arg, addAttribute }) {
|
|
162
|
+
const state = requireCurrentRenderState()
|
|
163
|
+
const initial = bindListAttr(state, arg, 'class', elementId, composeClass)
|
|
164
|
+
addAttribute('class', initial)
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const styleListDirective = defineDirective<StyleListSpec>({
|
|
169
|
+
name: 'style:list',
|
|
170
|
+
apply({ elementId, arg, addAttribute }) {
|
|
171
|
+
const state = requireCurrentRenderState()
|
|
172
|
+
const initial = bindListAttr(state, arg, 'style', elementId, composeStyle)
|
|
173
|
+
addAttribute('style', initial)
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
export function classList(spec: ClassListSpec): DirectiveInvocation<ClassListSpec> {
|
|
178
|
+
return invoke(classListDirective, '', spec)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function styleList(spec: StyleListSpec): DirectiveInvocation<StyleListSpec> {
|
|
182
|
+
return invoke(styleListDirective, '', spec)
|
|
183
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type EventDescriptor, isEventDescriptor } from '../../server/render-context.ts'
|
|
2
|
+
import { type DirectiveInvocation, defineDirective, invoke } from './core.ts'
|
|
3
|
+
|
|
4
|
+
type Handler = () => EventDescriptor | undefined
|
|
5
|
+
|
|
6
|
+
const onDirective = defineDirective<Handler>({
|
|
7
|
+
name: 'on',
|
|
8
|
+
apply({ modifier, arg, addAttribute }) {
|
|
9
|
+
const result = arg()
|
|
10
|
+
if (!isEventDescriptor(result)) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`stator: on('${modifier}', ...) handler must be exactly one machine.send(...) call. ` +
|
|
13
|
+
`Multi-statement handlers and arbitrary side effects are not supported in the POC.`,
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
addAttribute(`data-event-${modifier}`, JSON.stringify(result))
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export function on(modifier: string, handler: Handler): DirectiveInvocation<Handler> {
|
|
21
|
+
return invoke(onDirective, modifier, handler)
|
|
22
|
+
}
|