jq79 0.4.18 → 0.5.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/dist/jq79.cjs +12 -12
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.d.ts +1 -0
- package/dist/jq79.global.js +12 -12
- package/dist/jq79.global.js.map +1 -1
- package/dist/jq79.js +12 -12
- package/dist/jq79.js.map +1 -1
- package/package.json +1 -1
- package/src/jq79.ts +64 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jq79",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Mini reactive component library: single-file components, Svelte-style setup scripts, fine-grained proxy reactivity. Single-file build, zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"reactive",
|
package/src/jq79.ts
CHANGED
|
@@ -550,6 +550,11 @@ const renderNestedComponent = (key: string, node: TemplateNode, scope: Record<st
|
|
|
550
550
|
// the assignment would vanish into the comment and compile as a bare read,
|
|
551
551
|
// dropping every update without a word
|
|
552
552
|
const assignment = (expr: string) => `${expr}\n= $value`
|
|
553
|
+
// the models whose expression will never take an update, decided here rather
|
|
554
|
+
// than at update time: an assignment that landed and one that was dropped
|
|
555
|
+
// both evaluate to the value assigned, so the result can't tell them apart -
|
|
556
|
+
// which is what $updateModel's return has to report
|
|
557
|
+
const unassignable = new Set<string>()
|
|
553
558
|
Object.entries(models).forEach(([name, expr]) => {
|
|
554
559
|
const prop = modelProp(name)
|
|
555
560
|
if (props[prop] !== undefined) {
|
|
@@ -559,6 +564,7 @@ const renderNestedComponent = (key: string, node: TemplateNode, scope: Record<st
|
|
|
559
564
|
// an expression that can't be an assignment target is a wiring mistake -
|
|
560
565
|
// say so now, not on the first update that silently goes nowhere
|
|
561
566
|
if (compileExpr(assignment(expr), ["$value"]) === null) {
|
|
567
|
+
unassignable.add(name)
|
|
562
568
|
console.warn(`jq79: ${modelAttr(name)}="${expr}" is not assignable - updates from <${node.tag}> will be dropped`)
|
|
563
569
|
}
|
|
564
570
|
})
|
|
@@ -640,39 +646,37 @@ const renderNestedComponent = (key: string, node: TemplateNode, scope: Record<st
|
|
|
640
646
|
// the content this site wrote inside the tag, before the first render: a
|
|
641
647
|
// <slot> is resolved while rendering, so the map has to be there by then
|
|
642
648
|
if (slots) instance.slots = slots
|
|
643
|
-
// the writeback half of :model -
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
//
|
|
649
|
+
// the writeback half of :model - the function the child's $updateModel
|
|
650
|
+
// calls, handed over before the first render. Not an event: nothing about
|
|
651
|
+
// a parent-child assignment wants a CustomEvent bubbling through the page
|
|
652
|
+
// on every keystroke, and a direct call has no payload shape to get wrong.
|
|
653
|
+
// The name is normalized like the attribute was (kebab->camel; absent
|
|
654
|
+
// means the default model), and a name nothing binds warns: a typo must
|
|
655
|
+
// not be an input that types into the void
|
|
648
656
|
if (Object.keys(models).length) {
|
|
649
657
|
// each mistake is warned once per instance, not once per keystroke: an
|
|
650
|
-
// input
|
|
658
|
+
// input updating a typo'd name would otherwise flood the console on
|
|
651
659
|
// every character typed into it
|
|
652
660
|
const warned = new Set<string>()
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
warned.add(key)
|
|
656
|
-
console.warn(message)
|
|
657
|
-
}
|
|
658
|
-
instance.on("model:update", (_event, payload) => {
|
|
659
|
-
if (payload === null || typeof payload !== "object") {
|
|
660
|
-
warnOnce("payload", `jq79: model:update expects a { name?, value } payload, got ${payload === null ? "null" : typeof payload}`)
|
|
661
|
-
return
|
|
662
|
-
}
|
|
663
|
-
const name = payload.name == null ? "default" : kebabToCamel(String(payload.name))
|
|
661
|
+
instance.modelWriteback = (rawName, value) => {
|
|
662
|
+
const name = rawName == null ? "default" : kebabToCamel(String(rawName))
|
|
664
663
|
const expr = models[name]
|
|
665
664
|
if (expr === undefined) {
|
|
666
|
-
|
|
667
|
-
|
|
665
|
+
if (!warned.has(name)) {
|
|
666
|
+
warned.add(name)
|
|
667
|
+
console.warn(`jq79: <${node.tag}> has no ${modelAttr(name)} - bound: ${Object.keys(models).map(modelAttr).join(", ")}`)
|
|
668
|
+
}
|
|
669
|
+
return false
|
|
668
670
|
}
|
|
669
|
-
|
|
671
|
+
if (unassignable.has(name)) return false // already warned, at wiring time
|
|
672
|
+
// untracked, like the tag handlers: a child updating from its setup
|
|
670
673
|
// script runs inside the parent's *creation* effect, and the reads a
|
|
671
674
|
// path assignment makes (`user` in `user.name = $value`) would land
|
|
672
675
|
// in its deps - donating that effect one wasted (guard-stopped) wake
|
|
673
676
|
// per later write. An imperative writeback is nobody's dependency
|
|
674
|
-
untracked(() => evalExpr(assignment(expr), scope, { $value:
|
|
675
|
-
|
|
677
|
+
untracked(() => evalExpr(assignment(expr), scope, { $value: value }))
|
|
678
|
+
return true
|
|
679
|
+
}
|
|
676
680
|
}
|
|
677
681
|
|
|
678
682
|
// @event on the tag listens to this instance's $emit channel (and only
|
|
@@ -1902,6 +1906,13 @@ export class Component79 {
|
|
|
1902
1906
|
// and every render reads it from here: a hot reload re-renders from a data
|
|
1903
1907
|
// snapshot, which a symbol on the store would not survive
|
|
1904
1908
|
slots?: SlotMap
|
|
1909
|
+
// the writeback half of :model, same story: the function that assigns into
|
|
1910
|
+
// the parent, set by renderNestedComponent before the first render and
|
|
1911
|
+
// called by this instance's $updateModel. Kept outside the render generation
|
|
1912
|
+
// so it survives re-render and hot reload. Absent means no :model on the tag
|
|
1913
|
+
// (or no tag at all - a root mount), which makes every $updateModel a no-op.
|
|
1914
|
+
// Internal: set by the usage site, not part of the public API
|
|
1915
|
+
modelWriteback?: (name: string | undefined, value: any) => boolean
|
|
1905
1916
|
|
|
1906
1917
|
data: ReactiveDeepData<Record<string, any>> | null = null
|
|
1907
1918
|
|
|
@@ -2094,7 +2105,16 @@ export class Component79 {
|
|
|
2094
2105
|
// event is cancelable so preventDefault() - from either channel - flips
|
|
2095
2106
|
// the return to false, telling the emitting child "the parent vetoed"
|
|
2096
2107
|
const marker = this.startMarker
|
|
2108
|
+
// model:update used to be the writeback's event name; it is a direct call
|
|
2109
|
+
// now ($updateModel), so an emit under that name reaches nothing. Said
|
|
2110
|
+
// once per generation rather than per keystroke, and said at all because
|
|
2111
|
+
// the alternative is a child whose edits silently stop arriving
|
|
2112
|
+
let warnedModelUpdate = false
|
|
2097
2113
|
const $emit = (eventName: string, payload?: any): boolean => {
|
|
2114
|
+
if (eventName === "model:update" && !warnedModelUpdate) {
|
|
2115
|
+
warnedModelUpdate = true
|
|
2116
|
+
console.warn("jq79: $emit('model:update', …) no longer feeds :model - call $updateModel(value) or $updateModel(name, value) instead")
|
|
2117
|
+
}
|
|
2098
2118
|
const event = new CustomEvent(eventName, { detail: payload, bubbles: true, composed: true, cancelable: true })
|
|
2099
2119
|
if (marker === this.startMarker) {
|
|
2100
2120
|
this.emitListeners.get(eventName)?.forEach(listener => listener(event, payload))
|
|
@@ -2141,17 +2161,35 @@ export class Component79 {
|
|
|
2141
2161
|
const $import = (url: string): Promise<any> =>
|
|
2142
2162
|
modules && url in modules ? Promise.resolve(modules[url]) : importResource(url)
|
|
2143
2163
|
|
|
2164
|
+
// the writeback half of :model, from the child's side: one argument is the
|
|
2165
|
+
// value for the default model (the bare :model), two are a name and a
|
|
2166
|
+
// value. Arity is what tells them apart, so the value is never inspected -
|
|
2167
|
+
// an object with `name`/`value` keys is just a value, which is exactly
|
|
2168
|
+
// what a payload-shaped contract could not promise. Returns whether a
|
|
2169
|
+
// bound model took it; no :model at the usage site is a silent no-op,
|
|
2170
|
+
// since a child may be designed to work bound or unbound
|
|
2171
|
+
const $updateModel = (...args: [value?: any] | [name: string, value: any]): boolean => {
|
|
2172
|
+
const [name, value] = args.length > 1 ? args as [string, any] : [undefined, args[0]]
|
|
2173
|
+
// the same stale-generation guard $emit has: destroy() nulls the marker,
|
|
2174
|
+
// so a closure the old child leaked (a timer, a registered callback)
|
|
2175
|
+
// cannot keep writing a parent that replaced it
|
|
2176
|
+
if (marker !== this.startMarker) return false
|
|
2177
|
+
return this.modelWriteback?.(name, value) ?? false
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2144
2180
|
// the names a component answers on top of its store: $emit, so an inline
|
|
2145
2181
|
// handler can emit without routing through a setup function
|
|
2146
|
-
// (@input="$emit('update', $event.target.value)"),
|
|
2147
|
-
//
|
|
2148
|
-
//
|
|
2182
|
+
// (@input="$emit('update', $event.target.value)"), $updateModel, the
|
|
2183
|
+
// writeback a :model binding listens for, and $slots, the static map of
|
|
2184
|
+
// the names the usage site filled, so a wrapper can be dropped when
|
|
2185
|
+
// nothing filled it (<footer :if="$slots.footer">). All reach the
|
|
2149
2186
|
// template (through templateScope, below) and both script modes (as
|
|
2150
|
-
// instance helpers), and a same-named store key shadows
|
|
2187
|
+
// instance helpers), and a same-named store key shadows any of them.
|
|
2151
2188
|
// Null-prototype, for the same reason storeApi is: `key in injected` must
|
|
2152
2189
|
// not start answering true for toString, constructor and the rest
|
|
2153
2190
|
const injected: Record<string, any> = Object.assign(Object.create(null), {
|
|
2154
2191
|
$emit,
|
|
2192
|
+
$updateModel,
|
|
2155
2193
|
$slots: Object.fromEntries(Object.keys(this.slots ?? {}).map(name => [name, true])),
|
|
2156
2194
|
})
|
|
2157
2195
|
|