ripple 0.4.3 → 0.4.5

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/package.json +3 -3
  3. package/src/constants.js +6 -0
  4. package/src/runtime/internal/client/attributes.js +259 -126
  5. package/src/runtime/internal/client/blocks.js +16 -6
  6. package/src/runtime/internal/client/composite.js +292 -100
  7. package/src/runtime/internal/client/constants.js +8 -0
  8. package/src/runtime/internal/client/events.js +15 -1
  9. package/src/runtime/internal/client/expression.js +4 -4
  10. package/src/runtime/internal/client/for.js +13 -38
  11. package/src/runtime/internal/client/if.js +56 -1
  12. package/src/runtime/internal/client/index.js +8 -6
  13. package/src/runtime/internal/client/operations.js +9 -4
  14. package/src/runtime/internal/client/render.js +37 -0
  15. package/src/runtime/internal/client/runtime.js +94 -20
  16. package/src/runtime/internal/client/template-ns.js +45 -4
  17. package/src/runtime/internal/client/template.js +103 -0
  18. package/tests/client/composite/composite.dynamic-components.test.tsrx +40 -0
  19. package/tests/client/css/scoped-styles.test.tsrx +3 -1
  20. package/tests/client/svg.test.tsrx +273 -1
  21. package/tests/hydration/compiled/client/basic.js +79 -51
  22. package/tests/hydration/compiled/client/composite.js +13 -4
  23. package/tests/hydration/compiled/client/events.js +22 -7
  24. package/tests/hydration/compiled/client/for.js +110 -71
  25. package/tests/hydration/compiled/client/fragment-cursor.js +342 -156
  26. package/tests/hydration/compiled/client/head.js +13 -13
  27. package/tests/hydration/compiled/client/hmr.js +16 -4
  28. package/tests/hydration/compiled/client/html-in-template.js +1 -1
  29. package/tests/hydration/compiled/client/html.js +144 -87
  30. package/tests/hydration/compiled/client/if-children.js +118 -33
  31. package/tests/hydration/compiled/client/if-fragment-controlflow.js +41 -21
  32. package/tests/hydration/compiled/client/if.js +70 -18
  33. package/tests/hydration/compiled/client/mixed-control-flow.js +50 -20
  34. package/tests/hydration/compiled/client/nested-control-flow.js +191 -100
  35. package/tests/hydration/compiled/client/portal.js +29 -9
  36. package/tests/hydration/compiled/client/reactivity.js +9 -5
  37. package/tests/hydration/compiled/client/streaming.js +21 -18
  38. package/tests/hydration/compiled/client/switch.js +17 -17
  39. package/tests/hydration/compiled/client/track-async-serialization.js +29 -21
  40. package/tests/hydration/compiled/client/try.js +13 -10
  41. package/tests/hydration/compiled/client/typed-text.js +3 -2
  42. package/tests/utils/inline-drift.test.js +83 -0
@@ -18,6 +18,8 @@ export {
18
18
  set_value,
19
19
  set_checked,
20
20
  set_selected,
21
+ set_attribute_value,
22
+ set_property_value,
21
23
  } from './render.js';
22
24
 
23
25
  export {
@@ -40,7 +42,7 @@ export {
40
42
  RENDER_ENTRY as $r,
41
43
  } from './constants.js';
42
44
  export { event, render_event, delegate, listen } from './events.js';
43
- export { set_style, set_attribute, render_spread } from './attributes.js';
45
+ export { set_style, set_attribute, render_spread, spread } from './attributes.js';
44
46
  export { portal } from './portal.js';
45
47
 
46
48
  export {
@@ -73,7 +75,7 @@ export {
73
75
  derived,
74
76
  tick,
75
77
  with_block,
76
- with_ns,
78
+ set_ns,
77
79
  handle_error,
78
80
  queue_post_block_flush_callback,
79
81
  schedule_update,
@@ -81,7 +83,7 @@ export {
81
83
 
82
84
  export { track_async } from './track-async.js';
83
85
 
84
- export { composite } from './composite.js';
86
+ export { composite, dynamic, dynamic_init } from './composite.js';
85
87
 
86
88
  export { render_component } from './component.js';
87
89
 
@@ -89,15 +91,15 @@ export { for_block as for, for_block_keyed as for_keyed, item } from './for.js';
89
91
 
90
92
  export { selector, selector_match } from './selector.js';
91
93
 
92
- export { if_block as if } from './if.js';
94
+ export { if_block as if, if_static, if_update } from './if.js';
93
95
 
94
96
  export { try_block as try, get_pending_boundary } from './try.js';
95
97
 
96
98
  export { if_block as switch } from './if.js';
97
99
 
98
- export { template, append, text } from './template.js';
100
+ export { template, template_el, append, text } from './template.js';
99
101
 
100
- export { template_ns } from './template-ns.js';
102
+ export { template_ns, with_ns, render_component_ns } from './template-ns.js';
101
103
 
102
104
  export {
103
105
  ripple_array,
@@ -32,9 +32,12 @@ export var is_firefox;
32
32
  export function init_operations() {
33
33
  var node_prototype = Node.prototype;
34
34
  var element_prototype = Element.prototype;
35
- var event_target_prototype = Event.prototype;
36
35
 
37
- is_firefox = /Firefox/.test(navigator.userAgent);
36
+ // Firefox prefers `importNode` for template content (see `template.js`).
37
+ // Its Firefox-only `window` property is the test: the first read of
38
+ // `navigator` creates its wrapper, about 0.1 ms on a fresh page, paid by
39
+ // the first mount.
40
+ is_firefox = 'mozInnerScreenX' in window;
38
41
  document = window.document;
39
42
 
40
43
  first_child_getter = /** @type {(() => Node | null)} */ (
@@ -47,9 +50,11 @@ export function init_operations() {
47
50
  get_descriptor(node_prototype, 'lastChild')?.get
48
51
  );
49
52
 
50
- // the following assignments improve perf of lookups on DOM nodes
53
+ // Declared on the prototype so a delegated handler lookup on an element
54
+ // stops at Element instead of walking to Object. `Event.prototype.__root`
55
+ // is declared with the first delegated listener (`events.js`): touching
56
+ // `Event.prototype` costs about 0.05 ms on a fresh page.
51
57
  element_prototype.__click = undefined;
52
- event_target_prototype.__root = undefined;
53
58
  }
54
59
 
55
60
  /**
@@ -165,3 +165,40 @@ export function set_selected(element, selected) {
165
165
  element.removeAttribute('selected');
166
166
  }
167
167
  }
168
+
169
+ /**
170
+ * Writes an attribute the compiler proved is not a property of its element
171
+ * (`dom-setters.js` in the compiler), so no setter is looked for: `null` and
172
+ * `undefined` remove it.
173
+ * @param {Element} element
174
+ * @param {string} attribute
175
+ * @param {any} value
176
+ * @returns {void}
177
+ */
178
+ export function set_attribute_value(element, attribute, value) {
179
+ if (value == null) {
180
+ element.removeAttribute(attribute);
181
+ } else {
182
+ element.setAttribute(attribute, value);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Writes a name the compiler proved is a settable property of its element:
188
+ * a string sets the attribute, any other value the property, and `null` or
189
+ * `undefined` remove the attribute, as `set_attribute` decides at runtime
190
+ * after discovering the element's setters.
191
+ * @param {Element} element
192
+ * @param {string} attribute
193
+ * @param {any} value
194
+ * @returns {void}
195
+ */
196
+ export function set_property_value(element, attribute, value) {
197
+ if (value == null) {
198
+ element.removeAttribute(attribute);
199
+ } else if (typeof value !== 'string') {
200
+ /** @type {any} */ (element)[attribute] = value;
201
+ } else {
202
+ element.setAttribute(attribute, value);
203
+ }
204
+ }
@@ -4,7 +4,7 @@
4
4
  /** @typedef {DerivedValue} Derived */
5
5
 
6
6
  import { DEV } from 'esm-env';
7
- import { destroy_block, destroy_non_branch_children, effect } from './blocks.js';
7
+ import { block, destroy_block, destroy_non_branch_children } from './blocks.js';
8
8
  import {
9
9
  ASYNC_DERIVED_READ_THROWN,
10
10
  BLOCK_HAS_RUN,
@@ -27,10 +27,13 @@ import {
27
27
  SCHEDULED,
28
28
  SELECTOR,
29
29
  IF_BLOCK,
30
+ COMPOSITE_BLOCK,
30
31
  ITEM_BLOCK,
31
32
  RELEASED,
32
33
  RENDER_ENTRY,
33
34
  CREATES_DERIVEDS,
35
+ NAMESPACE_BLOCK,
36
+ SVG_BLOCK,
34
37
  } from './constants.js';
35
38
  import { is_ripple_object } from './utils.js';
36
39
  import { scope_orphan, set_in_derived, track_orphan, update_depth_exceeded } from './errors.js';
@@ -367,9 +370,10 @@ function handle_run_error(error, block) {
367
370
  * @returns {Derived[] | null}
368
371
  */
369
372
  function prepare_rerun(block) {
370
- // A list, an if, and a list item re-run only their own logic; their child
371
- // blocks (items, the branch's blocks, an item body's nested blocks) stay.
372
- if ((block.f & (FOR_BLOCK | IF_BLOCK | ITEM_BLOCK)) === 0) {
373
+ // A list, an if, a composite and a list item re-run only their own logic;
374
+ // their child blocks (items, the branch's blocks, a dynamic element's
375
+ // children, an item body's nested blocks) stay.
376
+ if ((block.f & (FOR_BLOCK | IF_BLOCK | ITEM_BLOCK | COMPOSITE_BLOCK)) === 0) {
373
377
  destroy_non_branch_children(block);
374
378
  }
375
379
  run_teardown(block);
@@ -393,17 +397,18 @@ function register_teardown(block, teardown) {
393
397
 
394
398
  /**
395
399
  * Runs the body of a branch block that was just created, in place of a first
396
- * `run_block`: a branch renders untracked and never re-runs, so the only
397
- * bookkeeping its first run needs is the globals the body reads and the
398
- * dependencies a nested `item` records on it. Errors are handled as
399
- * `run_block` handles them.
400
+ * `run_block`: a branch renders untracked, so the only bookkeeping its first
401
+ * run needs is the globals the body reads and the dependencies a nested
402
+ * `item` records on it (a list item re-runs through `run_block`, see
403
+ * `run_item`). Errors are handled as `run_block` handles them.
400
404
  * @param {Block} block
401
405
  * @param {(anchor: any, value: any, index?: any, key?: any) => void} fn
402
406
  * @param {any} anchor
403
- * @param {any} value
404
- * @param {any} [key] a keyed list item's key (see `create_item`)
407
+ * @param {any} value a list item's value, or its tracked
408
+ * @param {any} [index] a list item's tracked index (see `create_item`)
409
+ * @param {any} [key] a keyed list item's key
405
410
  */
406
- export function run_branch(block, fn, anchor, value, key) {
411
+ export function run_branch(block, fn, anchor, value, index, key) {
407
412
  var previous_block = active_block;
408
413
  var previous_reaction = active_reaction;
409
414
  var previous_tracking = tracking;
@@ -416,7 +421,7 @@ export function run_branch(block, fn, anchor, value, key) {
416
421
  active_component = block.co;
417
422
  tracking = false;
418
423
  active_dependency = null;
419
- fn(anchor, value, undefined, key);
424
+ fn(anchor, value, index, key);
420
425
  if (active_dependency !== null) {
421
426
  block.d = active_dependency;
422
427
  }
@@ -431,6 +436,43 @@ export function run_branch(block, fn, anchor, value, key) {
431
436
  }
432
437
  }
433
438
 
439
+ /**
440
+ * Runs `fn(block.s, arg)` as a rerun of `block` driven from outside it: an if
441
+ * whose condition the enclosing render function evaluates renders its branch
442
+ * this way. The function runs under the block, in the block's namespace, and
443
+ * the deriveds the block's previous run created are released afterwards
444
+ * unless still read, as `run_block` does for a rerun. An error propagates to
445
+ * the caller's block.
446
+ * @template S, A
447
+ * @param {Block} block
448
+ * @param {(state: S, arg: A) => void} fn
449
+ * @param {A} arg
450
+ */
451
+ export function run_in_block(block, fn, arg) {
452
+ var previous_block = active_block;
453
+ var previous_reaction = active_reaction;
454
+ var previous_component = active_component;
455
+ var previous_namespace = active_namespace;
456
+ var previous_deriveds = (block.f & CREATES_DERIVEDS) !== 0 ? take_created_deriveds(block) : null;
457
+
458
+ try {
459
+ active_block = block;
460
+ active_reaction = block;
461
+ active_component = block.co;
462
+ var ns = block.f & NAMESPACE_BLOCK;
463
+ active_namespace = ns === 0 ? DEFAULT_NAMESPACE : ns === SVG_BLOCK ? 'svg' : 'mathml';
464
+ fn(/** @type {S} */ (block.s), arg);
465
+ } finally {
466
+ active_block = previous_block;
467
+ active_reaction = previous_reaction;
468
+ active_component = previous_component;
469
+ active_namespace = previous_namespace;
470
+ if (previous_deriveds !== null) {
471
+ keep_deriveds(block, release_deriveds(previous_deriveds));
472
+ }
473
+ }
474
+ }
475
+
434
476
  /**
435
477
  * @param {Block} block
436
478
  * @param {boolean} [first_run] true when the block has no children, teardown,
@@ -442,6 +484,7 @@ export function run_block(block, first_run = false) {
442
484
  var previous_tracking = tracking;
443
485
  var previous_dependency = active_dependency;
444
486
  var previous_component = active_component;
487
+ var previous_namespace = active_namespace;
445
488
  /** @type {Derived[] | null} */
446
489
  var previous_deriveds = null;
447
490
 
@@ -449,6 +492,10 @@ export function run_block(block, first_run = false) {
449
492
  active_block = block;
450
493
  active_reaction = block;
451
494
  active_component = block.co;
495
+ // The namespace the block was created in (see `create_block`): a rerun
496
+ // from a flush is outside the `with_ns()` call that established it.
497
+ var ns = block.f & NAMESPACE_BLOCK;
498
+ active_namespace = ns === 0 ? DEFAULT_NAMESPACE : ns === SVG_BLOCK ? 'svg' : 'mathml';
452
499
 
453
500
  if (!first_run) {
454
501
  previous_deriveds = prepare_rerun(block);
@@ -478,6 +525,7 @@ export function run_block(block, first_run = false) {
478
525
  tracking = previous_tracking;
479
526
  active_dependency = previous_dependency;
480
527
  active_component = previous_component;
528
+ active_namespace = previous_namespace;
481
529
 
482
530
  // The previous run's deriveds, whether this run finished or threw, and
483
531
  // after this run's dependencies replaced the old ones (so a derived the
@@ -748,7 +796,9 @@ export function track_read_only(value, block) {
748
796
  * @returns {Tracked | Derived}
749
797
  */
750
798
  export function track(v, b, hash, get, set) {
751
- if (is_ripple_object(v)) {
799
+ // is_ripple_object(), inline: track() is on every component's cold path
800
+ // (kept identical by tests/utils/inline-drift.test.js).
801
+ if (typeof v === 'object' && v !== null && typeof v.f === 'number') {
752
802
  return v;
753
803
  }
754
804
  if (b === null) {
@@ -1078,21 +1128,29 @@ function flush_queue(pending) {
1078
1128
  blocks.sort(by_block_id);
1079
1129
  }
1080
1130
 
1081
- var has_effects = false;
1131
+ var effect_kinds = 0;
1132
+ var has_render_blocks = false;
1082
1133
 
1083
1134
  for (var i = 0; i < length; i++) {
1084
1135
  var block = blocks[i];
1085
1136
  var flags = block.f;
1086
1137
  block.f = flags & ~SCHEDULED;
1087
- if ((flags & (PRE_EFFECT_BLOCK | EFFECT_BLOCK)) !== 0) {
1088
- has_effects = true;
1138
+ var kind = flags & (PRE_EFFECT_BLOCK | EFFECT_BLOCK);
1139
+ if (kind !== 0) {
1140
+ effect_kinds |= kind;
1141
+ } else {
1142
+ has_render_blocks = true;
1089
1143
  }
1090
1144
  }
1091
1145
 
1092
1146
  // New schedules land in the queue that replaced `pending`, so the array
1093
- // can be run in place. Effects need the three-phase ordering; a queue of
1094
- // only render blocks (the common flush) runs straight through.
1095
- if (has_effects) {
1147
+ // can be run in place. A queue mixing kinds needs the three-phase
1148
+ // ordering; one of a single kind (the common flush of render blocks,
1149
+ // the first flush of a mounted app's effects) runs straight through.
1150
+ if (
1151
+ effect_kinds === (PRE_EFFECT_BLOCK | EFFECT_BLOCK) ||
1152
+ (effect_kinds !== 0 && has_render_blocks)
1153
+ ) {
1096
1154
  run_phases(blocks, length);
1097
1155
  } else {
1098
1156
  run_phase(blocks);
@@ -1805,7 +1863,9 @@ function create_deferred_effects(effects) {
1805
1863
  for (var i = 0; i < length; i += 3) {
1806
1864
  active_block = /** @type {Block} */ (effects[i + 1]);
1807
1865
  active_reaction = /** @type {Block | Derived | null} */ (effects[i + 2]);
1808
- effect(/** @type {Function} */ (effects[i]));
1866
+ // effect(), inline: one function less on every component's cold path
1867
+ // (kept identical by tests/utils/inline-drift.test.js).
1868
+ block(EFFECT_BLOCK, /** @type {Function} */ (effects[i]));
1809
1869
  }
1810
1870
  active_block = previous_block;
1811
1871
  active_reaction = previous_reaction;
@@ -1827,6 +1887,20 @@ export function with_ns(namespace, fn) {
1827
1887
  }
1828
1888
  }
1829
1889
 
1890
+ /**
1891
+ * Sets the active namespace and returns the previous one, for setup code that
1892
+ * switches namespace part-way through a render function (the children of a
1893
+ * `foreignObject`), where a `with_ns()` thunk would hide its declarations.
1894
+ * `run_block` restores the namespace after the render function either way.
1895
+ * @param {keyof typeof NAMESPACE_URI} namespace
1896
+ * @returns {keyof typeof NAMESPACE_URI}
1897
+ */
1898
+ export function set_ns(namespace) {
1899
+ var previous_namespace = active_namespace;
1900
+ active_namespace = namespace;
1901
+ return previous_namespace;
1902
+ }
1903
+
1830
1904
  /**
1831
1905
  * @returns {symbol}
1832
1906
  */
@@ -1,4 +1,6 @@
1
+ /** @import { NAMESPACE_URI } from './constants.js' */
1
2
  import { get_first_child } from './operations.js';
3
+ import { render_component, set_ns, with_ns as run_with_ns } from './runtime.js';
2
4
  import { set_ns_parser, template } from './template.js';
3
5
 
4
6
  /**
@@ -26,12 +28,20 @@ function from_namespace(content, ns) {
26
28
  }
27
29
 
28
30
  /**
29
- * Makes namespaced template parsing available: called by every path that can
30
- * clone a template into the SVG or MathML namespace, so an app without one
31
- * never loads it.
31
+ * Runs `fn` with `namespace` active: content rendered inside it (children
32
+ * passed into an `<svg>`, a component rendered there, a dynamic element's
33
+ * children) creates its DOM in that namespace, and every block created
34
+ * inside records it (see `create_block`) so its reruns do too. Templates
35
+ * cloned in the namespace parse through `from_namespace`, so it is installed
36
+ * here: the compiler only emits this call for the SVG and MathML namespaces.
37
+ * @template T
38
+ * @param {keyof typeof NAMESPACE_URI} namespace
39
+ * @param {() => T} fn
40
+ * @returns {T}
32
41
  */
33
- export function install_ns_templates() {
42
+ export function with_ns(namespace, fn) {
34
43
  set_ns_parser(from_namespace);
44
+ return run_with_ns(namespace, fn);
35
45
  }
36
46
 
37
47
  /**
@@ -45,3 +55,34 @@ export function template_ns(content, flags, count) {
45
55
  set_ns_parser(from_namespace);
46
56
  return template(content, flags, count);
47
57
  }
58
+
59
+ /**
60
+ * Enters `namespace` for the rest of a setup function, as `with_ns` does for
61
+ * a thunk: the compiler emits this where the content that follows is a
62
+ * dynamic element or a component called inside an `<svg>` or `<math>`
63
+ * template. Returns the namespace to restore with `set_ns`.
64
+ * @param {keyof typeof NAMESPACE_URI} namespace
65
+ * @returns {keyof typeof NAMESPACE_URI}
66
+ */
67
+ export function push_ns(namespace) {
68
+ set_ns_parser(from_namespace);
69
+ return set_ns(namespace);
70
+ }
71
+
72
+ /**
73
+ * `render_component` for a component called inside an `<svg>` or `<math>`
74
+ * template: its content renders in that namespace.
75
+ * @param {keyof typeof NAMESPACE_URI} namespace
76
+ * @param {Function} fn
77
+ * @param {Node | import('#client').AppendIntoAnchor} anchor
78
+ * @param {Record<string, any>} props
79
+ * @returns {void}
80
+ */
81
+ export function render_component_ns(namespace, fn, anchor, props) {
82
+ var previous = push_ns(namespace);
83
+ try {
84
+ render_component(fn, anchor, props);
85
+ } finally {
86
+ set_ns(previous);
87
+ }
88
+ }
@@ -9,6 +9,7 @@ import {
9
9
  import { H, hydrate_node, hydrating } from './hydration.js';
10
10
  import { create_text, get_first_child, is_firefox } from './operations.js';
11
11
  import { active_block, active_namespace } from './runtime.js';
12
+ import { DEFAULT_NAMESPACE, NAMESPACE_URI } from './constants.js';
12
13
  import { HYDRATION } from 'ripple/internal/client/hydration-enabled';
13
14
 
14
15
  /**
@@ -142,6 +143,108 @@ export function template(content, flags = 0, count = 1) {
142
143
  : clone_template(t);
143
144
  }
144
145
 
146
+ /**
147
+ * The document that owns parsed template content: it has no browsing
148
+ * context, so an element created in it does not load its `src` (an image,
149
+ * a video) until a clone is adopted into the page, exactly like a node
150
+ * parsed from a `<template>`.
151
+ * @type {Document | undefined}
152
+ */
153
+ var inert_document;
154
+
155
+ /**
156
+ * A nested element of a DOM-built template: its tag, attributes and children,
157
+ * trailing absences left out.
158
+ * @typedef {[string, (string[] | null)?, (string | TemplateChildren)?]} TemplateElementSpec
159
+ */
160
+
161
+ /**
162
+ * The children of a DOM-built template element in order: a nested element,
163
+ * a text string, or `null` for a placeholder comment.
164
+ * @typedef {Array<string | null | TemplateElementSpec>} TemplateChildren
165
+ */
166
+
167
+ /**
168
+ * Builds one element of a DOM-built template, in the inert document and
169
+ * the active namespace, as a parsed template would create it.
170
+ * @param {Document} doc
171
+ * @param {keyof typeof NAMESPACE_URI} ns
172
+ * @param {string} tag
173
+ * @param {string[] | null | undefined} attributes
174
+ * @param {string | TemplateChildren | undefined} children
175
+ * @returns {Element}
176
+ */
177
+ function build_template_element(doc, ns, tag, attributes, children) {
178
+ var node =
179
+ ns === DEFAULT_NAMESPACE ? doc.createElement(tag) : doc.createElementNS(NAMESPACE_URI[ns], tag);
180
+ if (attributes != null) {
181
+ for (var i = 0; i < attributes.length; i += 2) {
182
+ node.setAttribute(attributes[i], attributes[i + 1]);
183
+ }
184
+ }
185
+ if (children === undefined || typeof children === 'string') {
186
+ if (children !== undefined && children !== '') {
187
+ node.textContent = children;
188
+ }
189
+ } else {
190
+ for (var j = 0; j < children.length; j++) {
191
+ var child = children[j];
192
+ node.appendChild(
193
+ child === null
194
+ ? doc.createComment('')
195
+ : typeof child === 'string'
196
+ ? doc.createTextNode(child)
197
+ : build_template_element(doc, ns, child[0], child[1], child[2]),
198
+ );
199
+ }
200
+ }
201
+ return node;
202
+ }
203
+
204
+ /**
205
+ * A template of a few HTML elements with static attributes, built with DOM
206
+ * calls instead of parsed: a `<template>` parse has a fixed cost that dwarfs
207
+ * the elements themselves, and an app's first render pays it once per
208
+ * distinct template, plus the parser's own start-up on a fresh page. The
209
+ * compiler decides which templates qualify (`dom_built_template` in the
210
+ * client transform); every other shape still parses. The nodes are created
211
+ * in the active namespace, as a parsed template would be, and in the inert
212
+ * template document when one of them could load a resource (`inert`), also
213
+ * as parsed; any other master is built in the document itself, which skips
214
+ * creating the inert document on a fresh page.
215
+ * @param {string} tag
216
+ * @param {string[] | null} [attributes] - flat name/value pairs
217
+ * @param {string | TemplateChildren} [children] - one text child, or the
218
+ * child nodes in order
219
+ * @param {number} [inert] - 1 when an element could load a resource from an
220
+ * attribute the master carries
221
+ * @returns {() => Node}
222
+ */
223
+ export function template_el(tag, attributes = null, children = '', inert = 0) {
224
+ /** @type {Element | undefined} */
225
+ var node;
226
+ /** @type {string | undefined} */
227
+ var node_ns;
228
+
229
+ return () => {
230
+ if (HYDRATION && hydrating) {
231
+ return /** @type {import('./hydrate.js').HydrationRuntime} */ (H).t(false, 1);
232
+ }
233
+ var ns = active_namespace;
234
+ if (node === undefined || node_ns !== ns) {
235
+ var doc =
236
+ inert === 0
237
+ ? document
238
+ : (inert_document ??= document.createElement('template').content.ownerDocument);
239
+ node = build_template_element(doc, ns, tag, attributes, children);
240
+ node_ns = ns;
241
+ }
242
+ var clone = is_firefox ? document.importNode(node, true) : node.cloneNode(true);
243
+ assign_nodes(clone, clone);
244
+ return clone;
245
+ };
246
+ }
247
+
145
248
  /**
146
249
  * Appends a DOM node before the anchor node.
147
250
  * @param {ChildNode | AppendIntoAnchor} anchor - The anchor node.
@@ -76,6 +76,46 @@ describe('composite > dynamic components', () => {
76
76
  expect(container.textContent).toBe('hidden');
77
77
  });
78
78
 
79
+ it(
80
+ 'remounts a childless dynamic component with fresh props when its tag expression re-evaluates',
81
+ () => {
82
+ let bump: () => void = () => {};
83
+ let poke: () => void = () => {};
84
+
85
+ function Child(props: { n: number }) @{
86
+ <span>{props.n}</span>
87
+ }
88
+
89
+ function App() @{
90
+ const n = track(0);
91
+ const flag = track(true);
92
+ bump = () => {
93
+ n.value++;
94
+ };
95
+ poke = () => {
96
+ flag.value = !flag.value;
97
+ };
98
+ // The tag expression reads `flag`, but always resolves to `Child`.
99
+ <{flag.value || !flag.value ? Child : null} n={n.value} />
100
+ }
101
+
102
+ render(App);
103
+ flushSync();
104
+ const span = () => container.querySelector('span');
105
+ expect(span()?.textContent).toBe('0');
106
+
107
+ // A prop is a value: the instance keeps the number it got.
108
+ bump();
109
+ flushSync();
110
+ expect(span()?.textContent).toBe('0');
111
+
112
+ // The tag expression re-evaluated: a new instance reads the literal again.
113
+ poke();
114
+ flushSync();
115
+ expect(span()?.textContent).toBe('1');
116
+ },
117
+ );
118
+
79
119
  it('handles dynamic component switching', () => {
80
120
  function Child1() @{
81
121
  <div>{'I am child 1'}</div>
@@ -645,8 +645,10 @@ export function Precedence() @{
645
645
  // block is written after the nested scope.
646
646
  expect(black).toBeLessThan(italic);
647
647
  expect(italic).toBeLessThan(blue);
648
+ // The outer div shares a fragment template with the placeholder that
649
+ // follows it; the inner div is a flat element template of its own.
648
650
  expect(code).toContain(`class="outer ${outer} ${theme_hash}"`);
649
- expect(code).toContain(`class="inner ${outer} ${inner} ${theme_hash}"`);
651
+ expect(code).toContain(`'class', 'inner ${outer} ${inner} ${theme_hash}'`);
650
652
  },
651
653
  );
652
654
  });