jq79 0.6.3 → 0.6.4
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 +13 -13
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.d.ts +1 -0
- package/dist/jq79.global.js +13 -13
- package/dist/jq79.global.js.map +1 -1
- package/dist/jq79.js +13 -13
- package/dist/jq79.js.map +1 -1
- package/package.json +2 -1
- package/src/jq79.ts +175 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jq79",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"benchmark": "node scripts/run-benchmark.mjs",
|
|
68
68
|
"benchmark:ab": "node scripts/run-ab.mjs",
|
|
69
69
|
"benchmark:comparison": "node scripts/run-comparison.mjs",
|
|
70
|
+
"benchmark:once": "node scripts/run-once-render.mjs",
|
|
70
71
|
"prepublishOnly": "npm test && npm run build"
|
|
71
72
|
},
|
|
72
73
|
"peerDependencies": {
|
package/src/jq79.ts
CHANGED
|
@@ -25,6 +25,13 @@ type TemplateNode = {
|
|
|
25
25
|
// parse (see stampComponentTag) and lifted off attrs here, where it stops
|
|
26
26
|
// looking like an attribute to every loop downstream
|
|
27
27
|
component?: string
|
|
28
|
+
// the element's namespace, present only when it is NOT HTML - an <svg>
|
|
29
|
+
// subtree, or MathML. Read straight off the parsed tree, because the HTML
|
|
30
|
+
// parser has already run the foreign-content algorithm over it and knows
|
|
31
|
+
// things a tag name cannot say: whether this <title> is SVG's or HTML's, and
|
|
32
|
+
// where a <foreignObject> hands the namespace back. Absent is the common
|
|
33
|
+
// case and means HTML, so an ordinary node is exactly the shape it was
|
|
34
|
+
ns?: string
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
type TagBlock = {
|
|
@@ -51,17 +58,25 @@ const elementAttrs = (el: Element): Record<string, string> =>
|
|
|
51
58
|
// they are not in the AST at all - which is where slot content is written
|
|
52
59
|
// (<template :slot.name>), and why a nested <template> used to render as an
|
|
53
60
|
// empty element whatever was inside it
|
|
61
|
+
const HTML_NS = "http://www.w3.org/1999/xhtml"
|
|
62
|
+
|
|
54
63
|
const elementToAST = (el: Element): TemplateNode => {
|
|
55
64
|
const attrs = elementAttrs(el)
|
|
65
|
+
// `tagName` is uppercase for HTML and as-authored for everything else, so the
|
|
66
|
+
// lowercasing that normalizes <DIV> would destroy <clipPath>, <linearGradient>
|
|
67
|
+
// and <feGaussianBlur>, whose names are case-sensitive
|
|
68
|
+
const ns = el.namespaceURI
|
|
56
69
|
// the pre-parse stamp becomes a field and leaves attrs entirely: it is not a
|
|
57
70
|
// prop, not a directive and not an attribute, and every loop that walks attrs
|
|
58
71
|
// would otherwise need to know its name
|
|
59
72
|
const component = attrs[COMPONENT_TAG_ATTR]
|
|
60
73
|
delete attrs[COMPONENT_TAG_ATTR]
|
|
74
|
+
const foreign = ns !== null && ns !== HTML_NS
|
|
61
75
|
return {
|
|
62
|
-
tag: el.tagName.toLowerCase(),
|
|
76
|
+
tag: foreign ? el.tagName : el.tagName.toLowerCase(),
|
|
63
77
|
attrs,
|
|
64
78
|
...(component === undefined ? {} : { component }),
|
|
79
|
+
...(foreign ? { ns } : {}),
|
|
65
80
|
children: Array.from((el instanceof HTMLTemplateElement ? el.content : el).childNodes).flatMap((node): (TemplateNode | string)[] => {
|
|
66
81
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
67
82
|
const text = node.textContent ?? ""
|
|
@@ -1194,6 +1209,14 @@ const BOOLEAN_ATTRS = new Set([
|
|
|
1194
1209
|
// browser doesn't have - and `readonly`/`novalidate`/`ismap` reflect under
|
|
1195
1210
|
// camelCase property names no kebab->camel pass can produce, failing toward
|
|
1196
1211
|
// `readonly="false"`, which is read-only
|
|
1212
|
+
// the one place an element is built, so the interpreted path and the cloner
|
|
1213
|
+
// cannot disagree about what a tag means. `ns` is set only for a foreign
|
|
1214
|
+
// element (see TemplateNode) - and creating one in its own namespace is what
|
|
1215
|
+
// makes `viewBox` keep its case, because setAttribute only lowercases a
|
|
1216
|
+
// qualified name on an HTML element
|
|
1217
|
+
const createFor = (node: TemplateNode): Element =>
|
|
1218
|
+
node.ns === undefined ? document.createElement(node.tag) : document.createElementNS(node.ns, node.tag)
|
|
1219
|
+
|
|
1197
1220
|
const applyAttr = (el: Element, name: string, value: any) => {
|
|
1198
1221
|
const boolean = BOOLEAN_ATTRS.has(name)
|
|
1199
1222
|
if (boolean ? !value : value == null) el.removeAttribute(name)
|
|
@@ -1226,17 +1249,26 @@ const applyAttr = (el: Element, name: string, value: any) => {
|
|
|
1226
1249
|
// Two rules keep this from becoming the bug it could be:
|
|
1227
1250
|
//
|
|
1228
1251
|
// 1. **The holes are an allowlist, never a denylist.** `plannableAttr` names
|
|
1229
|
-
//
|
|
1230
|
-
//
|
|
1231
|
-
//
|
|
1232
|
-
//
|
|
1252
|
+
// what a skeleton knows how to fill; every other attribute makes the subtree
|
|
1253
|
+
// unplannable. So a directive added to renderNode later is *slower* until
|
|
1254
|
+
// somebody teaches it here - never silently mis-rendered, which is the
|
|
1255
|
+
// failure a second render path invites.
|
|
1256
|
+
//
|
|
1257
|
+
// The allowlist is where the one divergence found so far came from, and it
|
|
1258
|
+
// came from *widening* rather than from renderNode growing: `:model` is the
|
|
1259
|
+
// single directive renderNode treats specially that CONTROL_ATTRS does not
|
|
1260
|
+
// name, so it slipped through the generic `:<name>` clause. Adding to this
|
|
1261
|
+
// list is the dangerous edit in this file - see
|
|
1262
|
+
// TODOS/2026-08-24.more-holes-in-the-cloner.md.
|
|
1233
1263
|
// 2. **The interpreted path stays the fallback for everything else**, including
|
|
1234
1264
|
// every tag that could still turn into a component. The upgrade watch and
|
|
1235
1265
|
// the unresolved-component throw are not reimplemented here; they are never
|
|
1236
1266
|
// reached from here.
|
|
1237
1267
|
//
|
|
1238
|
-
// tests/skeleton.test.ts renders a corpus both ways and diffs the DOM
|
|
1239
|
-
// what makes rule 1 enforceable
|
|
1268
|
+
// tests/skeleton.test.ts renders a corpus both ways and diffs the DOM *and the
|
|
1269
|
+
// order the bindings register in*, which is what makes rule 1 enforceable
|
|
1270
|
+
// rather than a promise. The order axis is not decoration: :value on a <select>
|
|
1271
|
+
// has to run after its <option>s are bound, and no DOM diff can see that.
|
|
1240
1272
|
// ---------------------------------------------------------------------------
|
|
1241
1273
|
|
|
1242
1274
|
// Flipping this must never change what renders, only how - which is what
|
|
@@ -1257,7 +1289,16 @@ export type DebugFlags = {
|
|
|
1257
1289
|
cloneSkeletons: boolean
|
|
1258
1290
|
}
|
|
1259
1291
|
|
|
1260
|
-
//
|
|
1292
|
+
// the control attributes a skeleton knows how to fill. The rest of
|
|
1293
|
+
// CONTROL_ATTRS stays rejected on purpose: :if/:elseif/:else/:each/:key change
|
|
1294
|
+
// the shape rather than filling a hole, :with changes the scope its subtree
|
|
1295
|
+
// evaluates in, :props belongs to a component tag, and :html carries a
|
|
1296
|
+
// sanitizer plus a second attribute (:html.allowed) whose "without :html"
|
|
1297
|
+
// warning fires once per render interpreted and would fire once per definition
|
|
1298
|
+
// here - see TODOS/2026-08-24.more-holes-in-the-cloner.md
|
|
1299
|
+
const PLANNABLE_CONTROL_ATTRS = new Set([":text", ":attrs", ":value", ":checked", ":selected"])
|
|
1300
|
+
|
|
1301
|
+
// What a hole can be, in the order renderNode registers them.
|
|
1261
1302
|
// A `:` attribute with a dot in it is rejected wholesale except `:class.`:
|
|
1262
1303
|
// `:model.`, `:props.`, `:slot.` and `:html.allowed` all live in that shape, and
|
|
1263
1304
|
// so would the next directive family somebody invents
|
|
@@ -1265,17 +1306,33 @@ const plannableAttr = (name: string): boolean => {
|
|
|
1265
1306
|
if (name.startsWith("@")) return true
|
|
1266
1307
|
if (name === ":class") return true
|
|
1267
1308
|
if (name.startsWith(":class.")) return true
|
|
1309
|
+
if (PLANNABLE_CONTROL_ATTRS.has(name)) return true
|
|
1268
1310
|
if (!name.startsWith(":")) return name !== COMPONENT_TAG_ATTR // a static attribute
|
|
1311
|
+
// `:model` is the one directive renderNode treats specially that CONTROL_ATTRS
|
|
1312
|
+
// does not name, so the clause below would let it through as a generic
|
|
1313
|
+
// `:<name>` binding and the skeleton would write `model="..."` where the
|
|
1314
|
+
// interpreted path warns and writes nothing (`:model` binds component tags
|
|
1315
|
+
// only). `:model.<name>` is caught by the dot; the bare form needs saying
|
|
1316
|
+
if (name === ":model") return false
|
|
1269
1317
|
return !isControlAttr(name) && !name.includes(".")
|
|
1270
1318
|
}
|
|
1271
1319
|
|
|
1272
1320
|
const plannableNode = (node: TemplateNode): boolean => {
|
|
1273
1321
|
if (node.component || node.tag.includes("-")) return false
|
|
1274
1322
|
if (isSlotTag(node.tag) || node.tag === "template") return false
|
|
1275
|
-
// an unknown tag may still become a component,
|
|
1276
|
-
//
|
|
1277
|
-
|
|
1323
|
+
// an unknown tag may still become a component, so it stays interpreted - the
|
|
1324
|
+
// upgrade watch lives there and a clone cannot carry it. A *foreign* element
|
|
1325
|
+
// skips the test rather than failing it: <circle> is an HTMLUnknownElement
|
|
1326
|
+
// when built with createElement, which is exactly the mistake this used to
|
|
1327
|
+
// make, and nothing in an <svg> subtree can ever become a component (no SVG
|
|
1328
|
+
// tag is uppercase-initial, so none is ever stamped as one)
|
|
1329
|
+
if (node.ns === undefined && document.createElement(node.tag) instanceof HTMLUnknownElement) return false
|
|
1278
1330
|
for (const key in node.attrs) if (!plannableAttr(key)) return false
|
|
1331
|
+
// an element with :text has no children on either path (see buildSkeleton), so
|
|
1332
|
+
// what the source wrote inside it cannot make the subtree unplannable - a
|
|
1333
|
+
// component tag under a :text is markup nobody renders, not markup the clone
|
|
1334
|
+
// path would get wrong
|
|
1335
|
+
if (node.attrs[":text"] !== undefined) return true
|
|
1279
1336
|
return node.children.every(child => typeof child === "string" || plannableNode(child))
|
|
1280
1337
|
}
|
|
1281
1338
|
|
|
@@ -1287,7 +1344,12 @@ type SkeletonOp =
|
|
|
1287
1344
|
| { kind: "text"; path: number[]; parts: TextPart[] }
|
|
1288
1345
|
| { kind: "event"; path: number[]; attr: string; expr: string }
|
|
1289
1346
|
| { kind: "attr"; path: number[]; name: string; expr: string }
|
|
1347
|
+
| { kind: "attrs"; path: number[]; expr: string }
|
|
1290
1348
|
| { kind: "class"; path: number[]; classExpr?: string; toggles: [string, string][] | null; staticClasses: Set<string> }
|
|
1349
|
+
| { kind: "textContent"; path: number[]; expr: string }
|
|
1350
|
+
| { kind: "value"; path: number[]; expr: string }
|
|
1351
|
+
| { kind: "checked"; path: number[]; expr: string }
|
|
1352
|
+
| { kind: "selected"; path: number[]; expr: string }
|
|
1291
1353
|
|
|
1292
1354
|
type SkeletonPlan = { skeleton: Element; ops: SkeletonOp[]; tags: string[] }
|
|
1293
1355
|
|
|
@@ -1296,7 +1358,7 @@ type SkeletonPlan = { skeleton: Element; ops: SkeletonOp[]; tags: string[] }
|
|
|
1296
1358
|
// registration order, so this is not cosmetic
|
|
1297
1359
|
const buildSkeleton = (node: TemplateNode, path: number[], ops: SkeletonOp[], tags: Set<string>): Element => {
|
|
1298
1360
|
tags.add(node.tag)
|
|
1299
|
-
const el =
|
|
1361
|
+
const el = createFor(node)
|
|
1300
1362
|
|
|
1301
1363
|
let classExpr: string | undefined
|
|
1302
1364
|
let toggles: [string, string][] | null = null
|
|
@@ -1305,16 +1367,29 @@ const buildSkeleton = (node: TemplateNode, path: number[], ops: SkeletonOp[], ta
|
|
|
1305
1367
|
if (key.startsWith("@")) ops.push({ kind: "event", path, attr: key, expr: value })
|
|
1306
1368
|
else if (key === ":class") classExpr = value
|
|
1307
1369
|
else if (key.startsWith(":class.")) (toggles ??= []).push([key.slice(":class.".length), value])
|
|
1370
|
+
// a directive of its own, bound below - the same skip renderNode's walk
|
|
1371
|
+
// makes, and for the same reason: without it :text would be written out as
|
|
1372
|
+
// an attribute named `text`. :class/:class. are control attrs too and are
|
|
1373
|
+
// already caught above
|
|
1374
|
+
else if (isControlAttr(key)) { /* handled after the walk */ }
|
|
1308
1375
|
else if (key.startsWith(":")) {
|
|
1309
1376
|
const name = key.slice(1)
|
|
1310
1377
|
ops.push({ kind: "attr", path, name, expr: value || kebabToCamel(name) })
|
|
1311
1378
|
} else el.setAttribute(key, value)
|
|
1312
1379
|
}
|
|
1380
|
+
const attrsExpr = node.attrs[":attrs"]
|
|
1381
|
+
if (attrsExpr !== undefined) ops.push({ kind: "attrs", path, expr: attrsExpr })
|
|
1382
|
+
|
|
1313
1383
|
if (classExpr !== undefined || toggles) {
|
|
1314
1384
|
ops.push({ kind: "class", path, classExpr, toggles, staticClasses: new Set(classNames(node.attrs.class ?? "")) })
|
|
1315
1385
|
}
|
|
1316
1386
|
|
|
1317
|
-
|
|
1387
|
+
// :text replaces the element's content, and renderNode never renders the
|
|
1388
|
+
// children of an element carrying one. So the skeleton gives it none either:
|
|
1389
|
+
// a :text node is a leaf on both paths, whatever the source wrote inside it
|
|
1390
|
+
const textExpr = node.attrs[":text"]
|
|
1391
|
+
if (textExpr !== undefined) ops.push({ kind: "textContent", path, expr: textExpr })
|
|
1392
|
+
else node.children.forEach((child, index) => {
|
|
1318
1393
|
if (typeof child === "string") {
|
|
1319
1394
|
// an interpolated text node is a hole; the skeleton holds the empty node
|
|
1320
1395
|
// it will be written into, so the child indices match either way
|
|
@@ -1327,6 +1402,18 @@ const buildSkeleton = (node: TemplateNode, path: number[], ops: SkeletonOp[], ta
|
|
|
1327
1402
|
el.appendChild(buildSkeleton(child, [...path, index], ops, tags))
|
|
1328
1403
|
})
|
|
1329
1404
|
|
|
1405
|
+
// after the children, because renderNode registers them there and for its
|
|
1406
|
+
// reason: :value on a <select> can only pick an <option> that already exists.
|
|
1407
|
+
// `ops` is flat and in registration order, and this is the recursive call's
|
|
1408
|
+
// tail, so a parent's form-state ops land after every op of every descendant -
|
|
1409
|
+
// which is exactly what renderNode's own recursion does
|
|
1410
|
+
const valueExpr = node.attrs[":value"]
|
|
1411
|
+
if (valueExpr !== undefined) ops.push({ kind: "value", path, expr: valueExpr })
|
|
1412
|
+
const checkedExpr = node.attrs[":checked"]
|
|
1413
|
+
if (checkedExpr !== undefined) ops.push({ kind: "checked", path, expr: checkedExpr })
|
|
1414
|
+
const selectedExpr = node.attrs[":selected"]
|
|
1415
|
+
if (selectedExpr !== undefined) ops.push({ kind: "selected", path, expr: selectedExpr })
|
|
1416
|
+
|
|
1330
1417
|
return el
|
|
1331
1418
|
}
|
|
1332
1419
|
|
|
@@ -1336,15 +1423,46 @@ const buildSkeleton = (node: TemplateNode, path: number[], ops: SkeletonOp[], ta
|
|
|
1336
1423
|
// and a regression on a row whose only fragments are that small
|
|
1337
1424
|
const MIN_SKELETON_ELEMENTS = 3
|
|
1338
1425
|
|
|
1426
|
+
// children under a :text are not built by either path, so they are not elements
|
|
1427
|
+
// this threshold should be counting - a <p :text="v"> with two <span>s written
|
|
1428
|
+
// inside it is one element's worth of cloning, not three
|
|
1339
1429
|
const countElements = (node: TemplateNode): number =>
|
|
1340
|
-
|
|
1430
|
+
node.attrs[":text"] !== undefined
|
|
1431
|
+
? 1
|
|
1432
|
+
: 1 + node.children.reduce((total, child) => total + (typeof child === "string" ? 0 : countElements(child)), 0)
|
|
1341
1433
|
|
|
1342
1434
|
const skeletonPlans = new WeakMap<TemplateNode, SkeletonPlan | null>()
|
|
1343
1435
|
|
|
1436
|
+
// A definition rendered ONCE pays for a plan it never reuses: +23% at
|
|
1437
|
+
// MIN_SKELETON_ELEMENTS, +11.5% at six elements, measured in
|
|
1438
|
+
// TODOS/2026-08-24.one-shot-render-measured.md. So the plan is built on the
|
|
1439
|
+
// SECOND render, not the first - a one-shot definition never builds one at all,
|
|
1440
|
+
// and a :each of 1,000 rows interprets row 1 and clones the other 999.
|
|
1441
|
+
//
|
|
1442
|
+
// The element count could not answer this. It is a proxy for "will this be
|
|
1443
|
+
// rendered again", and raising it to protect the one-shot case would take a
|
|
1444
|
+
// 9-element list row - which amortizes beautifully - off the clone path. This
|
|
1445
|
+
// keys on the thing itself.
|
|
1446
|
+
//
|
|
1447
|
+
// renderEach calls renderNode per row and renderNode calls planOf, so the list
|
|
1448
|
+
// case needs no special handling; it falls out.
|
|
1449
|
+
//
|
|
1450
|
+
// The third state is a WeakSet rather than a sentinel in the map, so the map's
|
|
1451
|
+
// type keeps saying what it means: absent is "never seen", null is "examined,
|
|
1452
|
+
// not plannable"
|
|
1453
|
+
const seenOnce = new WeakSet<TemplateNode>()
|
|
1454
|
+
|
|
1344
1455
|
const planOf = (node: TemplateNode): SkeletonPlan | null => {
|
|
1345
1456
|
const cached = skeletonPlans.get(node)
|
|
1346
1457
|
if (cached !== undefined) return cached
|
|
1347
1458
|
|
|
1459
|
+
// first sighting: interpret it, and decide nothing. Examining it here is the
|
|
1460
|
+
// cost the one-shot case was paying
|
|
1461
|
+
if (!seenOnce.has(node)) {
|
|
1462
|
+
seenOnce.add(node)
|
|
1463
|
+
return null
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1348
1466
|
let plan: SkeletonPlan | null = null
|
|
1349
1467
|
if (plannableNode(node) && countElements(node) >= MIN_SKELETON_ELEMENTS) {
|
|
1350
1468
|
const ops: SkeletonOp[] = []
|
|
@@ -1365,6 +1483,9 @@ const atPath = (root: Node, path: number[]): Node => {
|
|
|
1365
1483
|
const renderFromSkeleton = (plan: SkeletonPlan, scope: Record<string, any>, fx: EffectScope): Node => {
|
|
1366
1484
|
const root = plan.skeleton.cloneNode(true) as Element
|
|
1367
1485
|
|
|
1486
|
+
// ordered by how often a row actually carries the kind, not by when it was
|
|
1487
|
+
// added: this chain runs once per op per instance, so the four holes a
|
|
1488
|
+
// benchmark row is made of are matched before the five a form is
|
|
1368
1489
|
for (const op of plan.ops) {
|
|
1369
1490
|
const target = op.path.length === 0 ? root : atPath(root, op.path)
|
|
1370
1491
|
|
|
@@ -1381,7 +1502,7 @@ const renderFromSkeleton = (plan: SkeletonPlan, scope: Record<string, any>, fx:
|
|
|
1381
1502
|
const el = target as Element
|
|
1382
1503
|
const { name, expr } = op
|
|
1383
1504
|
fx.effect(() => applyAttr(el, name, evalExpr(expr, scope)))
|
|
1384
|
-
} else {
|
|
1505
|
+
} else if (op.kind === "class") {
|
|
1385
1506
|
const el = target as Element
|
|
1386
1507
|
const { classExpr, toggles, staticClasses } = op
|
|
1387
1508
|
let bound: string[] = []
|
|
@@ -1396,6 +1517,44 @@ const renderFromSkeleton = (plan: SkeletonPlan, scope: Record<string, any>, fx:
|
|
|
1396
1517
|
el.classList.add(...next)
|
|
1397
1518
|
bound = next
|
|
1398
1519
|
})
|
|
1520
|
+
} else if (op.kind === "attrs") {
|
|
1521
|
+
const el = target as Element
|
|
1522
|
+
const { expr } = op
|
|
1523
|
+
let boundKeys: string[] = []
|
|
1524
|
+
fx.effect(() => {
|
|
1525
|
+
boundKeys.forEach(key => el.removeAttribute(key))
|
|
1526
|
+
const bound = evalExpr(expr, scope)
|
|
1527
|
+
boundKeys = bound && typeof bound === "object" ? Object.keys(bound) : []
|
|
1528
|
+
boundKeys.forEach(key => applyAttr(el, key, bound[key]))
|
|
1529
|
+
})
|
|
1530
|
+
} else if (op.kind === "textContent") {
|
|
1531
|
+
const el = target as Element
|
|
1532
|
+
const { expr } = op
|
|
1533
|
+
fx.effect(() => { el.textContent = String(evalExpr(expr, scope) ?? "") })
|
|
1534
|
+
} else if (op.kind === "value") {
|
|
1535
|
+
// the property, not the attribute, and skipping a write that would not
|
|
1536
|
+
// change it - renderNode's reasons apply here unchanged
|
|
1537
|
+
const el = target as HTMLInputElement
|
|
1538
|
+
const { expr } = op
|
|
1539
|
+
fx.effect(() => {
|
|
1540
|
+
const value = String(evalExpr(expr, scope) ?? "")
|
|
1541
|
+
if (el.value !== value) el.value = value
|
|
1542
|
+
})
|
|
1543
|
+
} else if (op.kind === "checked") {
|
|
1544
|
+
const el = target as HTMLInputElement
|
|
1545
|
+
const { expr } = op
|
|
1546
|
+
fx.effect(() => { el.checked = !!evalExpr(expr, scope) })
|
|
1547
|
+
} else if (op.kind === "selected") {
|
|
1548
|
+
const el = target as HTMLOptionElement
|
|
1549
|
+
const { expr } = op
|
|
1550
|
+
fx.effect(() => { el.selected = !!evalExpr(expr, scope) })
|
|
1551
|
+
} else {
|
|
1552
|
+
// every kind is named above, so this is unreachable - and the assignment
|
|
1553
|
+
// is what makes the compiler say so. A kind added to SkeletonOp and
|
|
1554
|
+
// forgotten here is the exact failure this whole file is arranged to
|
|
1555
|
+
// prevent, and it is cheaper to catch it in tsc than in the corpus
|
|
1556
|
+
const unhandled: never = op
|
|
1557
|
+
void unhandled
|
|
1399
1558
|
}
|
|
1400
1559
|
}
|
|
1401
1560
|
|
|
@@ -1430,7 +1589,7 @@ const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: Eff
|
|
|
1430
1589
|
if (plan && !plan.tags.some(tag => findComponentKey(scope, tag))) return renderFromSkeleton(plan, scope, fx)
|
|
1431
1590
|
}
|
|
1432
1591
|
|
|
1433
|
-
const el =
|
|
1592
|
+
const el = createFor(node)
|
|
1434
1593
|
|
|
1435
1594
|
// <UserCrad /> - written as a component (node.component), resolving to no
|
|
1436
1595
|
// component, and not an element either. Nothing else on the page can supply
|