ripple 0.4.3 → 0.4.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/package.json +3 -3
  3. package/src/constants.js +6 -0
  4. package/src/runtime/internal/client/attributes.js +251 -125
  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/expression.js +4 -4
  9. package/src/runtime/internal/client/for.js +13 -38
  10. package/src/runtime/internal/client/if.js +56 -1
  11. package/src/runtime/internal/client/index.js +6 -6
  12. package/src/runtime/internal/client/runtime.js +73 -11
  13. package/src/runtime/internal/client/template-ns.js +45 -4
  14. package/src/runtime/internal/client/template.js +56 -0
  15. package/tests/client/composite/composite.dynamic-components.test.tsrx +40 -0
  16. package/tests/client/css/scoped-styles.test.tsrx +3 -1
  17. package/tests/client/svg.test.tsrx +273 -1
  18. package/tests/hydration/compiled/client/basic.js +36 -35
  19. package/tests/hydration/compiled/client/composite.js +11 -2
  20. package/tests/hydration/compiled/client/events.js +1 -1
  21. package/tests/hydration/compiled/client/for.js +73 -62
  22. package/tests/hydration/compiled/client/fragment-cursor.js +182 -115
  23. package/tests/hydration/compiled/client/head.js +10 -10
  24. package/tests/hydration/compiled/client/hmr.js +11 -2
  25. package/tests/hydration/compiled/client/html-in-template.js +1 -1
  26. package/tests/hydration/compiled/client/html.js +78 -50
  27. package/tests/hydration/compiled/client/if-children.js +94 -26
  28. package/tests/hydration/compiled/client/if-fragment-controlflow.js +34 -14
  29. package/tests/hydration/compiled/client/if.js +65 -17
  30. package/tests/hydration/compiled/client/mixed-control-flow.js +50 -20
  31. package/tests/hydration/compiled/client/nested-control-flow.js +188 -97
  32. package/tests/hydration/compiled/client/portal.js +14 -5
  33. package/tests/hydration/compiled/client/reactivity.js +3 -3
  34. package/tests/hydration/compiled/client/streaming.js +17 -17
  35. package/tests/hydration/compiled/client/switch.js +17 -17
  36. package/tests/hydration/compiled/client/track-async-serialization.js +19 -19
  37. package/tests/hydration/compiled/client/try.js +9 -9
  38. package/tests/hydration/compiled/client/typed-text.js +3 -2
@@ -1,131 +1,323 @@
1
- /** @import { Block } from '#client' */
1
+ /** @import { AppendIntoAnchor, Block } from '#client' */
2
+ /** @import { SpreadState } from './attributes.js' */
2
3
  import { component_invalid } from './errors.js';
3
4
 
4
- import { branch, destroy_block, render } from './blocks.js';
5
- import { render_spread } from './attributes.js';
6
- import { COMPOSITE_BLOCK, DEFAULT_NAMESPACE, NAMESPACE_URI } from './constants.js';
5
+ import { branch, destroy_block, destroy_block_children, render } from './blocks.js';
6
+ import { release_spread, spread } from './attributes.js';
7
+ import { COMPOSITE_BLOCK, DEFAULT_NAMESPACE, NAMESPACE_URI, UNINITIALIZED } from './constants.js';
7
8
  import { hydrate_node, hydrate_next, hydrating, set_hydrate_node } from './hydration.js';
8
9
  import { first_child } from './operations.js';
9
- import { active_block, active_namespace, get, untrack, with_ns } from './runtime.js';
10
+ import { active_block, active_namespace, get, set_ns, set_tracking, untrack } from './runtime.js';
10
11
  import { top_element_to_ns } from './utils.js';
11
- import { install_ns_templates } from './template-ns.js';
12
+ import { push_ns, with_ns } from './template-ns.js';
13
+ import { append } from './template.js';
12
14
  import { is_tsrx_element } from '../../element.js';
13
15
  import { render_component } from './component.js';
14
16
  import { HYDRATION } from 'ripple/internal/client/hydration-enabled';
15
17
 
16
18
  /**
17
19
  * @typedef {Function | string | null | undefined | false} CompositeTarget
20
+ * The state of a composite block. A tag target is the block's own DOM range
21
+ * (`start`/`end`, the element) and its attributes the block's own spread; a
22
+ * component target renders in a branch, a child of the block.
23
+ * @typedef {{
24
+ * start: Element | null;
25
+ * end: Element | null;
26
+ * g: () => CompositeTarget;
27
+ * a: Node | AppendIntoAnchor;
28
+ * p: () => Record<string, any>;
29
+ * c: CompositeTarget | typeof UNINITIALIZED;
30
+ * s: SpreadState | undefined;
31
+ * }} CompositeState
32
+ */
33
+
34
+ /**
35
+ * @param {CompositeState} state
36
+ */
37
+ function run_composite(state) {
38
+ var block = /** @type {Block} */ (active_block);
39
+ // @ts-ignore — get() handles non-tracked values via is_ripple_object() check
40
+ var component = get(state.g());
41
+ var current = state.c;
42
+ var old = state.start;
43
+
44
+ if (component === current && typeof component === 'string') {
45
+ // The same tag keeps its element: only the attributes follow the props.
46
+ state.s = spread(/** @type {Element} */ (old), state.p(), state.s);
47
+ return;
48
+ }
49
+
50
+ if (current !== UNINITIALIZED) {
51
+ // The previous target's blocks: the children rendered into the element,
52
+ // whose DOM goes with it, or a component's branch, which removes its own.
53
+ destroy_block_children(block, old === null);
54
+ state.s = undefined;
55
+ }
56
+ state.c = component;
57
+
58
+ if (typeof component === 'function') {
59
+ if (old !== null) {
60
+ old.remove();
61
+ state.start = state.end = null;
62
+ }
63
+ var anchor = state.a;
64
+ var get_props = state.p;
65
+ // Handle as regular component
66
+ branch(() => {
67
+ var props = untrack(get_props);
68
+ render_component(component, /** @type {Node} */ (anchor), props);
69
+ });
70
+ return;
71
+ }
72
+ if (is_tsrx_element(component)) {
73
+ component_invalid(true);
74
+ }
75
+ if (component == null) {
76
+ if (old !== null) {
77
+ old.remove();
78
+ state.start = state.end = null;
79
+ }
80
+ return;
81
+ }
82
+
83
+ // A tag: the element is created in the namespace the tag resolves to.
84
+ var ns = top_element_to_ns(component, active_namespace);
85
+ /** @type {Element} */
86
+ var element;
87
+ if (HYDRATION && hydrating) {
88
+ // Claim the SSR-rendered element instead of creating a new one.
89
+ element = /** @type {Element} */ (hydrate_node);
90
+ } else {
91
+ element =
92
+ ns !== DEFAULT_NAMESPACE
93
+ ? document.createElementNS(
94
+ NAMESPACE_URI[ns],
95
+ /** @type {keyof HTMLElementTagNameMap} */ (component),
96
+ )
97
+ : document.createElement(/** @type {keyof HTMLElementTagNameMap} */ (component));
98
+ if (old !== null) {
99
+ // A tag change takes the previous element's place.
100
+ old.before(element);
101
+ old.remove();
102
+ } else {
103
+ append(/** @type {ChildNode | AppendIntoAnchor} */ (state.a), element);
104
+ }
105
+ }
106
+ state.start = state.end = element;
107
+
108
+ // The spread reads the props once, tracked: a props change re-runs the
109
+ // block, which diffs the attributes above. The children come from that
110
+ // same read.
111
+ var spread_state = (state.s = spread(element, state.p(), undefined));
112
+ var props = /** @type {Record<string, any>} */ (spread_state.n);
113
+ if (is_tsrx_element(props.children)) {
114
+ /** @type {Node} */
115
+ var child_anchor;
116
+ if (HYDRATION && hydrating) {
117
+ // The server renders children directly inside the element with no
118
+ // extra markers; descend the cursor so they claim those nodes.
119
+ child_anchor = /** @type {Node} */ (first_child(element));
120
+ } else {
121
+ child_anchor = document.createComment('');
122
+ element.appendChild(child_anchor);
123
+ }
124
+
125
+ // The children render untracked, as a branch renders its content. The
126
+ // children of a `foreignObject` are HTML whatever namespace the element
127
+ // itself takes.
128
+ var child_ns = component === 'foreignObject' ? DEFAULT_NAMESPACE : ns;
129
+ set_tracking(false);
130
+ if (child_ns !== active_namespace) {
131
+ with_ns(child_ns, () => props.children.render(child_anchor, block, props.children.p));
132
+ } else {
133
+ props.children.render(child_anchor, block, props.children.p);
134
+ }
135
+ set_tracking(true);
136
+
137
+ if (HYDRATION && hydrating) {
138
+ // Reset the cursor to the claimed element so sibling traversal
139
+ // continues after it.
140
+ set_hydrate_node(element);
141
+ }
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Renders a dynamic element or component (`<{expr}>`): the block re-runs
147
+ * when the expression or the props change. A tag that stays the same keeps
148
+ * its element and diffs the attributes; a new tag or component replaces the
149
+ * content in place.
18
150
  * @param {() => CompositeTarget} get_component
19
- * @param {Node} node
151
+ * @param {Node | AppendIntoAnchor} node
20
152
  * @param {() => Record<string, any>} get_props the props literal of the call
21
153
  * site: read once per rendered component (props are plain values), and
22
154
  * re-read as element attributes when the target is a tag
155
+ * @param {keyof typeof NAMESPACE_URI} [namespace] the namespace of the
156
+ * template the call site sits in, when it is not HTML
23
157
  * @returns {void}
24
158
  */
25
- export function composite(get_component, node, get_props) {
159
+ export function composite(get_component, node, get_props, namespace) {
26
160
  if (HYDRATION && hydrating) {
27
161
  // During hydration, `node` may already point at the first real SSR node
28
162
  // (e.g. layout children). Only skip forward when we are on an empty
29
163
  // comment anchor from a client template placeholder.
30
- if (node.nodeType === 8 && /** @type {Comment} */ (node).data === '') {
164
+ if (/** @type {Node} */ (node).nodeType === 8 && /** @type {Comment} */ (node).data === '') {
31
165
  hydrate_next();
32
166
  }
33
167
  }
34
168
 
35
- var anchor = node;
36
- /** @type {Block | null} */
37
- var b = null;
169
+ /** @type {CompositeState} */
170
+ var state = {
171
+ start: null,
172
+ end: null,
173
+ g: get_component,
174
+ a: node,
175
+ p: get_props,
176
+ c: UNINITIALIZED,
177
+ s: undefined,
178
+ };
179
+ if (namespace === undefined) {
180
+ render(run_composite, state, COMPOSITE_BLOCK);
181
+ return;
182
+ }
183
+ var previous = push_ns(namespace);
184
+ try {
185
+ render(run_composite, state, COMPOSITE_BLOCK);
186
+ } finally {
187
+ set_ns(previous);
188
+ }
189
+ }
38
190
 
39
- render(
40
- () => {
41
- // @ts-ignore — get() handles non-tracked values via is_ripple_object() check
42
- var component = get(get_component());
191
+ /**
192
+ * The state of a dynamic element the enclosing render function drives (see
193
+ * `dynamic`): the element rendered for a tag, or the branch rendered for a
194
+ * component, and the tag or component itself.
195
+ * @typedef {{
196
+ * e: Element | null;
197
+ * b: Block | null;
198
+ * t: CompositeTarget | typeof UNINITIALIZED;
199
+ * s: SpreadState | undefined;
200
+ * g: (() => CompositeTarget) | null;
201
+ * p: (() => Record<string, any>) | null;
202
+ * }} DynamicState
203
+ */
43
204
 
44
- if (b !== null) {
45
- destroy_block(b);
46
- b = null;
205
+ /**
206
+ * A dynamic element without children, applied from the render function of
207
+ * the content it sits in instead of a block of its own: `<{tag} {...attrs} />`
208
+ * compiles to `__prev.x = _$_.dynamic(__prev.x, anchor, tag, props)`, so the
209
+ * tag and the props are read by that render function, and a run whose tag is
210
+ * unchanged only diffs the attributes. The element sits in the enclosing
211
+ * template's range, which owns it. A component target is handed to a
212
+ * composite block of its own (in a branch the render function keeps), which
213
+ * reads the tag and the props through the thunks kept since creation and so
214
+ * remounts the component with fresh props whenever the tag expression
215
+ * re-evaluates, exactly as `<{expr}>` did before; from then on the slot is
216
+ * null and the calls are no-ops.
217
+ * @param {DynamicState | null | undefined} state null once a composite block
218
+ * owns the content (a component target, or the hydration claim, see
219
+ * `dynamic_init`)
220
+ * @param {Node | AppendIntoAnchor} anchor
221
+ * @param {CompositeTarget} tag
222
+ * @param {Record<string, any> | null | undefined} props
223
+ * @param {keyof typeof NAMESPACE_URI} [namespace] the namespace of the
224
+ * template the call site sits in, when it is not HTML
225
+ * @returns {DynamicState | null}
226
+ */
227
+ export function dynamic(state, anchor, tag, props, namespace) {
228
+ if (state === null) {
229
+ return null;
230
+ }
231
+ // @ts-ignore — get() handles non-tracked values via is_ripple_object() check
232
+ tag = get(tag);
233
+ /** @type {DynamicState} */
234
+ var s;
235
+ if (state === undefined) {
236
+ s = { e: null, b: null, t: UNINITIALIZED, s: undefined, g: null, p: null };
237
+ } else {
238
+ s = state;
239
+ if (tag === s.t) {
240
+ if (s.e !== null) {
241
+ s.s = spread(s.e, props, s.s);
47
242
  }
243
+ return s;
244
+ }
245
+ }
48
246
 
49
- if (typeof component === 'function') {
50
- // Handle as regular component
51
- b = branch(() => {
52
- var props = untrack(get_props);
53
- render_component(component, anchor, props);
54
- });
55
- } else if (is_tsrx_element(component)) {
56
- component_invalid(true);
57
- } else if (component != null) {
58
- // Custom element - only create if component is not null/undefined
59
- const ns = top_element_to_ns(component, active_namespace);
60
- if (ns !== DEFAULT_NAMESPACE) {
61
- // Templates cloned inside the element parse in its namespace.
62
- install_ns_templates();
63
- }
64
- var run = () => {
65
- var block = /** @type {Block} */ (active_block);
66
-
67
- /** @type {Element} */
68
- var element;
69
- if (HYDRATION && hydrating) {
70
- // Claim the SSR-rendered element instead of creating a new one.
71
- element = /** @type {Element} */ (hydrate_node);
72
- } else {
73
- element =
74
- ns !== DEFAULT_NAMESPACE
75
- ? document.createElementNS(
76
- NAMESPACE_URI[ns],
77
- /** @type {keyof HTMLElementTagNameMap} */ (component),
78
- )
79
- : document.createElement(/** @type {keyof HTMLElementTagNameMap} */ (component));
80
-
81
- /** @type {ChildNode} */ (anchor).before(element);
82
- }
83
-
84
- if (block.s === null) {
85
- block.s = {
86
- start: element,
87
- end: element,
88
- };
89
- }
90
-
91
- render_spread(element, get_props, 0);
92
-
93
- var props = untrack(get_props);
94
- if (is_tsrx_element(props.children)) {
95
- /** @type {Node} */
96
- var child_anchor;
97
- if (HYDRATION && hydrating) {
98
- // The server renders children directly inside the element with no
99
- // extra markers; descend the cursor so they claim those nodes.
100
- child_anchor = /** @type {Node} */ (first_child(element));
101
- } else {
102
- child_anchor = document.createComment('');
103
- element.appendChild(child_anchor);
104
- }
105
-
106
- if (ns !== DEFAULT_NAMESPACE) {
107
- with_ns(ns, () => props.children.render(child_anchor, block, props.children.p));
108
- } else {
109
- props.children.render(child_anchor, block, props.children.p);
110
- }
111
-
112
- if (HYDRATION && hydrating) {
113
- // Reset the cursor to the claimed element so sibling traversal
114
- // continues after it.
115
- set_hydrate_node(element);
116
- }
117
- }
118
- };
119
-
120
- if (ns !== active_namespace) {
121
- // support top-level dynamic element svg/math tags
122
- b = branch(() => with_ns(ns, run));
123
- } else {
124
- b = branch(run);
125
- }
126
- }
127
- },
128
- null,
129
- COMPOSITE_BLOCK,
247
+ var old = s.e;
248
+ if (s.b !== null) {
249
+ destroy_block(s.b);
250
+ s.b = null;
251
+ }
252
+ // The previous element's refs and listeners go with it.
253
+ release_spread(s.s);
254
+ s.t = tag;
255
+ s.s = undefined;
256
+
257
+ if (typeof tag === 'function') {
258
+ if (old !== null) {
259
+ old.remove();
260
+ s.e = null;
261
+ }
262
+ const get_component = s.g;
263
+ const get_props = s.p;
264
+ if (get_component !== null && get_props !== null) {
265
+ branch(() => composite(get_component, anchor, get_props, namespace));
266
+ return null;
267
+ }
268
+ // No thunks (a call site compiled without `dynamic_init`): the
269
+ // component renders once with these props.
270
+ s.b = branch(() => {
271
+ render_component(tag, /** @type {Node} */ (anchor), props ?? {});
272
+ });
273
+ return s;
274
+ }
275
+ if (is_tsrx_element(tag)) {
276
+ component_invalid(true);
277
+ }
278
+ if (tag == null || tag === false) {
279
+ if (old !== null) {
280
+ old.remove();
281
+ s.e = null;
282
+ }
283
+ return s;
284
+ }
285
+
286
+ var ns = top_element_to_ns(
287
+ /** @type {Parameters<typeof top_element_to_ns>[0]} */ (tag),
288
+ namespace === undefined ? active_namespace : namespace,
130
289
  );
290
+ var element =
291
+ ns !== DEFAULT_NAMESPACE
292
+ ? document.createElementNS(NAMESPACE_URI[ns], /** @type {string} */ (tag))
293
+ : document.createElement(/** @type {string} */ (tag));
294
+ if (old !== null) {
295
+ old.before(element);
296
+ old.remove();
297
+ } else {
298
+ append(/** @type {ChildNode | AppendIntoAnchor} */ (anchor), element);
299
+ }
300
+ s.e = element;
301
+ s.s = spread(element, props, undefined);
302
+ return s;
303
+ }
304
+
305
+ /**
306
+ * The initial state of a `dynamic` slot: the tag and props thunks a
307
+ * component target is handed over with. While hydrating, the element is
308
+ * rendered here by a composite block of its own, which claims its server
309
+ * nodes in document order and owns the content from then on, so the render
310
+ * function's `dynamic` calls are no-ops (null).
311
+ * @param {Node | AppendIntoAnchor} node
312
+ * @param {() => CompositeTarget} get_component
313
+ * @param {() => Record<string, any>} get_props
314
+ * @param {keyof typeof NAMESPACE_URI} [namespace]
315
+ * @returns {DynamicState | null}
316
+ */
317
+ export function dynamic_init(node, get_component, get_props, namespace) {
318
+ if (HYDRATION && hydrating) {
319
+ composite(get_component, node, get_props, namespace);
320
+ return null;
321
+ }
322
+ return { e: null, b: null, t: UNINITIALIZED, s: undefined, g: get_component, p: get_props };
131
323
  }
@@ -35,6 +35,14 @@ export var CREATES_DERIVEDS = 1 << 25;
35
35
  * re-runs the body's update function.
36
36
  */
37
37
  export var ITEM_BLOCK = 1 << 26;
38
+ /**
39
+ * The namespace a block was created in, so a rerun outside the `with_ns()`
40
+ * call that established it (a scheduled flush) creates its DOM in the same
41
+ * namespace. Neither bit set means HTML.
42
+ */
43
+ export var SVG_BLOCK = 1 << 27;
44
+ export var MATHML_BLOCK = 1 << 28;
45
+ export var NAMESPACE_BLOCK = SVG_BLOCK | MATHML_BLOCK;
38
46
 
39
47
  export var CONTROL_FLOW_BLOCK = FOR_BLOCK | IF_BLOCK | SWITCH_BLOCK | TRY_BLOCK | COMPOSITE_BLOCK;
40
48
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { is_array } from '@tsrx/core/runtime/language-helpers';
4
4
  import { branch, destroy_block, render } from './blocks.js';
5
- import { BRANCH_BLOCK, IF_BLOCK, UNINITIALIZED } from './constants.js';
5
+ import { BRANCH_BLOCK, COMPOSITE_BLOCK, IF_BLOCK, UNINITIALIZED } from './constants.js';
6
6
  import { create_text, get_next_sibling } from './operations.js';
7
7
  import { assign_nodes } from './template.js';
8
8
  import { active_block } from './runtime.js';
@@ -12,14 +12,14 @@ import { is_tsrx_element, TSRX_ELEMENT } from '../../element.js';
12
12
  import { HYDRATION } from 'ripple/internal/client/hydration-enabled';
13
13
 
14
14
  /**
15
- * Finds the nearest enclosing block that owns a DOM range (a branch or an if
16
- * block) in the block hierarchy.
15
+ * Finds the nearest enclosing block that owns a DOM range (a branch, an if
16
+ * or a composite block) in the block hierarchy.
17
17
  * @param {Block | null} block
18
18
  * @returns {Block | null}
19
19
  */
20
20
  function find_enclosing_branch(block) {
21
21
  while (block !== null) {
22
- if ((block.f & (BRANCH_BLOCK | IF_BLOCK)) !== 0) {
22
+ if ((block.f & (BRANCH_BLOCK | IF_BLOCK | COMPOSITE_BLOCK)) !== 0) {
23
23
  return block;
24
24
  }
25
25
  block = block.p;
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { IS_CONTROLLED, IS_INDEXED, LOCAL_ITEMS, ROOT_CONTROLLED } from '../../../constants.js';
4
4
  import {
5
- block as create_block,
6
5
  create_block as allocate_block,
7
6
  branch,
8
7
  destroy_block,
@@ -60,21 +59,10 @@ function create_item(anchor, value, index, key, state) {
60
59
  p: null,
61
60
  };
62
61
 
63
- // An item without a tracked of its own (a plain list's, or a local one)
64
- // renders in place: allocated, linked and run through `run_branch`, with
65
- // none of the first-run bookkeeping a block that may re-run needs.
66
- if (tracked_value === value && tracked_index === undefined) {
67
- var lean = allocate_block(BRANCH_BLOCK, run_item, item_state);
68
- run_branch(lean, render_fn, anchor, value, key);
69
- lean.f ^= BLOCK_HAS_RUN;
70
- return lean;
71
- }
72
-
73
- // Passed through module state rather than a per-item closure; run_item
74
- // reads them before rendering, so nested loops cannot observe a stale pair.
75
- item_anchor = anchor;
76
- item_render_fn = render_fn;
77
- var b = create_block(BRANCH_BLOCK, run_item, item_state);
62
+ // The item renders in place: allocated, linked and run through
63
+ // `run_branch`, with none of the first-run bookkeeping a block that may
64
+ // re-run needs (a rerun is `run_item`'s, through `run_block`).
65
+ var b = allocate_block(BRANCH_BLOCK, run_item, item_state);
78
66
 
79
67
  // The item's tracked value and index are owned by the item block itself.
80
68
  if (tracked_value !== value) {
@@ -83,6 +71,8 @@ function create_item(anchor, value, index, key, state) {
83
71
  if (tracked_index !== undefined) {
84
72
  /** @type {Tracked} */ (tracked_index).b = b;
85
73
  }
74
+ run_branch(b, render_fn, anchor, tracked_value, tracked_index, key);
75
+ b.f ^= BLOCK_HAS_RUN;
86
76
  return b;
87
77
  }
88
78
 
@@ -118,32 +108,17 @@ function controlled_anchor(parent) {
118
108
  return { parent, into: true, tail: false };
119
109
  }
120
110
 
121
- /** @type {Node | AppendIntoAnchor | null} */
122
- var item_anchor = null;
123
- /** @type {((anchor: Node, value: any, index?: any, key?: any) => Block) | null} */
124
- var item_render_fn = null;
125
-
126
111
  /**
112
+ * A re-run of an item that carries its render block (see `item`): only the
113
+ * body's update function, tracked. The first run is `run_branch`'s, in
114
+ * `create_item`; an item block with no render block of its own never re-runs.
127
115
  * @param {{ i: Tracked | undefined, v: any, k: any, p: any }} state
128
116
  */
129
117
  function run_item(state) {
130
- var p = state.p;
131
- if (p !== null) {
132
- // A re-run of an item that carries its render block: only the body's
133
- // update function, tracked (see `item`).
134
- set_tracking(true);
135
- /** @type {(prev: any) => void} */ (
136
- /** @type {ListState} */ (/** @type {Block} */ (/** @type {Block} */ (active_block).p).s).u
137
- )(p);
138
- return;
139
- }
140
- var render_fn = /** @type {(anchor: Node, value: any, index?: any, key?: any) => Block} */ (
141
- item_render_fn
142
- );
143
- var anchor = /** @type {Node} */ (item_anchor);
144
- item_render_fn = null;
145
- item_anchor = null;
146
- render_fn(anchor, state.v, state.i, state.k);
118
+ set_tracking(true);
119
+ /** @type {(prev: any) => void} */ (
120
+ /** @type {ListState} */ (/** @type {Block} */ (/** @type {Block} */ (active_block).p).s).u
121
+ )(state.p);
147
122
  }
148
123
 
149
124
  /**
@@ -3,6 +3,7 @@
3
3
  import {
4
4
  block,
5
5
  branch,
6
+ create_block,
6
7
  destroy_block,
7
8
  get_first_node,
8
9
  get_last_node,
@@ -10,9 +11,10 @@ import {
10
11
  remove_block_dom,
11
12
  } from './blocks.js';
12
13
  import { DETACHED_BLOCK, IF_BLOCK, RENDER_BLOCK, UNINITIALIZED } from './constants.js';
14
+ import { IF_ROOT_CONTROLLED, IF_TRACKED } from '../../../constants.js';
13
15
  import { hydrate_next, hydrate_node, hydrating } from './hydration.js';
14
16
  import { create_text, resolve_anchor } from './operations.js';
15
- import { active_block, probe_if, run_untracked } from './runtime.js';
17
+ import { active_block, probe_if, run_in_block, run_untracked } from './runtime.js';
16
18
  import { append } from './template.js';
17
19
  import { HYDRATION } from 'ripple/internal/client/hydration-enabled';
18
20
 
@@ -262,3 +264,56 @@ export function if_block(node, fn, root_controlled, x) {
262
264
  append(/** @type {ChildNode} */ (node), /** @type {Node} */ (boundary));
263
265
  }
264
266
  }
267
+
268
+ /**
269
+ * An if whose condition the enclosing render function evaluates (see the
270
+ * compiler's if lowering): the block only owns the branch, its DOM range and
271
+ * its blocks, and never runs itself. The render function calls `if_update`
272
+ * with the branch the condition selected whenever that changes. As
273
+ * `if_block` does, a condition that read no tracked state has its branch
274
+ * rendered by the probe, with no block at all: null is returned and the
275
+ * updates are no-ops. While hydrating, the if keeps a block of its own that
276
+ * runs the condition, exactly as `if_block` creates: the branch claims its
277
+ * server nodes in document order, and that block owns the condition's
278
+ * subscription and a pending read it throws; null is returned then too.
279
+ * @param {Node | AppendIntoAnchor} node
280
+ * @param {IfState['fn']} fn
281
+ * @param {number} [flags] `IF_ROOT_CONTROLLED` (see `if_block`) and
282
+ * `IF_TRACKED`, set when the compiler proved the condition reads tracked
283
+ * state, so the probe is skipped
284
+ * @param {any} [x] see `if_block`
285
+ * @returns {Block | null}
286
+ */
287
+ export function if_static(node, fn, flags = 0, x) {
288
+ var root_controlled = (flags & IF_ROOT_CONTROLLED) !== 0;
289
+
290
+ if (HYDRATION && hydrating) {
291
+ if_block(node, fn, root_controlled, x);
292
+ return null;
293
+ }
294
+ if ((flags & IF_TRACKED) === 0) {
295
+ var rendered;
296
+ try {
297
+ rendered = probe_if(fn, x, node);
298
+ } catch {
299
+ rendered = false;
300
+ }
301
+ if (rendered) {
302
+ return null;
303
+ }
304
+ }
305
+
306
+ return create_block(IF_BLOCK, noop, if_block_state(node, fn, x));
307
+ }
308
+
309
+ /**
310
+ * Renders `fn` as the branch of an if created by `if_static`, when it is not
311
+ * the current one. The branch's blocks belong to the if block.
312
+ * @param {Block | null} block
313
+ * @param {Branch | undefined} fn
314
+ */
315
+ export function if_update(block, fn) {
316
+ if (block !== null) {
317
+ run_in_block(block, update_branch, fn);
318
+ }
319
+ }
@@ -40,7 +40,7 @@ export {
40
40
  RENDER_ENTRY as $r,
41
41
  } from './constants.js';
42
42
  export { event, render_event, delegate, listen } from './events.js';
43
- export { set_style, set_attribute, render_spread } from './attributes.js';
43
+ export { set_style, set_attribute, render_spread, spread } from './attributes.js';
44
44
  export { portal } from './portal.js';
45
45
 
46
46
  export {
@@ -73,7 +73,7 @@ export {
73
73
  derived,
74
74
  tick,
75
75
  with_block,
76
- with_ns,
76
+ set_ns,
77
77
  handle_error,
78
78
  queue_post_block_flush_callback,
79
79
  schedule_update,
@@ -81,7 +81,7 @@ export {
81
81
 
82
82
  export { track_async } from './track-async.js';
83
83
 
84
- export { composite } from './composite.js';
84
+ export { composite, dynamic, dynamic_init } from './composite.js';
85
85
 
86
86
  export { render_component } from './component.js';
87
87
 
@@ -89,15 +89,15 @@ export { for_block as for, for_block_keyed as for_keyed, item } from './for.js';
89
89
 
90
90
  export { selector, selector_match } from './selector.js';
91
91
 
92
- export { if_block as if } from './if.js';
92
+ export { if_block as if, if_static, if_update } from './if.js';
93
93
 
94
94
  export { try_block as try, get_pending_boundary } from './try.js';
95
95
 
96
96
  export { if_block as switch } from './if.js';
97
97
 
98
- export { template, append, text } from './template.js';
98
+ export { template, template_el, append, text } from './template.js';
99
99
 
100
- export { template_ns } from './template-ns.js';
100
+ export { template_ns, with_ns, render_component_ns } from './template-ns.js';
101
101
 
102
102
  export {
103
103
  ripple_array,