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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,86 @@
1
1
  # ripple
2
2
 
3
+ ## 0.4.5
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1497](https://github.com/Ripple-TS/ripple/pull/1497)
8
+ [`2469ccf`](https://github.com/Ripple-TS/ripple/commit/2469ccfba1a6afad4af6658a777dc735767c5625)
9
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Trim the cold path of a first
10
+ mount: `template_el` builds nested templates and builds a master in the document
11
+ itself unless the compiler flagged a resource-loading element; Firefox is
12
+ detected without reading `navigator` (about 0.1 ms on a fresh page);
13
+ `Event.prototype.__root` is declared with the first delegated listener instead
14
+ of at mount; `set_attribute` skips the property-setter walk for `data-` and
15
+ `aria-` names; the first flush of a mounted app's effects runs in one phase;
16
+ `track()` and deferred component effects call one function less each; and a
17
+ mount before any delegated event is registered attaches no listeners.
18
+
19
+ New `set_attribute_value` and `set_property_value` helpers write an attribute
20
+ the compiler classified with the Web IDL setter table, so a standard element
21
+ never triggers the descriptor walk of `set_attribute`.
22
+
23
+ - Updated dependencies
24
+ [[`2469ccf`](https://github.com/Ripple-TS/ripple/commit/2469ccfba1a6afad4af6658a777dc735767c5625)]:
25
+ - @tsrx/ripple@0.2.4
26
+
27
+ ## 0.4.4
28
+
29
+ ### Patch Changes
30
+
31
+ - [#1496](https://github.com/Ripple-TS/ripple/pull/1496)
32
+ [`0473e93`](https://github.com/Ripple-TS/ripple/commit/0473e93006e53cd15fcf6d851443d427ccf13e8b)
33
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Update `@tsrx/core` to ^0.2.3
34
+ and use its SVG and MathML tag-name predicates for dynamic class handling,
35
+ removing the compiler's duplicate tag-name list.
36
+
37
+ - [#1490](https://github.com/Ripple-TS/ripple/pull/1490)
38
+ [`ff3a26a`](https://github.com/Ripple-TS/ripple/commit/ff3a26acef0c9584cf41d32bc8cbcc39d863d74c)
39
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Blocks keep the namespace
40
+ they were created in when they rerun. A dynamic element whose tag changes, an
41
+ `@if` that toggles, or an `@for` that grows inside an SVG that is only known at
42
+ runtime (children rendered into a component's `<svg>`, a leaf component rendered
43
+ there) now creates its elements in the SVG namespace instead of HTML, and a
44
+ nested template cloned in that context no longer throws before a namespaced
45
+ template has been parsed. A dynamic element whose tag becomes `foreignObject`
46
+ renders its children as HTML.
47
+
48
+ - [#1488](https://github.com/Ripple-TS/ripple/pull/1488)
49
+ [`b3bf787`](https://github.com/Ripple-TS/ripple/commit/b3bf78756d9235412a2af264e42897652e8f911e)
50
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Build a template that is one
51
+ element with static attributes and at most a text child with DOM calls instead
52
+ of parsing it. Parsing a `<template>` has a fixed cost that dwarfs such an
53
+ element, and an app's first render pays it once per distinct template; a chain
54
+ of 100 single-element components mounts about 35% faster.
55
+
56
+ - [#1495](https://github.com/Ripple-TS/ripple/pull/1495)
57
+ [`1b8381f`](https://github.com/Ripple-TS/ripple/commit/1b8381f31f98f84049764ca56676fc320721e844)
58
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Faster element spreads,
59
+ dynamic elements and list items. An element spread is applied from the element's
60
+ own render function instead of a render block of its own, and a spread object
61
+ identical to the one applied last is skipped without a diff (a spread of a
62
+ tracked object or of tracked values is always diffed). A dynamic element
63
+ (`<{tag}>`) is one block that owns its element and applies its attributes
64
+ itself; a tag that does not change keeps its element and diffs the attributes,
65
+ so a swap between variants with the same tag no longer recreates the element. An
66
+ `@if` whose condition reads tracked state is evaluated by the render function of
67
+ the enclosing content (a list item's, an element's), so the if keeps a block for
68
+ its branch but not one that runs the condition. A dynamic element or component
69
+ called inside an `<svg>` or `<math>` template receives the namespace as an
70
+ argument instead of a `with_ns` closure. A dynamic element without children is
71
+ driven by the render function of the content it sits in, with no block or
72
+ closures of its own (`_$_.dynamic`); a build that can hydrate lets a composite
73
+ block claim the server element while hydrating. With `textTypes`, an attribute
74
+ value TypeScript proves to be a string is set with a direct `setAttribute` call,
75
+ and a proven `class` takes the string-only path. On the SVG dashboard benchmark,
76
+ mounting is about 20% faster and swapping 150 dynamic icons 40% faster.
77
+ - Updated dependencies
78
+ [[`0473e93`](https://github.com/Ripple-TS/ripple/commit/0473e93006e53cd15fcf6d851443d427ccf13e8b),
79
+ [`b3bf787`](https://github.com/Ripple-TS/ripple/commit/b3bf78756d9235412a2af264e42897652e8f911e),
80
+ [`1b8381f`](https://github.com/Ripple-TS/ripple/commit/1b8381f31f98f84049764ca56676fc320721e844),
81
+ [`ff3a26a`](https://github.com/Ripple-TS/ripple/commit/ff3a26acef0c9584cf41d32bc8cbcc39d863d74c)]:
82
+ - @tsrx/ripple@0.2.3
83
+
3
84
  ## 0.4.3
4
85
 
5
86
  ### Patch Changes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.4.3",
6
+ "version": "0.4.5",
7
7
  "type": "module",
8
8
  "sideEffects": false,
9
9
  "module": "src/runtime/index-client.js",
@@ -86,8 +86,8 @@
86
86
  "csstype": "^3.2.3",
87
87
  "devalue": "^5.9.2",
88
88
  "esm-env": "^1.2.2",
89
- "@tsrx/core": "^0.2.0",
90
- "@tsrx/ripple": "0.2.2"
89
+ "@tsrx/core": "^0.2.3",
90
+ "@tsrx/ripple": "0.2.4"
91
91
  },
92
92
  "devDependencies": {
93
93
  "@types/estree": "^1.0.8",
package/src/constants.js CHANGED
@@ -38,3 +38,9 @@ export const ELEMENT_NODE = 1;
38
38
  export const TEXT_NODE = 3;
39
39
  export const COMMENT_NODE = 8;
40
40
  export const DOCUMENT_FRAGMENT_NODE = 11;
41
+
42
+ // `if_static` flags (see the runtime's if module): the if renders directly
43
+ // before the component's `__anchor`, and the compiler proved its condition
44
+ // reads tracked state.
45
+ export const IF_ROOT_CONTROLLED = 1;
46
+ export const IF_TRACKED = 1 << 1;
@@ -1,7 +1,7 @@
1
1
  /** @import { Block } from '#client' */
2
2
 
3
3
  import { block, branch, destroy_block, ref } from './blocks.js';
4
- import { DESTROYED, REF_PROP, RENDER_BLOCK } from './constants.js';
4
+ import { DESTROYED, REF_PROP, RENDER_BLOCK, TRACKED_OBJECT } from './constants.js';
5
5
  import { isRefProp as is_ref_prop } from '@tsrx/core/runtime/ref';
6
6
  import { is_ripple_object } from './utils.js';
7
7
  import {
@@ -80,7 +80,14 @@ export function set_style(element, value, prev = {}) {
80
80
  export function set_attribute(element, attribute, value) {
81
81
  if (value == null) {
82
82
  element.removeAttribute(attribute);
83
- } else if (typeof value !== 'string' && get_setters(element).includes(attribute)) {
83
+ } else if (
84
+ typeof value !== 'string' &&
85
+ // A `data-` or `aria-` name is never a property: the setter walk it
86
+ // would trigger reads every descriptor of the element's prototypes.
87
+ !attribute.startsWith('data-') &&
88
+ !attribute.startsWith('aria-') &&
89
+ get_setters(element).includes(attribute)
90
+ ) {
84
91
  /** @type {any} */ (element)[attribute] = value;
85
92
  } else {
86
93
  element.setAttribute(attribute, value);
@@ -120,10 +127,9 @@ function apply_styles(element, new_styles, prev) {
120
127
  * @param {Element} element
121
128
  * @param {string} key
122
129
  * @param {any} value
123
- * @param {Record<string, (() => void) | undefined>} remove_listeners
124
- * @param {Record<string | symbol, any>} prev
130
+ * @param {SpreadState} state
125
131
  */
126
- function set_attribute_helper(element, key, value, remove_listeners, prev) {
132
+ function set_attribute_helper(element, key, value, state) {
127
133
  if (key === 'class') {
128
134
  const is_html = element.namespaceURI === 'http://www.w3.org/1999/xhtml';
129
135
  set_class_value(/** @type {HTMLElement} */ (element), value, undefined, is_html);
@@ -131,20 +137,21 @@ function set_attribute_helper(element, key, value, remove_listeners, prev) {
131
137
  element.removeAttribute('innerhtml');
132
138
  /** @type {HTMLElement} */ (element).innerHTML = value == null ? '' : String(value);
133
139
  } else if (key === 'style') {
134
- set_style(element, value, prev.style);
140
+ set_style(element, value, state.p.style);
135
141
  } else if (key === '#class') {
136
142
  // Special case for the scope classes of an element that spreads props:
137
143
  // one token per scope hash and applied theme.
138
144
  element.classList.add(...String(value).split(' ').filter(Boolean));
139
- } else if (typeof key === 'string' && is_event_attribute(key)) {
145
+ } else if (is_event_attribute(key)) {
140
146
  // Handle event handlers in spread props
141
- if (remove_listeners[key]) {
142
- remove_listeners[key]();
143
- remove_listeners[key] = undefined;
147
+ var listeners = (state.l ??= {});
148
+ if (listeners[key]) {
149
+ listeners[key]();
150
+ listeners[key] = undefined;
144
151
  }
145
152
  if (value != null) {
146
153
  const event_name = get_attribute_event_name(key, value);
147
- remove_listeners[key] = event_listener(event_name, element, value);
154
+ listeners[key] = event_listener(event_name, element, value);
148
155
  }
149
156
  } else {
150
157
  set_attribute(element, key, value);
@@ -152,146 +159,257 @@ function set_attribute_helper(element, key, value, remove_listeners, prev) {
152
159
  }
153
160
 
154
161
  /**
155
- * @param {Element} element
156
- * @param {() => Record<string | symbol, any>} fn
157
- * @returns {() => void}
162
+ * The state of one element spread: the object and values it last applied,
163
+ * and the listeners and ref effects it owns. `spread()` creates it on its
164
+ * first call and is handed it back by every later one, so the compiled render
165
+ * function keeps it in its own state object, and a `render_spread` block in
166
+ * its block state.
158
167
  */
159
- export function apply_element_spread(element, fn) {
160
- /** @type {Record<string | symbol, any>} */
161
- var prev = {};
162
- /** @type {Record<string | symbol, Block | undefined>} */
163
- var effects = {};
164
- /** @type {Record<string | symbol, (() => void) | undefined>} */
165
- var remove_listeners = {};
166
-
167
- /** @type {Record<symbol, any>} */
168
- var prev_symbols = {};
169
- /** @type {Record<string, any>} */
170
- var prev_ref_props = {};
168
+ export class SpreadState {
169
+ /** @param {Element} element */
170
+ constructor(element) {
171
+ this.e = element;
172
+ /** @type {Record<string | symbol, any> | null} the object last applied, compared by identity */
173
+ this.n = null;
174
+ /** @type {Record<string, any>} the values last applied, by key */
175
+ this.p = {};
176
+ /**
177
+ * The object last applied was a tracked object, or held a tracked value:
178
+ * the same object can carry new values, so it is never skipped on identity.
179
+ */
180
+ this.t = false;
181
+ /** @type {Record<string, (() => void) | undefined> | null} listeners by event key */
182
+ this.l = null;
183
+ /** @type {Record<string | symbol, any> | null} the ref props last applied */
184
+ this.r = null;
185
+ /** @type {Record<string | symbol, Block | undefined> | null} the effects of those ref props */
186
+ this.b = null;
187
+ }
188
+ }
171
189
 
172
- return () => {
173
- var next = fn();
174
- var current_symbols = /** @type {Record<symbol, any>} */ ({});
190
+ /** What a nullish or primitive spread value applies. */
191
+ var EMPTY = {};
175
192
 
176
- for (const symbol of get_own_property_symbols(next)) {
177
- if (symbol.description !== REF_PROP) {
178
- continue;
179
- }
180
- const ref_fn = next[symbol];
181
- current_symbols[symbol] = ref_fn;
182
-
183
- if (
184
- !(symbol in prev_symbols) ||
185
- ref_fn !== prev_symbols[symbol] ||
186
- (effects[symbol] && (effects[symbol].f & DESTROYED) !== 0)
187
- ) {
188
- if (effects[symbol] && (effects[symbol].f & DESTROYED) === 0) {
189
- destroy_block(effects[symbol]);
190
- }
191
- effects[symbol] = create_spread_ref_effect(element, ref_fn);
192
- }
193
+ /**
194
+ * Applies a spread of attributes to an element, diffing against what the
195
+ * previous call applied: attributes, `class`, `style` objects, event
196
+ * listeners and ref props. An object identical to the last one applied is
197
+ * skipped outright, unless that object was a tracked object or held a
198
+ * tracked value (its contents can change while its identity does not).
199
+ * Called from a compiled render function with the state it returned before
200
+ * (`undefined` the first time), which the render function stores.
201
+ * @param {Element} element
202
+ * @param {any} next
203
+ * @param {SpreadState | undefined} state
204
+ * @returns {SpreadState}
205
+ */
206
+ export function spread(element, next, state) {
207
+ if (next == null || (typeof next !== 'object' && typeof next !== 'function')) {
208
+ next = EMPTY;
209
+ }
210
+ if (state === undefined) {
211
+ return apply_spread(new SpreadState(element), next);
212
+ }
213
+ if (state.n === next && !state.t) {
214
+ return state;
215
+ }
216
+
217
+ var prev = state.p;
218
+ /** @type {Record<string, any>} */
219
+ var current = {};
220
+ /** @type {Record<string | symbol, any> | null} */
221
+ var refs = null;
222
+ var tracked = TRACKED_OBJECT in next;
223
+ var key;
224
+
225
+ // A parent's ref prop travels through a spread on a symbol key.
226
+ var symbols = get_own_property_symbols(next);
227
+ for (var i = 0; i < symbols.length; i++) {
228
+ var symbol = symbols[i];
229
+ if (symbol.description === REF_PROP) {
230
+ refs = apply_ref_prop(state, symbol, next[symbol], refs);
193
231
  }
232
+ }
194
233
 
195
- for (const symbol of get_own_property_symbols(prev_symbols)) {
196
- if (!(symbol in current_symbols) && effects[symbol]) {
197
- destroy_block(/** @type {Block} */ (effects[symbol]));
198
- effects[symbol] = undefined;
234
+ // Listeners and attributes that are gone, or now carry a ref prop, are
235
+ // removed before anything is applied: removing `class` drops the scope
236
+ // classes too, which `#class` then adds back.
237
+ var listeners = state.l;
238
+ if (listeners !== null) {
239
+ for (key in listeners) {
240
+ if (listeners[key] !== undefined && (!(key in next) || is_ref_prop(next[key]))) {
241
+ /** @type {() => void} */ (listeners[key])();
242
+ listeners[key] = undefined;
199
243
  }
200
244
  }
245
+ }
246
+ for (key in prev) {
247
+ if (key !== '#class' && (!(key in next) || is_ref_prop(next[key]))) {
248
+ set_attribute_helper(element, key, null, state);
249
+ }
250
+ }
201
251
 
202
- prev_symbols = current_symbols;
203
-
204
- /** @type {Record<string, any>} */
205
- var current_ref_props = {};
206
- var keys = spread_keys(next);
207
-
208
- for (const key of keys) {
209
- const ref_fn = next[key];
210
- if (!is_ref_prop(ref_fn)) {
211
- continue;
212
- }
252
+ for (key in next) {
253
+ if (key === 'children') continue;
213
254
 
214
- current_ref_props[key] = ref_fn;
215
-
216
- if (
217
- !(key in prev_ref_props) ||
218
- ref_fn !== prev_ref_props[key] ||
219
- (effects[key] && (effects[key].f & DESTROYED) !== 0)
220
- ) {
221
- if (effects[key] && (effects[key].f & DESTROYED) === 0) {
222
- destroy_block(effects[key]);
223
- }
224
- effects[key] = create_spread_ref_effect(element, ref_fn);
225
- }
255
+ var value = next[key];
256
+ if (typeof value === 'function' && is_ref_prop(value)) {
257
+ refs = apply_ref_prop(state, key, value, refs);
258
+ continue;
259
+ }
260
+ if (is_ripple_object(value)) {
261
+ value = get(value);
262
+ tracked = true;
226
263
  }
264
+ current[key] = value;
227
265
 
228
- for (const key in prev_ref_props) {
229
- if (!(key in current_ref_props) && effects[key]) {
230
- destroy_block(/** @type {Block} */ (effects[key]));
231
- effects[key] = undefined;
232
- }
266
+ // The scope classes are added after every run: a `class` write may
267
+ // have replaced them.
268
+ if (key !== '#class' && key in prev && prev[key] === value) {
269
+ continue;
233
270
  }
271
+ set_attribute_helper(element, key, value, state);
272
+ }
234
273
 
235
- prev_ref_props = current_ref_props;
274
+ if (state.r !== null) {
275
+ release_ref_props(state, refs);
276
+ }
277
+ state.r = refs;
278
+ state.p = current;
279
+ state.n = next;
280
+ state.t = tracked;
281
+ return state;
282
+ }
236
283
 
237
- for (let key in remove_listeners) {
238
- // Remove event listeners that are no longer present
239
- if ((!keys.includes(key) || is_ref_prop(next[key])) && remove_listeners[key]) {
240
- remove_listeners[key]();
241
- remove_listeners[key] = undefined;
242
- }
284
+ /**
285
+ * The first run of a spread: nothing to remove or compare, every value is
286
+ * applied.
287
+ * @param {SpreadState} state
288
+ * @param {Record<string | symbol, any>} next
289
+ * @returns {SpreadState}
290
+ */
291
+ function apply_spread(state, next) {
292
+ var element = state.e;
293
+ /** @type {Record<string, any>} */
294
+ var current = {};
295
+ /** @type {Record<string | symbol, any> | null} */
296
+ var refs = null;
297
+ var tracked = TRACKED_OBJECT in next;
298
+
299
+ var symbols = get_own_property_symbols(next);
300
+ for (var i = 0; i < symbols.length; i++) {
301
+ var symbol = symbols[i];
302
+ if (symbol.description === REF_PROP) {
303
+ refs = apply_ref_prop(state, symbol, next[symbol], refs);
243
304
  }
305
+ }
244
306
 
245
- for (const key in prev) {
246
- if (!keys.includes(key) || is_ref_prop(next[key])) {
247
- if (key === '#class') {
248
- continue;
249
- }
250
- set_attribute_helper(element, key, null, remove_listeners, prev);
251
- }
252
- }
307
+ for (var key in next) {
308
+ if (key === 'children') continue;
253
309
 
254
- /** @type {typeof prev} */
255
- const current = {};
256
- for (const key of keys) {
257
- if (key === 'children') continue;
310
+ var value = next[key];
311
+ if (typeof value === 'function' && is_ref_prop(value)) {
312
+ refs = apply_ref_prop(state, key, value, refs);
313
+ continue;
314
+ }
315
+ if (is_ripple_object(value)) {
316
+ value = get(value);
317
+ tracked = true;
318
+ }
319
+ current[key] = value;
320
+ set_attribute_helper(element, key, value, state);
321
+ }
258
322
 
259
- let value = next[key];
260
- if (is_ref_prop(value)) {
261
- continue;
262
- }
263
- if (is_ripple_object(value)) {
264
- value = get(value);
265
- }
266
- current[key] = value;
323
+ state.r = refs;
324
+ state.p = current;
325
+ state.n = next;
326
+ state.t = tracked;
327
+ return state;
328
+ }
267
329
 
268
- if (key in prev && prev[key] === value && key !== '#class') {
269
- continue;
270
- }
330
+ /**
331
+ * Keeps the effect of a ref prop the spread carried before, or creates one
332
+ * for a ref prop that is new or changed.
333
+ * @param {SpreadState} state
334
+ * @param {string | symbol} key
335
+ * @param {any} ref_fn
336
+ * @param {Record<string | symbol, any> | null} refs the ref props of this run
337
+ * @returns {Record<string | symbol, any>}
338
+ */
339
+ function apply_ref_prop(state, key, ref_fn, refs) {
340
+ if (refs === null) {
341
+ refs = {};
342
+ }
343
+ refs[key] = ref_fn;
344
+
345
+ var previous = state.r;
346
+ var blocks = (state.b ??= {});
347
+ var block = blocks[key];
348
+ if (
349
+ previous === null ||
350
+ !(key in previous) ||
351
+ previous[key] !== ref_fn ||
352
+ (block !== undefined && (block.f & DESTROYED) !== 0)
353
+ ) {
354
+ if (block !== undefined && (block.f & DESTROYED) === 0) {
355
+ destroy_block(block);
356
+ }
357
+ blocks[key] = create_spread_ref_effect(state.e, ref_fn);
358
+ }
359
+ return refs;
360
+ }
271
361
 
272
- set_attribute_helper(element, key, value, remove_listeners, prev);
362
+ /**
363
+ * Destroys the effects of the ref props a spread no longer carries.
364
+ * @param {SpreadState} state
365
+ * @param {Record<string | symbol, any> | null} refs the ref props of this run
366
+ */
367
+ function release_ref_props(state, refs) {
368
+ var previous = /** @type {Record<string | symbol, any>} */ (state.r);
369
+ var blocks = /** @type {Record<string | symbol, Block | undefined>} */ (state.b);
370
+ /** @type {(string | symbol)[]} */
371
+ var keys = get_own_property_symbols(previous);
372
+ for (var key in previous) {
373
+ keys.push(key);
374
+ }
375
+ for (var i = 0; i < keys.length; i++) {
376
+ var k = keys[i];
377
+ var block = blocks[k];
378
+ if ((refs === null || !(k in refs)) && block !== undefined) {
379
+ destroy_block(block);
380
+ blocks[k] = undefined;
273
381
  }
274
- prev = current;
275
- };
382
+ }
276
383
  }
277
384
 
278
385
  /**
279
- * The keys an element spread applies: every prop of a props instance, or the
280
- * enumerable keys of a plain object.
281
- * @param {Record<string | symbol, any>} next
282
- * @returns {string[]}
386
+ * Releases what a spread owns when its element is dropped by something other
387
+ * than its block (a dynamic element replaced by a tag change): its ref
388
+ * effects, so refs receive null, and its listeners.
389
+ * @param {SpreadState | undefined} state
283
390
  */
284
- function spread_keys(next) {
285
- /** @type {string[]} */
286
- var keys = [];
287
- for (var key in next) keys.push(key);
288
- return keys;
391
+ export function release_spread(state) {
392
+ if (state === undefined) return;
393
+ var blocks = state.b;
394
+ if (blocks !== null) {
395
+ release_ref_props(state, null);
396
+ }
397
+ var listeners = state.l;
398
+ if (listeners !== null) {
399
+ for (var key in listeners) {
400
+ var remove = listeners[key];
401
+ if (remove !== undefined) {
402
+ remove();
403
+ listeners[key] = undefined;
404
+ }
405
+ }
406
+ }
289
407
  }
290
408
 
291
409
  /**
292
410
  * Keep spread refs in a branch block so ordinary spread updates do not destroy
293
- * and recreate the ref block before `apply_element_spread` can compare the
294
- * previous and current ref values.
411
+ * and recreate the ref block before `spread` can compare the previous and
412
+ * current ref values.
295
413
  *
296
414
  * @param {Element} element
297
415
  * @param {any} ref_fn
@@ -302,12 +420,27 @@ function create_spread_ref_effect(element, ref_fn) {
302
420
  ref(element, () => ref_fn);
303
421
  });
304
422
  }
423
+
305
424
  /**
306
- * A render block that applies a spread of attributes to an element.
307
- * @param {any} element
308
- * @param {any} fn
425
+ * @typedef {{ e: Element; f: () => any; s: SpreadState | undefined }} RenderSpreadState
426
+ */
427
+
428
+ /**
429
+ * @param {RenderSpreadState} s
430
+ */
431
+ function run_spread(s) {
432
+ s.s = spread(s.e, s.f(), s.s);
433
+ }
434
+
435
+ /**
436
+ * A render block that applies a spread of attributes to an element: for a
437
+ * dynamic element, whose attributes are its props literal. Compiled
438
+ * elements apply their spread from their own render function instead.
439
+ * @param {Element} element
440
+ * @param {() => any} fn
309
441
  * @param {number} [flags]
442
+ * @returns {Block}
310
443
  */
311
444
  export function render_spread(element, fn, flags = 0) {
312
- return block(RENDER_BLOCK | flags, apply_element_spread(element, fn));
445
+ return block(RENDER_BLOCK | flags, run_spread, { e: element, f: fn, s: undefined });
313
446
  }
@@ -15,6 +15,10 @@ import {
15
15
  HEAD_BLOCK,
16
16
  IF_BLOCK,
17
17
  CREATES_DERIVEDS,
18
+ COMPOSITE_BLOCK,
19
+ DEFAULT_NAMESPACE,
20
+ SVG_BLOCK,
21
+ MATHML_BLOCK,
18
22
  } from './constants.js';
19
23
  import { hydrating } from './hydration.js';
20
24
  import { next_sibling } from './operations.js';
@@ -31,6 +35,7 @@ import {
31
35
  run_teardown,
32
36
  schedule_update,
33
37
  untrack,
38
+ active_namespace,
34
39
  } from './runtime.js';
35
40
  import { is_ripple_object } from './utils.js';
36
41
  import { effect_orphan } from './errors.js';
@@ -263,7 +268,10 @@ export function create_block(flags, fn, state = null, co) {
263
268
  co: co || active_component,
264
269
  d: null,
265
270
  first: null,
266
- f: flags,
271
+ f:
272
+ active_namespace === DEFAULT_NAMESPACE
273
+ ? flags
274
+ : flags | (active_namespace === 'svg' ? SVG_BLOCK : MATHML_BLOCK),
267
275
  fn,
268
276
  i: ++block_id,
269
277
  last: null,
@@ -449,8 +457,8 @@ export function remove_block_dom(node, end) {
449
457
  export function move_block(block, target) {
450
458
  var f = block.f;
451
459
 
452
- // Only branch and if blocks (excluding TRY_BLOCK) can have DOM state to move
453
- if ((f & (BRANCH_BLOCK | IF_BLOCK)) !== 0 && (f & TRY_BLOCK) === 0) {
460
+ // Only branch, if and composite blocks (excluding TRY_BLOCK) can have DOM state to move
461
+ if ((f & (BRANCH_BLOCK | IF_BLOCK | COMPOSITE_BLOCK)) !== 0 && (f & TRY_BLOCK) === 0) {
454
462
  var s = block.s;
455
463
  if (s !== null && s.start !== null) {
456
464
  var node = s.start;
@@ -493,7 +501,7 @@ export function move_block(block, target) {
493
501
  */
494
502
  export function get_first_node(block) {
495
503
  var f = block.f;
496
- if ((f & (BRANCH_BLOCK | IF_BLOCK)) !== 0 && (f & TRY_BLOCK) === 0) {
504
+ if ((f & (BRANCH_BLOCK | IF_BLOCK | COMPOSITE_BLOCK)) !== 0 && (f & TRY_BLOCK) === 0) {
497
505
  var s = block.s;
498
506
  if (s !== null && s.start !== null) {
499
507
  return s.start;
@@ -517,7 +525,7 @@ export function get_first_node(block) {
517
525
  */
518
526
  export function get_last_node(block) {
519
527
  var f = block.f;
520
- if ((f & (BRANCH_BLOCK | IF_BLOCK)) !== 0 && (f & TRY_BLOCK) === 0) {
528
+ if ((f & (BRANCH_BLOCK | IF_BLOCK | COMPOSITE_BLOCK)) !== 0 && (f & TRY_BLOCK) === 0) {
521
529
  var s = block.s;
522
530
  if (s !== null && s.start !== null) {
523
531
  // An if's materialized anchor is its last node.
@@ -551,7 +559,9 @@ export function destroy_block(block, remove_dom = true) {
551
559
  }
552
560
 
553
561
  if (
554
- (remove_dom && (f & (BRANCH_BLOCK | ROOT_BLOCK | IF_BLOCK)) !== 0 && (f & TRY_BLOCK) === 0) ||
562
+ (remove_dom &&
563
+ (f & (BRANCH_BLOCK | ROOT_BLOCK | IF_BLOCK | COMPOSITE_BLOCK)) !== 0 &&
564
+ (f & TRY_BLOCK) === 0) ||
555
565
  (f & HEAD_BLOCK) !== 0
556
566
  ) {
557
567
  var s = block.s;