Merge branch 'main' into snippet-work

pull/10800/head
Rich Harris 3 years ago committed by GitHub
commit d7f3d45700
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: add support for webkitdirectory DOM boolean attribute

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: don't override instance methods during legacy class creation

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: add reactive Map class to svelte/reactivity

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: adjust scope parent for named slots

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve handling of unowned derived signals

@ -13,6 +13,7 @@
"afraid-moose-matter", "afraid-moose-matter",
"angry-books-jam", "angry-books-jam",
"angry-plums-punch", "angry-plums-punch",
"beige-cobras-smoke",
"beige-flies-wash", "beige-flies-wash",
"beige-mirrors-listen", "beige-mirrors-listen",
"beige-rabbits-shave", "beige-rabbits-shave",
@ -63,6 +64,7 @@
"dry-eggs-retire", "dry-eggs-retire",
"dull-coins-vanish", "dull-coins-vanish",
"dull-mangos-wave", "dull-mangos-wave",
"dull-pots-add",
"dull-roses-relate", "dull-roses-relate",
"early-ads-tie", "early-ads-tie",
"eight-steaks-shout", "eight-steaks-shout",
@ -98,6 +100,7 @@
"friendly-candles-relate", "friendly-candles-relate",
"friendly-lies-camp", "friendly-lies-camp",
"funny-wombats-argue", "funny-wombats-argue",
"fuzzy-bags-camp",
"gentle-dolls-juggle", "gentle-dolls-juggle",
"gentle-sheep-hug", "gentle-sheep-hug",
"gentle-spies-happen", "gentle-spies-happen",
@ -165,6 +168,7 @@
"lucky-schools-hang", "lucky-schools-hang",
"lucky-toes-begin", "lucky-toes-begin",
"many-trees-fix", "many-trees-fix",
"mighty-cooks-scream",
"mighty-files-hammer", "mighty-files-hammer",
"moody-carrots-lay", "moody-carrots-lay",
"moody-frogs-exist", "moody-frogs-exist",
@ -191,6 +195,7 @@
"old-mails-sneeze", "old-mails-sneeze",
"old-oranges-compete", "old-oranges-compete",
"olive-kangaroos-brake", "olive-kangaroos-brake",
"olive-mice-fix",
"olive-seals-sell", "olive-seals-sell",
"olive-shirts-complain", "olive-shirts-complain",
"olive-socks-kick", "olive-socks-kick",
@ -232,8 +237,10 @@
"rotten-rules-invite", "rotten-rules-invite",
"rude-ghosts-tickle", "rude-ghosts-tickle",
"selfish-dragons-knock", "selfish-dragons-knock",
"selfish-spies-help",
"selfish-tools-hide", "selfish-tools-hide",
"serious-kids-deliver", "serious-kids-deliver",
"serious-needles-joke",
"serious-socks-cover", "serious-socks-cover",
"serious-zebras-scream", "serious-zebras-scream",
"seven-deers-jam", "seven-deers-jam",
@ -263,6 +270,7 @@
"small-papayas-laugh", "small-papayas-laugh",
"small-sheep-type", "small-sheep-type",
"smart-parents-swim", "smart-parents-swim",
"smart-turkeys-tell",
"smart-zebras-pay", "smart-zebras-pay",
"smooth-rings-rush", "smooth-rings-rush",
"soft-clocks-remember", "soft-clocks-remember",
@ -304,6 +312,7 @@
"tasty-cheetahs-appear", "tasty-cheetahs-appear",
"tasty-numbers-perform", "tasty-numbers-perform",
"tasty-steaks-smile", "tasty-steaks-smile",
"ten-eels-move",
"ten-foxes-repeat", "ten-foxes-repeat",
"ten-jokes-divide", "ten-jokes-divide",
"ten-peaches-sleep", "ten-peaches-sleep",
@ -344,6 +353,7 @@
"wild-foxes-wonder", "wild-foxes-wonder",
"wise-apples-care", "wise-apples-care",
"wise-dancers-hang", "wise-dancers-hang",
"wise-dodos-tell",
"wise-donkeys-marry", "wise-donkeys-marry",
"wise-jobs-admire", "wise-jobs-admire",
"wise-radios-exercise", "wise-radios-exercise",

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve element class attribute behaviour

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: add types for svelte/reactivity

@ -0,0 +1,5 @@
---
"svelte": patch
---
breaking: apply fallback value every time in runes mode

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure select value is updated upon select option removal

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure capture events don't call delegated events

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure arguments are supported on all reactive Date methods

@ -11,3 +11,5 @@
/motion.d.ts /motion.d.ts
/store.d.ts /store.d.ts
/transition.d.ts /transition.d.ts
/scripts/_bundle.js

@ -1,5 +1,37 @@
# svelte # svelte
## 5.0.0-next.81
### Patch Changes
- feat: add support for webkitdirectory DOM boolean attribute ([#10847](https://github.com/sveltejs/svelte/pull/10847))
- fix: don't override instance methods during legacy class creation ([#10834](https://github.com/sveltejs/svelte/pull/10834))
- fix: adjust scope parent for named slots ([#10843](https://github.com/sveltejs/svelte/pull/10843))
- fix: improve handling of unowned derived signals ([#10842](https://github.com/sveltejs/svelte/pull/10842))
- fix: improve element class attribute behaviour ([#10856](https://github.com/sveltejs/svelte/pull/10856))
- fix: ensure select value is updated upon select option removal ([#10846](https://github.com/sveltejs/svelte/pull/10846))
- fix: ensure capture events don't call delegated events ([#10831](https://github.com/sveltejs/svelte/pull/10831))
## 5.0.0-next.80
### Patch Changes
- fix: add types for svelte/reactivity ([#10817](https://github.com/sveltejs/svelte/pull/10817))
- fix: ensure arguments are supported on all reactive Date methods ([#10813](https://github.com/sveltejs/svelte/pull/10813))
## 5.0.0-next.79
### Patch Changes
- feat: add reactive Map class to svelte/reactivity ([#10803](https://github.com/sveltejs/svelte/pull/10803))
## 5.0.0-next.78 ## 5.0.0-next.78
### Patch Changes ### Patch Changes

@ -1056,6 +1056,7 @@ export interface HTMLInputAttributes extends HTMLAttributes<HTMLInputElement> {
type?: HTMLInputTypeAttribute | undefined | null; type?: HTMLInputTypeAttribute | undefined | null;
value?: any; value?: any;
width?: number | string | undefined | null; width?: number | string | undefined | null;
webkitdirectory?: boolean | undefined | null;
'on:change'?: ChangeEventHandler<HTMLInputElement> | undefined | null; 'on:change'?: ChangeEventHandler<HTMLInputElement> | undefined | null;
onchange?: ChangeEventHandler<HTMLInputElement> | undefined | null; onchange?: ChangeEventHandler<HTMLInputElement> | undefined | null;

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.0.0-next.78", "version": "5.0.0-next.81",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {

@ -12,6 +12,15 @@ async function bundle_code(entry) {
virtual({ virtual({
__entry__: entry __entry__: entry
}), }),
{
name: 'resolve-svelte',
resolveId(importee) {
if (importee.startsWith('svelte')) {
const entry = pkg.exports[importee.replace('svelte', '.')];
return path.resolve(entry.browser ?? entry.default);
}
}
},
nodeResolve({ nodeResolve({
exportConditions: ['production', 'import', 'browser', 'default'] exportConditions: ['production', 'import', 'browser', 'default']
}) })
@ -65,7 +74,7 @@ for (const key in pkg.exports) {
} }
const client_main = path.resolve(pkg.exports['.'].browser); const client_main = path.resolve(pkg.exports['.'].browser);
const without_hydration = await bundle_code( const bundle = await bundle_code(
// Use all features which contain hydration code to ensure it's treeshakeable // Use all features which contain hydration code to ensure it's treeshakeable
compile( compile(
` `
@ -99,15 +108,18 @@ const without_hydration = await bundle_code(
{ filename: 'App.svelte' } { filename: 'App.svelte' }
).js.code ).js.code
); );
if (!without_hydration.includes('current_hydration_fragment')) {
if (!bundle.includes('current_hydration_fragment')) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(`✅ Hydration code treeshakeable`); console.error(`✅ Hydration code treeshakeable`);
} else { } else {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(without_hydration); console.error(bundle);
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(`❌ Hydration code not treeshakeable`); console.error(`❌ Hydration code not treeshakeable`);
failed = true; failed = true;
fs.writeFileSync('scripts/_bundle.js', bundle);
} }
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

@ -29,6 +29,7 @@ await createBundle({
[`${pkg.name}/easing`]: `${dir}/src/easing/index.js`, [`${pkg.name}/easing`]: `${dir}/src/easing/index.js`,
[`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`, [`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`,
[`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`, [`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`,
[`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index.js`,
[`${pkg.name}/server`]: `${dir}/src/server/index.js`, [`${pkg.name}/server`]: `${dir}/src/server/index.js`,
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`, [`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`, [`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,

@ -1072,6 +1072,7 @@ const common_visitors = {
} }
if ( if (
context.state.analysis.runes &&
node !== binding.node && node !== binding.node &&
// If we have $state that can be proxied or frozen and isn't re-assigned, then that means // If we have $state that can be proxied or frozen and isn't re-assigned, then that means
// it's likely not using a primitive value and thus this warning isn't that helpful. // it's likely not using a primitive value and thus this warning isn't that helpful.

@ -255,10 +255,23 @@ export function client_component(source, analysis, options) {
const key = binding.prop_alias ?? name; const key = binding.prop_alias ?? name;
properties.push( const getter = b.get(key, [b.return(b.call(b.id(name)))]);
b.get(key, [b.return(b.call(b.id(name)))]),
b.set(key, [b.stmt(b.call(b.id(name), b.id('$$value'))), b.stmt(b.call('$.flushSync'))]) const setter = b.set(key, [
); b.stmt(b.call(b.id(name), b.id('$$value'))),
b.stmt(b.call('$.flushSync'))
]);
if (analysis.runes && binding.initial) {
// turn `set foo($$value)` into `set foo($$value = expression)`
setter.value.params[0] = {
type: 'AssignmentPattern',
left: b.id('$$value'),
right: /** @type {import('estree').Expression} */ (binding.initial)
};
}
properties.push(getter, setter);
} }
} }

@ -28,10 +28,14 @@ import {
AttributeAliases, AttributeAliases,
DOMBooleanAttributes, DOMBooleanAttributes,
EACH_INDEX_REACTIVE, EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED,
EACH_IS_CONTROLLED, EACH_IS_CONTROLLED,
EACH_IS_STRICT_EQUALS, EACH_IS_STRICT_EQUALS,
EACH_ITEM_REACTIVE, EACH_ITEM_REACTIVE,
EACH_KEYED EACH_KEYED,
TRANSITION_GLOBAL,
TRANSITION_IN,
TRANSITION_OUT
} from '../../../../../constants.js'; } from '../../../../../constants.js';
import { regex_is_valid_identifier } from '../../../patterns.js'; import { regex_is_valid_identifier } from '../../../patterns.js';
import { javascript_visitors_runes } from './javascript-runes.js'; import { javascript_visitors_runes } from './javascript-runes.js';
@ -158,26 +162,6 @@ function serialize_class_directives(class_directives, element_id, context, is_at
} }
} }
/**
*
* @param {string | null} spread_id
* @param {import('#compiler').RegularElement} node
* @param {import('../types.js').ComponentContext} context
* @param {import('estree').Identifier} node_id
*/
function add_select_to_spread_update(spread_id, node, context, node_id) {
if (spread_id !== null && node.name === 'select') {
context.state.update.push({
grouped: b.if(
b.binary('in', b.literal('value'), b.id(spread_id)),
b.block([
b.stmt(b.call('$.select_option', node_id, b.member(b.id(spread_id), b.id('value'))))
])
)
});
}
}
/** /**
* @param {import('#compiler').Binding[]} references * @param {import('#compiler').Binding[]} references
* @param {import('../types.js').ComponentContext} context * @param {import('../types.js').ComponentContext} context
@ -223,6 +207,8 @@ function collect_transitive_dependencies(binding, seen = new Set()) {
* @param {import('../types.js').ComponentContext} context * @param {import('../types.js').ComponentContext} context
*/ */
function setup_select_synchronization(value_binding, context) { function setup_select_synchronization(value_binding, context) {
if (context.state.analysis.runes) return;
let bound = value_binding.expression; let bound = value_binding.expression;
while (bound.type === 'MemberExpression') { while (bound.type === 'MemberExpression') {
bound = /** @type {import('estree').Identifier | import('estree').MemberExpression} */ ( bound = /** @type {import('estree').Identifier | import('estree').MemberExpression} */ (
@ -243,7 +229,6 @@ function setup_select_synchronization(value_binding, context) {
} }
} }
if (!context.state.analysis.runes) {
const invalidator = b.call( const invalidator = b.call(
'$.invalidate_inner_signals', '$.invalidate_inner_signals',
b.thunk( b.thunk(
@ -272,30 +257,21 @@ function setup_select_synchronization(value_binding, context) {
) )
); );
} }
}
/** /**
* Serializes element attribute assignments that contain spreads to either only
* the init or the the init and update arrays, depending on whether or not the value is dynamic.
* Resulting code for static looks something like this:
* ```js
* $.spread_attributes(element, null, [...]);
* ```
* Resulting code for dynamic looks something like this:
* ```js
* let value;
* $.render_effect(() => {
* value = $.spread_attributes(element, value, [...])
* });
* ```
* Returns the id of the spread_attribute variable if spread isn't isolated, `null` otherwise.
* @param {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} attributes * @param {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} attributes
* @param {import('../types.js').ComponentContext} context * @param {import('../types.js').ComponentContext} context
* @param {import('#compiler').RegularElement} element * @param {import('#compiler').RegularElement} element
* @param {import('estree').Identifier} element_id * @param {import('estree').Identifier} element_id
* @returns {string | null} * @param {boolean} needs_select_handling
*/ */
function serialize_element_spread_attributes(attributes, context, element, element_id) { function serialize_element_spread_attributes(
attributes,
context,
element,
element_id,
needs_select_handling
) {
let needs_isolation = false; let needs_isolation = false;
/** @type {import('estree').Expression[]} */ /** @type {import('estree').Expression[]} */
@ -317,8 +293,9 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
const lowercase_attributes = const lowercase_attributes =
element.metadata.svg || is_custom_element_node(element) ? b.false : b.true; element.metadata.svg || is_custom_element_node(element) ? b.false : b.true;
const id = context.state.scope.generate('spread_attributes');
const isolated = b.stmt( const standalone = b.stmt(
b.call( b.call(
'$.spread_attributes_effect', '$.spread_attributes_effect',
element_id, element_id,
@ -327,17 +304,7 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
b.literal(context.state.analysis.css.hash) b.literal(context.state.analysis.css.hash)
) )
); );
const inside_effect = b.stmt(
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
if (needs_isolation) {
context.state.update_effects.push(isolated);
return null;
} else {
const id = context.state.scope.generate('spread_attributes');
context.state.init.push(b.let(id));
context.state.update.push({
singular: isolated,
grouped: b.stmt(
b.assignment( b.assignment(
'=', '=',
b.id(id), b.id(id),
@ -350,9 +317,44 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
b.literal(context.state.analysis.css.hash) b.literal(context.state.analysis.css.hash)
) )
) )
);
if (!needs_isolation || needs_select_handling) {
context.state.init.push(b.let(id));
}
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
if (needs_isolation) {
if (needs_select_handling) {
context.state.update_effects.push(
b.stmt(b.call('$.render_effect', b.arrow([], b.block([inside_effect]))))
);
} else {
context.state.update_effects.push(standalone);
}
} else {
context.state.update.push({
singular: needs_select_handling ? undefined : standalone,
grouped: inside_effect
});
}
if (needs_select_handling) {
context.state.init.push(
b.stmt(b.call('$.init_select', element_id, b.thunk(b.member(b.id(id), b.id('value')))))
);
context.state.update.push({
grouped: b.if(
b.binary('in', b.literal('value'), b.id(id)),
b.block([
// This ensures a one-way street to the DOM in case it's <select {value}>
// and not <select bind:value>. We need it in addition to $.init_select
// because the select value is not reflected as an attribute, so the
// mutation observer wouldn't notice.
b.stmt(b.call('$.select_option', element_id, b.member(b.id(id), b.id('value'))))
])
) )
}); });
return id;
} }
} }
@ -475,6 +477,7 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) {
function serialize_element_attribute_update_assignment(element, node_id, attribute, context) { function serialize_element_attribute_update_assignment(element, node_id, attribute, context) {
const state = context.state; const state = context.state;
const name = get_attribute_name(element, attribute, context); const name = get_attribute_name(element, attribute, context);
const is_svg = context.state.metadata.namespace === 'svg';
let [contains_call_expression, value] = serialize_attribute_value(attribute.value, context); let [contains_call_expression, value] = serialize_attribute_value(attribute.value, context);
// The foreign namespace doesn't have any special handling, everything goes through the attr function // The foreign namespace doesn't have any special handling, everything goes through the attr function
@ -509,13 +512,19 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
if (name === 'class') { if (name === 'class') {
if (singular) { if (singular) {
return { return {
singular: b.stmt(b.call('$.class_name_effect', node_id, b.thunk(singular))), singular: b.stmt(
grouped: b.stmt(b.call('$.class_name', node_id, singular)), b.call(
is_svg ? '$.svg_class_name_effect' : '$.class_name_effect',
node_id,
b.thunk(singular)
)
),
grouped: b.stmt(b.call(is_svg ? '$.svg_class_name' : '$.class_name', node_id, singular)),
skip_condition: true skip_condition: true
}; };
} }
return { return {
grouped: b.stmt(b.call('$.class_name', node_id, value)), grouped: b.stmt(b.call(is_svg ? '$.svg_class_name' : '$.class_name', node_id, value)),
skip_condition: true skip_condition: true
}; };
} else if (!DOMProperties.includes(name)) { } else if (!DOMProperties.includes(name)) {
@ -644,27 +653,27 @@ function serialize_element_special_value_attribute(element, node_id, attribute,
) )
); );
const is_reactive = attribute.metadata.dynamic; const is_reactive = attribute.metadata.dynamic;
const needs_selected_call = const is_select_with_value =
element === 'option' && (is_reactive || collect_parent_each_blocks(context).length > 0); // attribute.metadata.dynamic would give false negatives because even if the value does not change,
const needs_option_call = element === 'select' && is_reactive; // the inner options could still change, so we need to always treat it as reactive
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
const assignment = b.stmt( const assignment = b.stmt(
needs_selected_call is_select_with_value
? b.sequence([
inner_assignment,
// This ensures things stay in sync with the select binding
// in case of updates to the option value or new values appearing
b.call('$.selected', node_id)
])
: needs_option_call
? b.sequence([ ? b.sequence([
inner_assignment, inner_assignment,
// This ensures a one-way street to the DOM in case it's <select {value}> // This ensures a one-way street to the DOM in case it's <select {value}>
// and not <select bind:value> // and not <select bind:value>. We need it in addition to $.init_select
// because the select value is not reflected as an attribute, so the
// mutation observer wouldn't notice.
b.call('$.select_option', node_id, value) b.call('$.select_option', node_id, value)
]) ])
: inner_assignment : inner_assignment
); );
if (is_select_with_value) {
state.init.push(b.stmt(b.call('$.init_select', node_id, b.thunk(value))));
}
if (is_reactive) { if (is_reactive) {
const id = state.scope.generate(`${node_id.name}_value`); const id = state.scope.generate(`${node_id.name}_value`);
serialize_update_assignment( serialize_update_assignment(
@ -1279,6 +1288,12 @@ function create_block(parent, name, nodes, context) {
// It's important that close is the last statement in the block, as any previous statements // It's important that close is the last statement in the block, as any previous statements
// could contain element insertions into the template, which the close statement needs to // could contain element insertions into the template, which the close statement needs to
// know of when constructing the list of current inner elements. // know of when constructing the list of current inner elements.
if (context.path.length > 0) {
// this is a block — return DOM so it can be attached directly to the effect
close = b.return(close.expression);
}
body.push(close); body.push(close);
} }
@ -1923,7 +1938,7 @@ export const template_visitors = {
state.init.push( state.init.push(
b.stmt( b.stmt(
b.call( b.call(
'$.animate', '$.animation',
state.node, state.node,
b.thunk( b.thunk(
/** @type {import('estree').Expression} */ (visit(parse_directive_name(node.name))) /** @type {import('estree').Expression} */ (visit(parse_directive_name(node.name)))
@ -1940,25 +1955,21 @@ export const template_visitors = {
error(node, 'INTERNAL', 'Node should have been handled elsewhere'); error(node, 'INTERNAL', 'Node should have been handled elsewhere');
}, },
TransitionDirective(node, { state, visit }) { TransitionDirective(node, { state, visit }) {
const type = node.intro && node.outro ? '$.transition' : node.intro ? '$.in' : '$.out'; let flags = node.modifiers.includes('global') ? TRANSITION_GLOBAL : 0;
const expression = if (node.intro) flags |= TRANSITION_IN;
node.expression === null if (node.outro) flags |= TRANSITION_OUT;
? b.literal(null)
: b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression)));
state.init.push( const args = [
b.stmt( b.literal(flags),
b.call(
type,
state.node, state.node,
b.thunk( b.thunk(/** @type {import('estree').Expression} */ (visit(parse_directive_name(node.name))))
/** @type {import('estree').Expression} */ (visit(parse_directive_name(node.name))) ];
),
expression, if (node.expression) {
node.modifiers.includes('global') ? b.true : b.false args.push(b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression))));
) }
)
); state.init.push(b.stmt(b.call('$.transition', ...args)));
}, },
RegularElement(node, context) { RegularElement(node, context) {
if (node.name === 'noscript') { if (node.name === 'noscript') {
@ -2088,11 +2099,15 @@ export const template_visitors = {
// Then do attributes // Then do attributes
let is_attributes_reactive = false; let is_attributes_reactive = false;
if (node.metadata.has_spread) { if (node.metadata.has_spread) {
const spread_id = serialize_element_spread_attributes(attributes, context, node, node_id); serialize_element_spread_attributes(
if (child_metadata.namespace !== 'foreign') { attributes,
add_select_to_spread_update(spread_id, node, context, node_id); context,
} node,
is_attributes_reactive = spread_id !== null; node_id,
// If value binding exists, that one takes care of calling $.init_select
value_binding === null && node.name === 'select' && child_metadata.namespace !== 'foreign'
);
is_attributes_reactive = true;
} else { } else {
for (const attribute of /** @type {import('#compiler').Attribute[]} */ (attributes)) { for (const attribute of /** @type {import('#compiler').Attribute[]} */ (attributes)) {
if (is_event_attribute(attribute)) { if (is_event_attribute(attribute)) {
@ -2342,6 +2357,19 @@ export const template_visitors = {
each_type |= EACH_ITEM_REACTIVE; each_type |= EACH_ITEM_REACTIVE;
} }
// Since `animate:` can only appear on elements that are the sole child of a keyed each block,
// we can determine at compile time whether the each block is animated or not (in which
// case it should measure animated elements before and after reconciliation).
if (
node.key &&
node.body.nodes.some((child) => {
if (child.type !== 'RegularElement' && child.type !== 'SvelteElement') return false;
return child.attributes.some((attr) => attr.type === 'AnimateDirective');
})
) {
each_type |= EACH_IS_ANIMATED;
}
if (each_node_meta.is_controlled) { if (each_node_meta.is_controlled) {
each_type |= EACH_IS_CONTROLLED; each_type |= EACH_IS_CONTROLLED;
} }
@ -2554,10 +2582,7 @@ export const template_visitors = {
context.visit(node.consequent) context.visit(node.consequent)
); );
context.state.after_update.push( const args = [
b.stmt(
b.call(
'$.if',
context.state.node, context.state.node,
b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.test))), b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.test))),
b.arrow([b.id('$$anchor')], consequent), b.arrow([b.id('$$anchor')], consequent),
@ -2567,9 +2592,34 @@ export const template_visitors = {
/** @type {import('estree').BlockStatement} */ (context.visit(node.alternate)) /** @type {import('estree').BlockStatement} */ (context.visit(node.alternate))
) )
: b.literal(null) : b.literal(null)
) ];
)
); if (node.elseif) {
// We treat this...
//
// {#if x}
// ...
// {:else}
// {#if y}
// <div transition:foo>...</div>
// {/if}
// {/if}
//
// ...slightly differently to this...
//
// {#if x}
// ...
// {:else if y}
// <div transition:foo>...</div>
// {/if}
//
// ...even though they're logically equivalent. In the first case, the
// transition will only play when `y` changes, but in the second it
// should play when `x` or `y` change — both are considered 'local'
args.push(b.literal(true));
}
context.state.after_update.push(b.stmt(b.call('$.if', ...args)));
}, },
AwaitBlock(node, context) { AwaitBlock(node, context) {
context.state.template.push('<!>'); context.state.template.push('<!>');

@ -400,9 +400,9 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
(attribute) => attribute.type === 'Attribute' && attribute.name === 'slot' (attribute) => attribute.type === 'Attribute' && attribute.name === 'slot'
) )
) { ) {
// <div slot="..."> inherits the scope above the component, because slots are hella weird // <div slot="..."> inherits the scope above the component unless the component is a named slot itself, because slots are hella weird
scopes.set(child, state.scope); scopes.set(child, is_default_slot ? state.scope : scope);
visit(child); visit(child, { scope: is_default_slot ? state.scope : scope });
} else { } else {
if (child.type === 'ExpressionTag') { if (child.type === 'ExpressionTag') {
// expression tag is a special case — we don't visit it directly, but via process_children, // expression tag is a special case — we don't visit it directly, but via process_children,
@ -599,26 +599,38 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}, },
AwaitBlock(node, context) { AwaitBlock(node, context) {
context.next(); context.visit(node.expression);
if (node.pending) {
context.visit(node.pending);
}
if (node.then && node.value !== null) { if (node.then) {
context.visit(node.then);
if (node.value) {
const then_scope = /** @type {Scope} */ (scopes.get(node.then)); const then_scope = /** @type {Scope} */ (scopes.get(node.then));
const value_scope = context.state.scope.child(); const value_scope = context.state.scope.child();
scopes.set(node.value, value_scope);
context.visit(node.value, { scope: value_scope });
for (const id of extract_identifiers(node.value)) { for (const id of extract_identifiers(node.value)) {
then_scope.declare(id, 'normal', 'const'); then_scope.declare(id, 'normal', 'const');
value_scope.declare(id, 'normal', 'const'); value_scope.declare(id, 'normal', 'const');
} }
scopes.set(node.value, value_scope); }
} }
if (node.catch && node.error !== null) { if (node.catch) {
context.visit(node.catch);
if (node.error) {
const catch_scope = /** @type {Scope} */ (scopes.get(node.catch)); const catch_scope = /** @type {Scope} */ (scopes.get(node.catch));
const error_scope = context.state.scope.child(); const error_scope = context.state.scope.child();
scopes.set(node.error, error_scope);
context.visit(node.error, { scope: error_scope });
for (const id of extract_identifiers(node.error)) { for (const id of extract_identifiers(node.error)) {
catch_scope.declare(id, 'normal', 'const'); catch_scope.declare(id, 'normal', 'const');
error_scope.declare(id, 'normal', 'const'); error_scope.declare(id, 'normal', 'const');
} }
scopes.set(node.error, error_scope); }
} }
}, },

@ -210,7 +210,7 @@ export function function_declaration(id, params, body) {
/** /**
* @param {string} name * @param {string} name
* @param {import('estree').Statement[]} body * @param {import('estree').Statement[]} body
* @returns {import('estree').Property} * @returns {import('estree').Property & { value: import('estree').FunctionExpression}}}
*/ */
export function get(name, body) { export function get(name, body) {
return prop('get', key(name), function_builder(null, [], block(body))); return prop('get', key(name), function_builder(null, [], block(body)));
@ -305,11 +305,12 @@ export function object_pattern(properties) {
} }
/** /**
* @template {import('estree').Expression} Value
* @param {'init' | 'get' | 'set'} kind * @param {'init' | 'get' | 'set'} kind
* @param {import('estree').Expression} key * @param {import('estree').Expression} key
* @param {import('estree').Expression} value * @param {Value} value
* @param {boolean} computed * @param {boolean} computed
* @returns {import('estree').Property} * @returns {import('estree').Property & { value: Value }}
*/ */
export function prop(kind, key, value, computed = false) { export function prop(kind, key, value, computed = false) {
return { type: 'Property', kind, key, value, method: false, shorthand: false, computed }; return { type: 'Property', kind, key, value, method: false, shorthand: false, computed };
@ -355,7 +356,7 @@ export function sequence(expressions) {
/** /**
* @param {string} name * @param {string} name
* @param {import('estree').Statement[]} body * @param {import('estree').Statement[]} body
* @returns {import('estree').Property} * @returns {import('estree').Property & { value: import('estree').FunctionExpression}}
*/ */
export function set(name, body) { export function set(name, body) {
return prop('set', key(name), function_builder(null, [id('$$value')], block(body))); return prop('set', key(name), function_builder(null, [id('$$value')], block(body)));

@ -12,6 +12,10 @@ export const PROPS_IS_RUNES = 1 << 1;
export const PROPS_IS_UPDATED = 1 << 2; export const PROPS_IS_UPDATED = 1 << 2;
export const PROPS_IS_LAZY_INITIAL = 1 << 3; export const PROPS_IS_LAZY_INITIAL = 1 << 3;
export const TRANSITION_IN = 1;
export const TRANSITION_OUT = 1 << 1;
export const TRANSITION_GLOBAL = 1 << 2;
/** List of Element events that will be delegated */ /** List of Element events that will be delegated */
export const DelegatedEvents = [ export const DelegatedEvents = [
'beforeinput', 'beforeinput',
@ -83,7 +87,8 @@ export const DOMBooleanAttributes = [
'required', 'required',
'reversed', 'reversed',
'seamless', 'seamless',
'selected' 'selected',
'webkitdirectory'
]; ];
export const namespace_svg = 'http://www.w3.org/2000/svg'; export const namespace_svg = 'http://www.w3.org/2000/svg';

@ -9,17 +9,8 @@ export const DIRTY = 1 << 9;
export const MAYBE_DIRTY = 1 << 10; export const MAYBE_DIRTY = 1 << 10;
export const INERT = 1 << 11; export const INERT = 1 << 11;
export const DESTROYED = 1 << 12; export const DESTROYED = 1 << 12;
export const IS_ELSEIF = 1 << 13;
export const ROOT_BLOCK = 0; export const EFFECT_RAN = 1 << 14;
export const IF_BLOCK = 1;
export const EACH_BLOCK = 2;
export const EACH_ITEM_BLOCK = 3;
export const AWAIT_BLOCK = 4;
export const KEY_BLOCK = 5;
export const HEAD_BLOCK = 6;
export const DYNAMIC_COMPONENT_BLOCK = 7;
export const DYNAMIC_ELEMENT_BLOCK = 8;
export const SNIPPET_BLOCK = 9;
export const UNINITIALIZED = Symbol(); export const UNINITIALIZED = Symbol();
export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL = Symbol('$state');

@ -1,194 +1,132 @@
import { is_promise } from '../../../common.js'; import { is_promise } from '../../../common.js';
import { hydrate_block_anchor } from '../hydration.js'; import { hydrate_block_anchor } from '../hydration.js';
import { remove } from '../reconciler.js'; import { remove } from '../reconciler.js';
import { current_block, execute_effect, flushSync } from '../../runtime.js'; import {
import { destroy_effect, render_effect } from '../../reactivity/effects.js'; current_component_context,
import { trigger_transitions } from '../elements/transitions.js'; flushSync,
import { AWAIT_BLOCK, UNINITIALIZED } from '../../constants.js'; set_current_component_context,
set_current_effect,
/** @returns {import('../../types.js').AwaitBlock} */ set_current_reaction
export function create_await_block() { } from '../../runtime.js';
return { import { destroy_effect, pause_effect, render_effect } from '../../reactivity/effects.js';
// dom import { DESTROYED, INERT } from '../../constants.js';
d: null, import { create_block } from './utils.js';
// effect
e: null,
// parent
p: /** @type {import('../../types.js').Block} */ (current_block),
// pending
n: true,
// transition
r: null,
// type
t: AWAIT_BLOCK
};
}
/** /**
* @template V * @template V
* @param {Comment} anchor_node * @param {Comment} anchor
* @param {(() => Promise<V>)} input * @param {(() => Promise<V>)} get_input
* @param {null | ((anchor: Node) => void)} pending_fn * @param {null | ((anchor: Node) => void)} pending_fn
* @param {null | ((anchor: Node, value: V) => void)} then_fn * @param {null | ((anchor: Node, value: V) => void)} then_fn
* @param {null | ((anchor: Node, error: unknown) => void)} catch_fn * @param {null | ((anchor: Node, error: unknown) => void)} catch_fn
* @returns {void} * @returns {void}
*/ */
export function await_block(anchor_node, input, pending_fn, then_fn, catch_fn) { export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
const block = create_await_block(); const block = create_block();
const component_context = current_component_context;
/** @type {null | import('../../types.js').Render} */ hydrate_block_anchor(anchor);
let current_render = null;
hydrate_block_anchor(anchor_node);
/** @type {{}} */ /** @type {any} */
let latest_token; let input;
/** @type {typeof UNINITIALIZED | V} */ /** @type {import('#client').Effect | null} */
let resolved_value = UNINITIALIZED; let pending_effect;
/** @type {import('#client').Effect | null} */
let then_effect;
/** @type {import('#client').Effect | null} */
let catch_effect;
/** @type {unknown} */
let error = UNINITIALIZED;
let pending = false;
block.r =
/** /**
* @param {import('../../types.js').Transition} transition * @param {(anchor: Comment, value: any) => void} fn
* @returns {void} * @param {any} value
*/ */
(transition) => { function create_effect(fn, value) {
const render = /** @type {import('../../types.js').Render} */ (current_render); set_current_effect(branch);
const transitions = render.s; set_current_reaction(branch); // TODO do we need both?
transitions.add(transition); set_current_component_context(component_context);
transition.f(() => { var effect = render_effect(() => fn(anchor, value), {}, true);
transitions.delete(transition); set_current_component_context(null);
if (transitions.size === 0) { set_current_reaction(null);
// If the current render has changed since, then we can remove the old render set_current_effect(null);
// effect as it's stale.
if (current_render !== render && render.e !== null) { // without this, the DOM does not update until two ticks after the promise,
if (render.d !== null) { // resolves which is unexpected behaviour (and somewhat irksome to test)
remove(render.d); flushSync();
render.d = null;
} return effect;
destroy_effect(render.e);
render.e = null;
}
} }
/** @param {import('#client').Effect} effect */
function pause(effect) {
if ((effect.f & DESTROYED) !== 0) return;
const block = effect.block;
pause_effect(effect, () => {
// TODO make this unnecessary
const dom = block?.d;
if (dom) remove(dom);
}); });
};
const create_render_effect = () => {
/** @type {import('../../types.js').Render} */
const render = {
d: null,
e: null,
s: new Set(),
p: current_render
};
const effect = render_effect(
() => {
if (error === UNINITIALIZED) {
if (resolved_value === UNINITIALIZED) {
// pending = true
block.n = true;
if (pending_fn !== null) {
pending_fn(anchor_node);
}
} else if (then_fn !== null) {
// pending = false
block.n = false;
then_fn(anchor_node, resolved_value);
}
} else if (catch_fn !== null) {
// pending = false
block.n = false;
catch_fn(anchor_node, error);
}
render.d = block.d;
block.d = null;
},
block,
true,
true
);
render.e = effect;
current_render = render;
};
const render = () => {
const render = current_render;
if (render === null) {
create_render_effect();
return;
}
const transitions = render.s;
if (transitions.size === 0) {
if (render.d !== null) {
remove(render.d);
render.d = null;
} }
if (render.e) {
execute_effect(render.e); const branch = render_effect(() => {
} else { if (input === (input = get_input())) return;
create_render_effect();
if (is_promise(input)) {
const promise = /** @type {Promise<any>} */ (input);
if (pending_fn) {
if (pending_effect && (pending_effect.f & INERT) === 0) {
if (pending_effect.block?.d) remove(pending_effect.block.d);
destroy_effect(pending_effect);
} }
} else {
create_render_effect(); pending_effect = render_effect(() => pending_fn(anchor), {}, true);
trigger_transitions(transitions, 'out');
} }
};
const await_effect = render_effect( if (then_effect) pause(then_effect);
() => { if (catch_effect) pause(catch_effect);
const token = {};
latest_token = token;
const promise = input();
if (is_promise(promise)) {
promise.then( promise.then(
/** @param {V} v */ (value) => {
(v) => { if (promise !== input) return;
if (latest_token === token) { if (pending_effect) pause(pending_effect);
// Ensure UI is in sync before resolving value.
flushSync(); if (then_fn) {
resolved_value = v; then_effect = create_effect(then_fn, value);
pending = false;
render();
} }
}, },
/** @param {unknown} _error */ (error) => {
(_error) => { if (promise !== input) return;
error = _error; if (pending_effect) pause(pending_effect);
pending = false;
render(); if (catch_fn) {
} catch_effect = create_effect(catch_fn, error);
);
if (resolved_value !== UNINITIALIZED || error !== UNINITIALIZED) {
error = UNINITIALIZED;
resolved_value = UNINITIALIZED;
}
if (!pending) {
pending = true;
render();
} }
} else {
error = UNINITIALIZED;
resolved_value = promise;
pending = false;
render();
} }
},
block,
false
); );
await_effect.ondestroy = () => { } else {
let render = current_render; if (pending_effect) pause(pending_effect);
latest_token = {}; if (catch_effect) pause(catch_effect);
while (render !== null) {
const dom = render.d; if (then_fn) {
if (dom !== null) { if (then_effect) {
remove(dom); if (then_effect.block?.d) remove(then_effect.block.d);
destroy_effect(then_effect);
} }
const effect = render.e;
if (effect !== null) { then_effect = render_effect(() => then_fn(anchor, input), {}, true);
destroy_effect(effect);
} }
render = render.p;
} }
}, block);
branch.ondestroy = () => {
// TODO this sucks, tidy it up
if (pending_effect?.block?.d) remove(pending_effect.block.d);
if (then_effect?.block?.d) remove(then_effect.block.d);
if (catch_effect?.block?.d) remove(catch_effect.block.d);
}; };
block.e = await_effect;
} }

@ -2,7 +2,7 @@ import { namespace_svg } from '../../../../constants.js';
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js'; import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js';
import { empty } from '../operations.js'; import { empty } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js'; import { render_effect } from '../../reactivity/effects.js';
import { insert, remove } from '../reconciler.js'; import { remove } from '../reconciler.js';
/** /**
* @param {Element | Text | Comment} anchor * @param {Element | Text | Comment} anchor
@ -33,7 +33,7 @@ export function css_props(anchor, is_html, props, component) {
tag = document.createElementNS(namespace_svg, 'g'); tag = document.createElementNS(namespace_svg, 'g');
} }
insert(tag, null, anchor); anchor.before(tag);
component_anchor = empty(); component_anchor = empty();
tag.appendChild(component_anchor); tag.appendChild(component_anchor);
} }

File diff suppressed because it is too large Load Diff

@ -1,4 +1,4 @@
import { IF_BLOCK } from '../../constants.js'; import { IS_ELSEIF } from '../../constants.js';
import { import {
current_hydration_fragment, current_hydration_fragment,
hydrate_block_anchor, hydrate_block_anchor,
@ -6,112 +6,55 @@ import {
set_current_hydration_fragment set_current_hydration_fragment
} from '../hydration.js'; } from '../hydration.js';
import { remove } from '../reconciler.js'; import { remove } from '../reconciler.js';
import { current_block, execute_effect } from '../../runtime.js'; import {
import { destroy_effect, render_effect } from '../../reactivity/effects.js'; destroy_effect,
import { trigger_transitions } from '../elements/transitions.js'; pause_effect,
render_effect,
/** @returns {import('#client').IfBlock} */ resume_effect
function create_if_block() { } from '../../reactivity/effects.js';
return { import { create_block } from './utils.js';
// alternate transitions
a: null,
// alternate effect
ae: null,
// consequent transitions
c: null,
// consequent effect
ce: null,
// dom
d: null,
// effect
e: null,
// parent
p: /** @type {import('#client').Block} */ (current_block),
// transition
r: null,
// type
t: IF_BLOCK,
// value
v: false
};
}
/** /**
* @param {Comment} anchor_node * @param {Comment} anchor
* @param {() => boolean} condition_fn * @param {() => boolean} get_condition
* @param {(anchor: Node) => void} consequent_fn * @param {(anchor: Node) => import('#client').TemplateNode | import('#client').TemplateNode[]} consequent_fn
* @param {null | ((anchor: Node) => void)} alternate_fn * @param {null | ((anchor: Node) => import('#client').TemplateNode | import('#client').TemplateNode[])} alternate_fn
* @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local'
* @returns {void} * @returns {void}
*/ */
export function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) { export function if_block(anchor, get_condition, consequent_fn, alternate_fn, elseif = false) {
const block = create_if_block(); const block = create_block();
hydrate_block_anchor(anchor_node);
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;
/** @type {null | import('#client').TemplateNode | Array<import('#client').TemplateNode>} */ hydrate_block_anchor(anchor);
let consequent_dom = null;
/** @type {null | import('#client').TemplateNode | Array<import('#client').TemplateNode>} */ /** @type {undefined | import('#client').TemplateNode | Array<import('#client').TemplateNode>} */
let alternate_dom = null; let consequent_dom;
let has_mounted = false; /** @type {undefined | import('#client').TemplateNode | Array<import('#client').TemplateNode>} */
let alternate_dom;
/** /** @type {import('#client').Effect | null} */
* @type {import('#client').Effect | null} let consequent_effect = null;
*/
let current_branch_effect = null;
/** @type {import('#client').Effect} */ /** @type {import('#client').Effect | null} */
let consequent_effect; let alternate_effect = null;
/** @type {import('#client').Effect} */ /** @type {boolean | null} */
let alternate_effect; let condition = null;
const if_effect = render_effect(() => { const if_effect = render_effect(() => {
const result = !!condition_fn(); if (condition === (condition = !!get_condition())) return;
if (block.v !== result || !has_mounted) {
block.v = result;
if (has_mounted) {
const consequent_transitions = block.c;
const alternate_transitions = block.a;
if (result) {
if (alternate_transitions === null || alternate_transitions.size === 0) {
execute_effect(alternate_effect);
} else {
trigger_transitions(alternate_transitions, 'out');
}
if (consequent_transitions === null || consequent_transitions.size === 0) { /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
execute_effect(consequent_effect); let mismatch = false;
} else {
trigger_transitions(consequent_transitions, 'in');
}
} else {
if (consequent_transitions === null || consequent_transitions.size === 0) {
execute_effect(consequent_effect);
} else {
trigger_transitions(consequent_transitions, 'out');
}
if (alternate_transitions === null || alternate_transitions.size === 0) { if (hydrating) {
execute_effect(alternate_effect);
} else {
trigger_transitions(alternate_transitions, 'in');
}
}
} else if (hydrating) {
const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data; const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data;
if ( if (
!comment_text || !comment_text ||
(comment_text === 'ssr:if:true' && !result) || (comment_text === 'ssr:if:true' && !condition) ||
(comment_text === 'ssr:if:false' && result) (comment_text === 'ssr:if:false' && condition)
) { ) {
// Hydration mismatch: remove everything inside the anchor and start fresh. // Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen using when `{#if browser} .. {/if}` in SvelteKit. // This could happen using when `{#if browser} .. {/if}` in SvelteKit.
@ -124,78 +67,91 @@ export function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn)
} }
} }
has_mounted = true; if (condition) {
} if (consequent_effect) {
resume_effect(consequent_effect);
// create these here so they have the correct parent/child relationship } else {
consequent_effect ??= render_effect( consequent_effect = render_effect(
(/** @type {any} */ _, /** @type {import('#client').Effect | null} */ consequent_effect) => { () => {
const result = block.v; consequent_dom = consequent_fn(anchor);
if (!result && consequent_dom !== null) { return () => {
// TODO make this unnecessary by linking the dom to the effect,
// and removing automatically on teardown
if (consequent_dom !== undefined) {
remove(consequent_dom); remove(consequent_dom);
consequent_dom = null; consequent_dom = undefined;
}
};
},
block,
true
);
} }
if (result && current_branch_effect !== consequent_effect) { if (alternate_effect) {
consequent_fn(anchor_node); pause_effect(alternate_effect, () => {
if (mismatch && current_branch_effect === null) { alternate_effect = null;
// Set fragment so that Svelte continues to operate in hydration mode if (alternate_dom) remove(alternate_dom);
set_current_hydration_fragment([]); });
} }
current_branch_effect = consequent_effect; } else {
consequent_dom = block.d; if (alternate_effect) {
resume_effect(alternate_effect);
} else if (alternate_fn) {
alternate_effect = render_effect(
() => {
alternate_dom = alternate_fn(anchor);
return () => {
// TODO make this unnecessary by linking the dom to the effect,
// and removing automatically on teardown
if (alternate_dom !== undefined) {
remove(alternate_dom);
alternate_dom = undefined;
} }
};
block.d = null;
}, },
block, block,
true true
); );
block.ce = consequent_effect;
alternate_effect ??= render_effect(
(/** @type {any} */ _, /** @type {import('#client').Effect | null} */ alternate_effect) => {
const result = block.v;
if (result && alternate_dom !== null) {
remove(alternate_dom);
alternate_dom = null;
} }
if (!result && current_branch_effect !== alternate_effect) { if (consequent_effect) {
if (alternate_fn !== null) { pause_effect(consequent_effect, () => {
alternate_fn(anchor_node); consequent_effect = null;
if (consequent_dom) remove(consequent_dom);
});
}
} }
if (mismatch && current_branch_effect === null) { if (mismatch) {
// Set fragment so that Svelte continues to operate in hydration mode // Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]); set_current_hydration_fragment([]);
} }
}, block);
current_branch_effect = alternate_effect; if (elseif) {
alternate_dom = block.d; if_effect.f |= IS_ELSEIF;
} }
block.d = null;
},
block,
true
);
block.ae = alternate_effect;
}, block);
if_effect.ondestroy = () => { if_effect.ondestroy = () => {
if (consequent_dom !== null) { // TODO make this unnecessary by linking the dom to the effect,
// and removing automatically on teardown
if (consequent_dom !== undefined) {
remove(consequent_dom); remove(consequent_dom);
} }
if (alternate_dom !== null) { if (alternate_dom !== undefined) {
remove(alternate_dom); remove(alternate_dom);
} }
if (consequent_effect) {
destroy_effect(consequent_effect); destroy_effect(consequent_effect);
}
if (alternate_effect) {
destroy_effect(alternate_effect); destroy_effect(alternate_effect);
}
}; };
block.e = if_effect;
} }

@ -1,140 +1,76 @@
import { UNINITIALIZED, KEY_BLOCK } from '../../constants.js'; import { UNINITIALIZED } from '../../constants.js';
import { hydrate_block_anchor } from '../hydration.js'; import { hydrate_block_anchor } from '../hydration.js';
import { remove } from '../reconciler.js'; import { remove } from '../reconciler.js';
import { current_block, execute_effect } from '../../runtime.js'; import { pause_effect, render_effect } from '../../reactivity/effects.js';
import { destroy_effect, render_effect } from '../../reactivity/effects.js';
import { trigger_transitions } from '../elements/transitions.js';
import { safe_not_equal } from '../../reactivity/equality.js'; import { safe_not_equal } from '../../reactivity/equality.js';
import { create_block } from './utils.js';
/** @returns {import('../../types.js').KeyBlock} */
function create_key_block() {
return {
// dom
d: null,
// effect
e: null,
// parent
p: /** @type {import('../../types.js').Block} */ (current_block),
// transition
r: null,
// type
t: KEY_BLOCK
};
}
/** /**
* @template V * @template V
* @param {Comment} anchor_node * @param {Comment} anchor
* @param {() => V} key * @param {() => V} get_key
* @param {(anchor: Node) => void} render_fn * @param {(anchor: Node) => void} render_fn
* @returns {void} * @returns {void}
*/ */
export function key_block(anchor_node, key, render_fn) { export function key_block(anchor, get_key, render_fn) {
const block = create_key_block(); const block = create_block();
/** @type {null | import('../../types.js').Render} */ hydrate_block_anchor(anchor);
let current_render = null;
hydrate_block_anchor(anchor_node);
/** @type {V | typeof UNINITIALIZED} */ /** @type {V | typeof UNINITIALIZED} */
let key_value = UNINITIALIZED; let key = UNINITIALIZED;
let mounted = false;
block.r = /** @type {import('#client').Effect} */
let effect;
/** /**
* @param {import('../../types.js').Transition} transition * Every time `key` changes, we create a new effect. Old effects are
* @returns {void} * removed from this set when they have fully transitioned out
* @type {Set<import('#client').Effect>}
*/ */
(transition) => { let effects = new Set();
const render = /** @type {import('../../types.js').Render} */ (current_render);
const transitions = render.s; const key_effect = render_effect(
transitions.add(transition); () => {
transition.f(() => { if (safe_not_equal(key, (key = get_key()))) {
transitions.delete(transition); if (effect) {
if (transitions.size === 0) { var e = effect;
// If the current render has changed since, then we can remove the old render pause_effect(e, () => {
// effect as it's stale. effects.delete(e);
if (current_render !== render && render.e !== null) { });
if (render.d !== null) {
remove(render.d);
render.d = null;
}
destroy_effect(render.e);
render.e = null;
} }
effect = render_effect(
() => {
render_fn(anchor);
const dom = block.d;
return () => {
if (dom !== null) {
remove(dom);
} }
});
}; };
const create_render_effect = () => {
/** @type {import('../../types.js').Render} */
const render = {
d: null,
e: null,
s: new Set(),
p: current_render
};
const effect = render_effect(
() => {
render_fn(anchor_node);
render.d = block.d;
block.d = null;
}, },
block, block,
true, true,
true true
); );
render.e = effect;
current_render = render; // @ts-expect-error TODO tidy up
}; effect.d = block.d;
const render = () => {
const render = current_render; effects.add(effect);
if (render === null) {
create_render_effect();
return;
}
const transitions = render.s;
if (transitions.size === 0) {
if (render.d !== null) {
remove(render.d);
render.d = null;
}
if (render.e) {
execute_effect(render.e);
} else {
create_render_effect();
}
} else {
trigger_transitions(transitions, 'out');
create_render_effect();
}
};
const key_effect = render_effect(
() => {
const prev_key_value = key_value;
key_value = key();
if (mounted && safe_not_equal(prev_key_value, key_value)) {
render();
} }
}, },
block, block,
false false
); );
// To ensure topological ordering of the key effect to the render effect,
// we trigger the effect after.
render();
mounted = true;
key_effect.ondestroy = () => { key_effect.ondestroy = () => {
let render = current_render; for (const e of effects) {
while (render !== null) { // @ts-expect-error TODO tidy up. ondestroy should be totally unnecessary
const dom = render.d; if (e.d) remove(e.d);
if (dom !== null) {
remove(dom);
}
const effect = render.e;
if (effect !== null) {
destroy_effect(effect);
}
render = render.p;
} }
}; };
block.e = key_effect;
} }

@ -1,7 +1,7 @@
import { SNIPPET_BLOCK } from '../../constants.js';
import { render_effect } from '../../reactivity/effects.js'; import { render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js'; import { remove } from '../reconciler.js';
import { current_block, untrack } from '../../runtime.js'; import { untrack } from '../../runtime.js';
import { create_block } from './utils.js';
/** /**
* @param {() => Function | null | undefined} get_snippet * @param {() => Function | null | undefined} get_snippet
@ -10,19 +10,7 @@ import { current_block, untrack } from '../../runtime.js';
* @returns {void} * @returns {void}
*/ */
export function snippet(get_snippet, node, ...args) { export function snippet(get_snippet, node, ...args) {
/** @type {import('#client').SnippetBlock} */ const block = create_block();
const block = {
// dom
d: null,
// parent
p: /** @type {import('#client').Block} */ (current_block),
// effect
e: null,
// transition
r: null,
// type
t: SNIPPET_BLOCK
};
render_effect(() => { render_effect(() => {
// Only rerender when the snippet function itself changes, // Only rerender when the snippet function itself changes,

@ -1,127 +1,69 @@
import { DYNAMIC_COMPONENT_BLOCK } from '../../constants.js';
import { hydrate_block_anchor } from '../hydration.js'; import { hydrate_block_anchor } from '../hydration.js';
import { destroy_effect, render_effect } from '../../reactivity/effects.js'; import { pause_effect, render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js'; import { remove } from '../reconciler.js';
import { current_block, execute_effect } from '../../runtime.js'; import { create_block } from './utils.js';
import { trigger_transitions } from '../elements/transitions.js';
// TODO this is very similar to `key`, can we deduplicate?
/** /**
* @template P * @template P
* @param {Comment} anchor_node * @template {(props: P) => void} C
* @param {() => (props: P) => void} component_fn * @param {Comment} anchor
* @param {(component: (props: P) => void) => void} render_fn * @param {() => C} get_component
* @param {(component: C) => void} render_fn
* @returns {void} * @returns {void}
*/ */
export function component(anchor_node, component_fn, render_fn) { export function component(anchor, get_component, render_fn) {
/** @type {import('#client').DynamicComponentBlock} */ const block = create_block();
const block = {
// dom hydrate_block_anchor(anchor);
d: null,
// effect
e: null,
// parent
p: /** @type {import('#client').Block} */ (current_block),
// transition
r: null,
// type
t: DYNAMIC_COMPONENT_BLOCK
};
/** @type {null | import('#client').Render} */ /** @type {C} */
let current_render = null; let component;
hydrate_block_anchor(anchor_node);
/** @type {null | ((props: P) => void)} */ /** @type {import('#client').Effect} */
let component = null; let effect;
block.r =
/** /**
* @param {import('#client').Transition} transition * Every time `component` changes, we create a new effect. Old effects are
* @returns {void} * removed from this set when they have fully transitioned out
* @type {Set<import('#client').Effect>}
*/ */
(transition) => { let effects = new Set();
const render = /** @type {import('#client').Render} */ (current_render);
const transitions = render.s;
transitions.add(transition);
transition.f(() => {
transitions.delete(transition);
if (transitions.size === 0) {
// If the current render has changed since, then we can remove the old render
// effect as it's stale.
if (current_render !== render && render.e !== null) {
if (render.d !== null) {
remove(render.d);
render.d = null;
}
destroy_effect(render.e);
render.e = null;
}
}
});
};
const create_render_effect = () => { const component_effect = render_effect(
/** @type {import('#client').Render} */
const render = {
d: null,
e: null,
s: new Set(),
p: current_render
};
// Managed effect
render.e = render_effect(
() => { () => {
const current = block.d; if (component === (component = get_component())) return;
if (current !== null) {
remove(current); if (effect) {
block.d = null; var e = effect;
pause_effect(e, () => {
effects.delete(e);
});
} }
if (component) { if (component) {
effect = render_effect(
() => {
render_fn(component); render_fn(component);
const dom = block.d;
return () => {
if (dom !== null) {
remove(dom);
} }
render.d = block.d; };
block.d = null;
}, },
block, block,
true,
true true
); );
current_render = render; // @ts-expect-error TODO tidy up
}; effect.d = block.d;
const render = () => {
const render = current_render;
if (render === null) {
create_render_effect();
return;
}
const transitions = render.s;
if (transitions.size === 0) { effects.add(effect);
if (render.d !== null) {
remove(render.d);
render.d = null;
}
if (render.e) {
execute_effect(render.e);
} else {
create_render_effect();
}
} else {
create_render_effect();
trigger_transitions(transitions, 'out');
}
};
const component_effect = render_effect(
() => {
const next_component = component_fn();
if (component !== next_component) {
component = next_component;
render();
} }
}, },
block, block,
@ -129,19 +71,9 @@ export function component(anchor_node, component_fn, render_fn) {
); );
component_effect.ondestroy = () => { component_effect.ondestroy = () => {
let render = current_render; for (const e of effects) {
while (render !== null) { // @ts-expect-error TODO tidy up. ondestroy should be totally unnecessary
const dom = render.d; if (e.d) remove(e.d);
if (dom !== null) {
remove(dom);
}
const effect = render.e;
if (effect !== null) {
destroy_effect(effect);
}
render = render.p;
} }
}; };
block.e = component_effect;
} }

@ -1,11 +1,18 @@
import { namespace_svg } from '../../../../constants.js'; import { namespace_svg } from '../../../../constants.js';
import { DYNAMIC_ELEMENT_BLOCK } from '../../constants.js';
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js'; import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js';
import { empty } from '../operations.js'; import { empty } from '../operations.js';
import { destroy_effect, render_effect } from '../../reactivity/effects.js'; import {
import { insert, remove } from '../reconciler.js'; destroy_effect,
import { current_block, execute_effect } from '../../runtime.js'; pause_effect,
render_effect,
resume_effect
} from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
import { is_array } from '../../utils.js'; import { is_array } from '../../utils.js';
import { set_should_intro } from '../../render.js';
import { current_each_item_block, set_current_each_item_block } from './each.js';
import { create_block } from './utils.js';
import { current_block } from '../../runtime.js';
/** /**
* @param {import('#client').Block} block * @param {import('#client').Block} block
@ -28,76 +35,84 @@ function swap_block_dom(block, from, to) {
} }
/** /**
* @param {Comment} anchor_node * @param {Comment} anchor
* @param {() => string} tag_fn * @param {() => string} get_tag
* @param {boolean | null} is_svg `null` == not statically known * @param {boolean | null} is_svg `null` == not statically known
* @param {undefined | ((element: Element, anchor: Node) => void)} render_fn * @param {undefined | ((element: Element, anchor: Node) => void)} render_fn
* @returns {void} * @returns {void}
*/ */
export function element(anchor_node, tag_fn, is_svg, render_fn) { export function element(anchor, get_tag, is_svg, render_fn) {
/** @type {import('#client').DynamicElementBlock} */ const parent_block = /** @type {import('#client').Block} */ (current_block);
const block = { const block = create_block();
// dom
d: null,
// effect
e: null,
// parent
p: /** @type {import('#client').Block} */ (current_block),
// transition
r: null,
// type
t: DYNAMIC_ELEMENT_BLOCK
};
hydrate_block_anchor(anchor_node); hydrate_block_anchor(anchor);
let has_mounted = false;
/** @type {string} */ /** @type {string | null} */
let tag; let tag;
/** @type {string | null} */
let current_tag;
/** @type {null | Element} */ /** @type {null | Element} */
let element = null; let element = null;
const element_effect = render_effect( /** @type {import('#client').Effect | null} */
() => { let effect;
tag = tag_fn();
if (has_mounted) { /**
execute_effect(render_effect_signal); * The keyed `{#each ...}` item block, if any, that this element is inside.
} * We track this so we can set it when changing the element, allowing any
has_mounted = true; * `animate:` directive to bind itself to the correct block
}, */
block, let each_item_block = current_each_item_block;
false
); const wrapper = render_effect(() => {
const next_tag = get_tag() || null;
if (next_tag === tag) return;
// See explanation of `each_item_block` above
var previous_each_item_block = current_each_item_block;
set_current_each_item_block(each_item_block);
// Managed effect
const render_effect_signal = render_effect(
() => {
// We try our best infering the namespace in case it's not possible to determine statically, // We try our best infering the namespace in case it's not possible to determine statically,
// but on the first render on the client (without hydration) the parent will be undefined, // but on the first render on the client (without hydration) the parent will be undefined,
// since the anchor is not attached to its parent / the dom yet. // since the anchor is not attached to its parent / the dom yet.
const ns = const ns =
is_svg || tag === 'svg' is_svg || next_tag === 'svg'
? namespace_svg ? namespace_svg
: is_svg === false || anchor_node.parentElement?.tagName === 'foreignObject' : is_svg === false || anchor.parentElement?.tagName === 'foreignObject'
? null ? null
: anchor_node.parentElement?.namespaceURI ?? null; : anchor.parentElement?.namespaceURI ?? null;
const next_element = tag if (effect) {
? hydrating if (next_tag === null) {
? /** @type {Element} */ (current_hydration_fragment[0]) // start outro
: ns pause_effect(effect, () => {
? document.createElementNS(ns, tag) effect = null;
: document.createElement(tag) current_tag = null;
: null; element?.remove(); // TODO this should be unnecessary
});
} else if (next_tag === current_tag) {
// same tag as is currently rendered — abort outro
resume_effect(effect);
} else {
// tag is changing — destroy immediately, render contents without intro transitions
destroy_effect(effect);
set_should_intro(false);
}
}
if (next_tag && next_tag !== current_tag) {
effect = render_effect(
() => {
const prev_element = element; const prev_element = element;
if (prev_element !== null) { element = hydrating
block.d = null; ? /** @type {Element} */ (current_hydration_fragment[0])
} : ns
? document.createElementNS(ns, next_tag)
: document.createElement(next_tag);
element = next_element; if (render_fn) {
if (element !== null && render_fn !== undefined) {
let anchor; let anchor;
if (hydrating) { if (hydrating) {
// Use the existing ssr comment as the anchor so that the inner open and close // Use the existing ssr comment as the anchor so that the inner open and close
@ -110,30 +125,34 @@ export function element(anchor_node, tag_fn, is_svg, render_fn) {
render_fn(element, anchor); render_fn(element, anchor);
} }
const has_prev_element = prev_element !== null; anchor.before(element);
if (has_prev_element) {
remove(prev_element); if (prev_element) {
}
if (element !== null) {
insert(element, null, anchor_node);
if (has_prev_element) {
const parent_block = block.p;
swap_block_dom(parent_block, prev_element, element); swap_block_dom(parent_block, prev_element, element);
} prev_element.remove();
} }
}, },
block, block,
true true
); );
}
tag = next_tag;
if (tag) current_tag = tag;
set_should_intro(true);
element_effect.ondestroy = () => { set_current_each_item_block(previous_each_item_block);
}, block);
wrapper.ondestroy = () => {
if (element !== null) { if (element !== null) {
remove(element); remove(element);
block.d = null; block.d = null;
element = null; element = null;
} }
destroy_effect(render_effect_signal);
};
block.e = element_effect; if (effect) {
destroy_effect(effect);
}
};
} }

@ -1,4 +1,3 @@
import { HEAD_BLOCK } from '../../constants.js';
import { import {
current_hydration_fragment, current_hydration_fragment,
get_hydration_fragment, get_hydration_fragment,
@ -8,26 +7,14 @@ import {
import { empty } from '../operations.js'; import { empty } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js'; import { render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js'; import { remove } from '../reconciler.js';
import { current_block } from '../../runtime.js'; import { create_block } from './utils.js';
/** /**
* @param {(anchor: Node | null) => void} render_fn * @param {(anchor: Node | null) => void} render_fn
* @returns {void} * @returns {void}
*/ */
export function head(render_fn) { export function head(render_fn) {
/** @type {import('#client').HeadBlock} */ const block = create_block();
const block = {
// dom
d: null,
// effect
e: null,
// parent
p: /** @type {import('#client').Block} */ (current_block),
// transition
r: null,
// type
t: HEAD_BLOCK
};
// The head function may be called after the first hydration pass and ssr comment nodes may still be present, // The head function may be called after the first hydration pass and ssr comment nodes may still be present,
// therefore we need to skip that when we detect that we're not in hydration mode. // therefore we need to skip that when we detect that we're not in hydration mode.
@ -66,8 +53,6 @@ export function head(render_fn) {
remove(current); remove(current);
} }
}; };
block.e = head_effect;
} finally { } finally {
if (is_hydrating) { if (is_hydrating) {
set_current_hydration_fragment(previous_hydration_fragment); set_current_hydration_fragment(previous_hydration_fragment);

@ -0,0 +1,7 @@
/** @returns {import('#client').Block} */
export function create_block() {
return {
// dom
d: null
};
}

@ -1,5 +1,5 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { render_effect } from '../../../reactivity/effects.js'; import { render_effect, user_effect } from '../../../reactivity/effects.js';
import { stringify } from '../../../render.js'; import { stringify } from '../../../render.js';
/** /**
@ -96,6 +96,11 @@ export function bind_group(inputs, group_index, input, get_value, update) {
} }
}); });
user_effect(() => {
// necessary to maintain binding group order in all insertion scenarios. TODO optimise
binding_group.sort((a, b) => (a.compareDocumentPosition(b) === 4 ? -1 : 1));
});
render_effect(() => { render_effect(() => {
return () => { return () => {
var index = binding_group.indexOf(input); var index = binding_group.indexOf(input);

@ -1,4 +1,5 @@
import { effect } from '../../../reactivity/effects.js'; import { effect } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js';
/** /**
* Selects the correct option(s) (depending on whether this is a multiple select) * Selects the correct option(s) (depending on whether this is a multiple select)
@ -26,26 +27,43 @@ export function select_option(select, value, mounting) {
} }
/** /**
* Finds the containing `<select>` element and potentially updates its `selected` state. * Selects the correct option(s) if `value` is given,
* @param {HTMLOptionElement} option * and then sets up a mutation observer to sync the
* @returns {void} * current selection to the dom when it changes. Such
* changes could for example occur when options are
* inside an `#each` block.
* @template V
* @param {HTMLSelectElement} select
* @param {() => V} [get_value]
*/ */
export function selected(option) { export function init_select(select, get_value) {
// Inside an effect because the element might not be connected
// to the parent <select> yet when this is called
effect(() => { effect(() => {
var select = option.parentNode; if (get_value) {
select_option(select, untrack(get_value));
while (select != null) {
if (select.nodeName === 'SELECT') break;
select = select.parentNode;
} }
var observer = new MutationObserver(() => {
// @ts-ignore // @ts-ignore
if (select != null && option.__value === select.__value) { var value = select.__value;
// never set to false, since this causes browser to select default option select_option(select, value);
option.selected = true; // Deliberately don't update the potential binding value,
} // the model should be preserved unless explicitly changed
});
observer.observe(select, {
// Listen to option element changes
childList: true,
subtree: true, // because of <optgroup>
// Listen to option element value attribute changes
// (doesn't get notified of select value changes,
// because that property is not reflected as an attribute)
attributes: true,
attributeFilter: ['value']
});
return () => {
observer.disconnect();
};
}); });
} }
@ -78,6 +96,7 @@ export function bind_select_value(select, get_value, update) {
var value = get_value(); var value = get_value();
select_option(select, value, mounting); select_option(select, value, mounting);
// Mounting and value undefined -> take selection from dom
if (mounting && value === undefined) { if (mounting && value === undefined) {
/** @type {HTMLOptionElement | null} */ /** @type {HTMLOptionElement | null} */
var selected_option = select.querySelector(':checked'); var selected_option = select.querySelector(':checked');
@ -91,6 +110,9 @@ export function bind_select_value(select, get_value, update) {
select.__value = value; select.__value = value;
mounting = false; mounting = false;
}); });
// don't pass get_value, we already initialize it in the effect above
init_select(select);
} }
/** /**

@ -3,7 +3,7 @@ import { set_class_name } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js'; import { render_effect } from '../../reactivity/effects.js';
/** /**
* @param {Element} dom * @param {HTMLElement} dom
* @param {() => string} value * @param {() => string} value
* @returns {void} * @returns {void}
*/ */
@ -14,7 +14,47 @@ export function class_name_effect(dom, value) {
} }
/** /**
* @param {Element} dom * @param {SVGElement} dom
* @param {() => string} value
* @returns {void}
*/
export function svg_class_name_effect(dom, value) {
render_effect(() => {
svg_class_name(dom, value());
});
}
/**
* @param {SVGElement} dom
* @param {string} value
* @returns {void}
*/
export function svg_class_name(dom, value) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value);
if (hydrating && dom.getAttribute('class') === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
} else if (
prev_class_name !== next_class_name ||
(hydrating && dom.getAttribute('class') !== next_class_name)
) {
if (next_class_name === '') {
dom.removeAttribute('class');
} else {
dom.setAttribute('class', next_class_name);
}
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
}
}
/**
* @param {HTMLElement} dom
* @param {string} value * @param {string} value
* @returns {void} * @returns {void}
*/ */
@ -31,7 +71,10 @@ export function class_name(dom, value) {
prev_class_name !== next_class_name || prev_class_name !== next_class_name ||
(hydrating && dom.className !== next_class_name) (hydrating && dom.className !== next_class_name)
) { ) {
if (next_class_name === '') { // Removing the attribute when the value is only an empty string causes
// peformance issues vs simply making the className an empty string. So
// we should only remove the class if the the value is nullish.
if (value == null) {
dom.removeAttribute('class'); dom.removeAttribute('class');
} else { } else {
set_class_name(dom, next_class_name); set_class_name(dom, next_class_name);

@ -17,7 +17,10 @@ export function event(event_name, dom, handler, capture, passive) {
* @this {EventTarget} * @this {EventTarget}
*/ */
function target_handler(/** @type {Event} */ event) { function target_handler(/** @type {Event} */ event) {
if (!capture) {
// Only call in the bubble phase, else delegated events would be called before the capturing events
handle_event_propagation(dom, event); handle_event_propagation(dom, event);
}
if (!event.cancelBubble) { if (!event.cancelBubble) {
return handler.call(this, event); return handler.call(this, event);
} }

@ -1,85 +1,13 @@
import { EACH_IS_ANIMATED, EACH_IS_CONTROLLED } from '../../../../constants.js'; import { noop } from '../../../common.js';
import { run_all } from '../../../common.js'; import { user_effect } from '../../reactivity/effects.js';
import { import { current_effect, untrack } from '../../runtime.js';
AWAIT_BLOCK,
DYNAMIC_COMPONENT_BLOCK,
EACH_BLOCK,
EACH_ITEM_BLOCK,
IF_BLOCK,
KEY_BLOCK,
ROOT_BLOCK
} from '../../constants.js';
import { destroy_each_item_block, get_first_element } from '../blocks/each.js';
import { schedule_raf_task } from '../task.js';
import { append_child, empty } from '../operations.js';
import {
destroy_effect,
effect,
managed_effect,
managed_pre_effect
} from '../../reactivity/effects.js';
import {
current_block,
current_effect,
execute_effect,
mark_subtree_inert,
untrack
} from '../../runtime.js';
import { raf } from '../../timing.js'; import { raf } from '../../timing.js';
import { loop } from '../../loop.js';
const active_tick_animations = new Set(); import { should_intro } from '../../render.js';
const DELAY_NEXT_TICK = Number.MIN_SAFE_INTEGER; import { is_function } from '../../utils.js';
import { current_each_item_block } from '../blocks/each.js';
/** @type {undefined | number} */ import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
let active_tick_ref = undefined; import { EFFECT_RAN } from '../../constants.js';
/**
* @template P
* @param {HTMLElement} dom
* @param {() => import('#client').TransitionFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props
* @param {any} global
* @returns {void}
*/
export function transition(dom, get_transition_fn, props, global = false) {
bind_transition(dom, get_transition_fn, props, 'both', global);
}
/**
* @template P
* @param {HTMLElement} dom
* @param {() => import('#client').TransitionFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props
* @returns {void}
*/
export function animate(dom, get_transition_fn, props) {
bind_transition(dom, get_transition_fn, props, 'key', false);
}
/**
* @template P
* @param {HTMLElement} dom
* @param {() => import('#client').TransitionFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props
* @param {any} global
* @returns {void}
*/
function in_fn(dom, get_transition_fn, props, global = false) {
bind_transition(dom, get_transition_fn, props, 'in', global);
}
export { in_fn as in };
/**
* @template P
* @param {HTMLElement} dom
* @param {() => import('#client').TransitionFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props
* @param {any} global
* @returns {void}
*/
export function out(dom, get_transition_fn, props, global = false) {
bind_transition(dom, get_transition_fn, props, 'out', global);
}
/** /**
* @template T * @template T
@ -95,7 +23,7 @@ function custom_event(type, detail, { bubbles = false, cancelable = false } = {}
} }
/** /**
* @param {HTMLElement} dom * @param {Element} dom
* @param {'introstart' | 'introend' | 'outrostart' | 'outroend'} type * @param {'introstart' | 'introend' | 'outrostart' | 'outroend'} type
* @returns {void} * @returns {void}
*/ */
@ -137,685 +65,281 @@ function css_to_keyframe(css) {
return keyframe; return keyframe;
} }
class TickAnimation { /** @param {number} t */
/** @type {null | (() => void)} */ const linear = (t) => t;
onfinish;
/** @type {(t: number, u: number) => string} */
#tick_fn;
/** @type {number} */
#duration;
/** @type {number} */
#current;
/** @type {number} */
#delay;
/** @type {number} */
#previous;
/** @type {boolean} */
paused;
/** @type {boolean} */
#reversed;
/** @type {number} */
#delay_current;
/** @type {boolean} */
#delayed_reverse;
/** /**
* @param {(t: number, u: number) => string} tick_fn * Called inside keyed `{#each ...}` blocks (as `$.animation(...)`). This creates an animation manager
* @param {number} duration * and attaches it to the block, so that moves can be animated following reconciliation.
* @param {number} delay * @template P
* @param {boolean} out * @param {Element} element
* @param {() => import('#client').AnimateFn<P | undefined>} get_fn
* @param {(() => P) | null} get_params
*/ */
constructor(tick_fn, duration, delay, out) { export function animation(element, get_fn, get_params) {
this.#duration = duration; var block = /** @type {import('#client').EachItem} */ (current_each_item_block);
this.#delay = delay;
this.paused = false;
this.#tick_fn = tick_fn;
this.#reversed = out;
this.#delay_current = delay;
this.#current = out ? duration : 0;
this.#previous = 0;
this.#delayed_reverse = false;
this.onfinish = null;
if (this.#delay) {
if (!out) {
this.#tick_fn(0, 1);
}
}
}
pause() { /** @type {DOMRect} */
this.paused = true; var from;
}
play() { /** @type {DOMRect} */
this.paused = false; var to;
if (!active_tick_animations.has(this)) {
this.#previous = raf.now();
if (active_tick_ref === undefined) {
active_tick_ref = raf.tick(handle_raf);
}
active_tick_animations.add(this);
}
}
#reverse() { /** @type {import('#client').Animation | undefined} */
this.#reversed = !this.#reversed; var animation;
if (this.paused) {
if (this.#current === 0) {
this.#current = this.#duration;
}
this.play();
}
}
reverse() { block.a ??= {
if (this.#delay === 0) { element,
this.#reverse(); measure() {
} else { from = this.element.getBoundingClientRect();
this.#delay_current = this.#delay; },
this.#delayed_reverse = true; apply() {
} animation?.abort();
}
cancel() { to = this.element.getBoundingClientRect();
active_tick_animations.delete(this);
const current = this.#current / this.#duration;
if (current > 0 && current < 1) {
const t = this.#reversed ? 1 : 0;
this.#tick_fn(t, 1 - t);
}
}
finish() { const options = get_fn()(this.element, { from, to }, get_params?.());
active_tick_animations.delete(this);
if (this.onfinish) {
this.onfinish();
}
}
/** @param {number} time */ if (
_update(time) { from.left !== to.left ||
let diff = time - this.#previous; from.right !== to.right ||
this.#previous = time; from.top !== to.top ||
if (this.#delay_current !== 0) { from.bottom !== to.bottom
const is_delayed = this.#delay_current === DELAY_NEXT_TICK; ) {
let cancel = !this.#delayed_reverse; animation = animate(this.element, options, undefined, 1, () => {
this.#delay_current -= diff; animation?.abort();
if (this.#delay_current < 0 || is_delayed || (this.#delay_current === 0 && this.#reversed)) { animation = undefined;
const delay_diff = is_delayed ? 0 : -this.#delay_current; });
this.#delay_current = 0;
if (this.#delayed_reverse) {
this.#delayed_reverse = false;
this.#reverse();
} else if (delay_diff !== 0 || this.#reversed) {
diff = delay_diff;
}
cancel = false;
} else if (this.#delay_current === 0) {
this.#delay_current = DELAY_NEXT_TICK;
}
if (cancel) {
return;
}
}
this.#current += this.#reversed ? -diff : diff;
let t = this.#current / this.#duration;
if (t < 0) {
t = 0;
} else if (t > 1) {
t = 1;
}
if ((this.#reversed && t <= 0) || (!this.#reversed && t >= 1)) {
t = this.#reversed ? 0 : 1;
if (this.#delay_current === 0) {
active_tick_animations.delete(this);
if (this.onfinish) {
this.paused = true;
this.onfinish();
}
}
}
this.#tick_fn(t, 1 - t);
} }
} }
};
/** @param {number} time */ // in the case of a `<svelte:element>`, it's possible for `$.animation(...)` to be called
function handle_raf(time) { // when an animation manager already exists, if the tag changes. in that case, we need to
for (const animation of active_tick_animations) { // swap out the element rather than creating a new manager, in case it happened at the same
if (!animation.paused) { // moment as a reconciliation
animation._update(time); block.a.element = element;
}
}
if (active_tick_animations.size !== 0) {
active_tick_ref = raf.tick(handle_raf);
} else {
active_tick_ref = undefined;
}
} }
/** /**
* @param {{(t: number): number;(t: number): number;(arg0: number): any;}} easing_fn * Called inside block effects as `$.transition(...)`. This creates a transition manager and
* @param {((t: number, u: number) => string)} css_fn * attaches it to the current effect — later, inside `pause_effect` and `resume_effect`, we
* @param {number} duration * use this to create `intro` and `outro` transitions.
* @param {string} direction * @template P
* @param {boolean} reverse * @param {number} flags
* @param {HTMLElement} element
* @param {() => import('#client').TransitionFn<P | undefined>} get_fn
* @param {(() => P) | null} get_params
* @returns {void}
*/ */
function create_keyframes(easing_fn, css_fn, duration, direction, reverse) { export function transition(flags, element, get_fn, get_params) {
/** @type {Keyframe[]} */ var is_intro = (flags & TRANSITION_IN) !== 0;
const keyframes = []; var is_outro = (flags & TRANSITION_OUT) !== 0;
// We need at least two frames var is_global = (flags & TRANSITION_GLOBAL) !== 0;
const frame_time = 16.666;
const max_duration = Math.max(duration, frame_time);
// Have a keyframe every fame for 60 FPS
for (let i = 0; i <= max_duration; i += frame_time) {
let time;
if (i + frame_time > max_duration) {
time = 1;
} else if (i === 0) {
time = 0;
} else {
time = i / max_duration;
}
let t = easing_fn(time);
if (reverse) {
t = 1 - t;
}
keyframes.push(css_to_keyframe(css_fn(t, 1 - t)));
}
if (direction === 'out' || reverse) {
keyframes.reverse();
}
return keyframes;
}
/** @param {number} t */ /** @type {'in' | 'out' | 'both'} */
const linear = (t) => t; var direction = is_intro && is_outro ? 'both' : is_intro ? 'in' : 'out';
/** /** @type {import('#client').AnimationConfig | ((opts: { direction: 'in' | 'out' }) => import('#client').AnimationConfig) | undefined} */
* @param {HTMLElement} dom var current_options;
* @param {() => import('../../types.js').TransitionPayload} init
* @param {'in' | 'out' | 'both' | 'key'} direction
* @param {import('../../types.js').Effect} effect
* @returns {import('../../types.js').Transition}
*/
function create_transition(dom, init, direction, effect) {
let curr_direction = 'in';
/** @type {Array<() => void>} */ var inert = element.inert;
let subs = [];
/** @type {null | Animation | TickAnimation} */ /** @type {import('#client').Animation | undefined} */
let animation = null; var intro;
let cancelled = false;
const create_animation = () => { /** @type {import('#client').Animation | undefined} */
let payload = /** @type {import('../../types.js').TransitionPayload} */ (transition.p); var outro;
if (typeof payload === 'function') {
// @ts-ignore
payload = payload({ direction: curr_direction });
}
if (payload == null) {
return;
}
const duration = payload.duration ?? 300;
const delay = payload.delay ?? 0;
const css_fn = payload.css;
const tick_fn = payload.tick;
const easing_fn = payload.easing || linear;
if (typeof tick_fn === 'function') {
animation = new TickAnimation(tick_fn, duration, delay, direction === 'out');
} else {
const keyframes =
typeof css_fn === 'function'
? create_keyframes(easing_fn, css_fn, duration, direction, false)
: [];
animation = dom.animate(keyframes, {
duration,
endDelay: delay,
delay,
fill: 'both'
});
}
animation.pause();
animation.onfinish = () => { /** @type {(() => void) | undefined} */
const is_outro = curr_direction === 'out'; var reset;
/** @type {Animation | TickAnimation} */ (animation).cancel();
if (is_outro) { function get_options() {
run_all(subs); // If a transition is still ongoing, we use the existing options rather than generating
subs = []; // new ones. This ensures that reversible transitions reverse smoothly, rather than
// jumping to a new spot because (for example) a different `duration` was used
return (current_options ??= get_fn()(element, get_params?.(), { direction }));
} }
dispatch_event(dom, is_outro ? 'outroend' : 'introend');
};
};
/** @type {import('../../types.js').Transition} */ /** @type {import('#client').TransitionManager} */
const transition = { var transition = {
e: effect, is_global,
i: init,
// payload
p: null,
// finished
/** @param {() => void} fn */
f(fn) {
subs.push(fn);
},
in() { in() {
const needs_reverse = curr_direction !== 'in'; element.inert = inert;
curr_direction = 'in';
if (animation === null || cancelled) { if (is_intro) {
cancelled = false; dispatch_event(element, 'introstart');
create_animation(); intro = animate(element, get_options(), outro, 1, () => {
} dispatch_event(element, 'introend');
if (animation === null) { intro = current_options = undefined;
transition.x(); });
} else { } else {
dispatch_event(dom, 'introstart'); outro?.abort();
if (needs_reverse) { reset?.();
/** @type {Animation | TickAnimation} */ (animation).reverse();
}
/** @type {Animation | TickAnimation} */ (animation).play();
} }
}, },
// out out(fn) {
o() { if (is_outro) {
// @ts-ignore element.inert = true;
const has_keyed_transition = dom.__animate;
// If we're outroing an element that has an animation, then we need to fix dispatch_event(element, 'outrostart');
// its position to ensure it behaves nicely without causing layout shift. outro = animate(element, get_options(), intro, 0, () => {
if (has_keyed_transition) { dispatch_event(element, 'outroend');
const style = getComputedStyle(dom); outro = current_options = undefined;
const position = style.position; fn?.();
});
if (position !== 'absolute' && position !== 'fixed') {
const { width, height } = style; // TODO arguably the outro should never null itself out until _all_ outros for this effect have completed...
const a = dom.getBoundingClientRect(); // in that case we wouldn't need to store `reset` separately
dom.style.position = 'absolute'; reset = outro.reset;
dom.style.width = width;
dom.style.height = height;
const b = dom.getBoundingClientRect();
if (a.left !== b.left || a.top !== b.top) {
const translate = `translate(${a.left - b.left}px, ${a.top - b.top}px)`;
const existing_transform = style.transform;
if (existing_transform === 'none') {
dom.style.transform = translate;
} else {
// Previously, in the Svelte 4, we'd just apply the transform the the DOM element. However,
// because we're now using Web Animations, we can't do that as it won't work properly if the
// animation is also making use of the same transformations. So instead, we apply an
// instantaneous animation and pause it on the first frame, just applying the same behavior.
// We also need to take into consideration matrix transforms and how they might combine with
// an existing behavior that is already in progress (such as scale).
// > Follow the white rabbit.
const transform = existing_transform.startsWith('matrix(1,')
? translate
: `matrix(1,0,0,1,0,0)`;
const frame = {
transform
};
const animation = dom.animate([frame, frame], { duration: 1 });
animation.pause();
}
}
}
}
const needs_reverse = direction === 'both' && curr_direction !== 'out';
curr_direction = 'out';
if (animation === null || cancelled) {
cancelled = false;
create_animation();
}
if (animation === null) {
transition.x();
} else {
dispatch_event(dom, 'outrostart');
if (needs_reverse) {
const payload = transition.p;
const current_animation = /** @type {Animation} */ (animation);
// If we are working with CSS animations, then before we call reverse, we also need to ensure
// that we reverse the easing logic. To do this we need to re-create the keyframes so they're
// in reverse with easing properly reversed too.
if (
payload !== null &&
payload.css !== undefined &&
current_animation.playState === 'idle'
) {
const duration = payload.duration ?? 300;
const css_fn = payload.css;
const easing_fn = payload.easing || linear;
const keyframes = create_keyframes(easing_fn, css_fn, duration, direction, true);
const effect = current_animation.effect;
if (effect !== null) {
// @ts-ignore
effect.setKeyframes(keyframes);
}
}
/** @type {Animation | TickAnimation} */ (animation).reverse();
} else { } else {
/** @type {Animation | TickAnimation} */ (animation).play(); fn?.();
}
} }
}, },
// cancel stop: () => {
c() { intro?.abort();
if (animation !== null) { outro?.abort();
/** @type {Animation | TickAnimation} */ (animation).cancel();
} }
cancelled = true;
},
// cleanup
x() {
run_all(subs);
subs = [];
},
r: direction,
d: dom
}; };
return transition;
}
/** var effect = /** @type {import('#client').Effect} */ (current_effect);
* @param {import('../../types.js').Block} block
* @returns {boolean}
*/
function is_transition_block(block) {
const type = block.t;
return (
type === IF_BLOCK ||
type === EACH_ITEM_BLOCK ||
type === KEY_BLOCK ||
type === AWAIT_BLOCK ||
type === DYNAMIC_COMPONENT_BLOCK ||
(type === EACH_BLOCK && block.v.length === 0)
);
}
/** (effect.transitions ??= []).push(transition);
* @template P
* @param {HTMLElement} dom
* @param {() => import('../../types.js').TransitionFn<P | undefined> | import('../../types.js').AnimateFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props_fn
* @param {'in' | 'out' | 'both' | 'key'} direction
* @param {boolean} global
* @returns {void}
*/
function bind_transition(dom, get_transition_fn, props_fn, direction, global) {
const transition_effect = /** @type {import('../../types.js').Effect} */ (current_effect);
const block = current_block;
const is_keyed_transition = direction === 'key';
let can_show_intro_on_mount = true; // if this is a local transition, we only want to run it if the parent (block) effect's
let can_apply_lazy_transitions = false; // parent (branch) effect is where the state change happened. we can determine that by
// looking at whether the branch effect is currently initializing
if (is_intro && should_intro) {
var parent = /** @type {import('#client').Effect} */ (effect.parent);
if (is_keyed_transition) { if (is_global || (parent.f & EFFECT_RAN) !== 0) {
// @ts-ignore user_effect(() => {
dom.__animate = true; untrack(() => transition.in());
} });
/** @type {import('../../types.js').Block | null} */
let transition_block = block;
main: while (transition_block !== null) {
if (is_transition_block(transition_block)) {
if (transition_block.t === EACH_ITEM_BLOCK) {
// Lazily apply the each block transition
transition_block.r = each_item_transition;
transition_block.a = each_item_animate;
transition_block = transition_block.p;
} else if (transition_block.t === AWAIT_BLOCK && transition_block.n /* pending */) {
can_show_intro_on_mount = true;
} else if (transition_block.t === IF_BLOCK) {
transition_block.r = if_block_transition;
if (can_show_intro_on_mount) {
/** @type {import('../../types.js').Block | null} */
let if_block = transition_block;
while (if_block.t === IF_BLOCK) {
// If we have an if block parent that is currently falsy then
// we can show the intro on mount as long as that block is mounted
if (if_block.e !== null && !if_block.v) {
can_show_intro_on_mount = true;
break main;
}
if_block = if_block.p;
}
}
}
if (!can_apply_lazy_transitions && can_show_intro_on_mount) {
can_show_intro_on_mount = transition_block.e !== null;
}
if (can_show_intro_on_mount || !global) {
can_apply_lazy_transitions = true;
} }
} else if (transition_block.t === ROOT_BLOCK && !can_apply_lazy_transitions) {
can_show_intro_on_mount = transition_block.e !== null || transition_block.i;
} }
transition_block = transition_block.p;
} }
/** @type {import('../../types.js').Transition} */ /**
let transition; * Animates an element, according to the provided configuration
* @param {Element} element
* @param {import('#client').AnimationConfig | ((opts: { direction: 'in' | 'out' }) => import('#client').AnimationConfig)} options
* @param {import('#client').Animation | undefined} counterpart The corresponding intro/outro to this outro/intro
* @param {number} t2 The target `t` value — `1` for intro, `0` for outro
* @param {(() => void) | undefined} callback
* @returns {import('#client').Animation}
*/
function animate(element, options, counterpart, t2, callback) {
if (is_function(options)) {
// In the case of a deferred transition (such as `crossfade`), `option` will be
// a function rather than an `AnimationConfig`. We need to call this function
// once DOM has been updated...
/** @type {import('#client').Animation} */
var a;
user_effect(() => {
var o = untrack(() => options({ direction: t2 === 1 ? 'in' : 'out' }));
a = animate(element, o, counterpart, t2, callback);
});
effect(() => { // ...but we want to do so without using `async`/`await` everywhere, so
let already_mounted = false; // we return a facade that allows everything to remain synchronous
if (transition !== undefined) { return {
already_mounted = true; abort: () => a.abort(),
// Destroy any existing transitions first deactivate: () => a.deactivate(),
transition.x(); reset: () => a.reset(),
t: (now) => a.t(now)
};
} }
const transition_fn = get_transition_fn();
/** @param {DOMRect} [from] */
const init = (from) =>
untrack(() => {
const props = props_fn === null ? {} : props_fn();
return is_keyed_transition
? /** @type {import('../../types.js').AnimateFn<any>} */ (transition_fn)(
dom,
{ from: /** @type {DOMRect} */ (from), to: dom.getBoundingClientRect() },
props,
{}
)
: /** @type {import('../../types.js').TransitionFn<any>} */ (transition_fn)(dom, props, {
direction
});
});
transition = create_transition(dom, init, direction, transition_effect); counterpart?.deactivate();
const is_intro = direction === 'in';
const show_intro = can_show_intro_on_mount && (is_intro || direction === 'both');
if (show_intro && !already_mounted) { if (!options?.duration) {
transition.p = transition.i(); callback?.();
return {
abort: noop,
deactivate: noop,
reset: noop,
t: () => t2
};
} }
const effect = managed_pre_effect(() => { var { delay = 0, duration, css, tick, easing = linear } = options;
destroy_effect(effect);
dom.inert = false;
if (show_intro && !already_mounted) { var start = raf.now() + delay;
transition.in(); var t1 = counterpart?.t(start) ?? 1 - t2;
} var delta = t2 - t1;
/** @type {import('../../types.js').Block | null} */ duration *= Math.abs(delta);
let transition_block = block; var end = start + duration;
while (!is_intro && transition_block !== null) {
const parent = transition_block.p;
if (is_transition_block(transition_block)) {
if (transition_block.r !== null) {
transition_block.r(transition);
}
if (
parent === null ||
(!global && (transition_block.t !== IF_BLOCK || parent.t !== IF_BLOCK || parent.v))
) {
break;
}
}
transition_block = parent;
}
});
});
if (direction === 'key') { /** @type {Animation} */
effect(() => { var animation;
return () => {
transition.x();
};
});
}
}
/** /** @type {import('#client').Task} */
* @param {Set<import('../../types.js').Transition>} transitions var task;
* @param {'in' | 'out' | 'key'} target_direction
* @param {DOMRect} [from]
* @returns {void}
*/
export function trigger_transitions(transitions, target_direction, from) {
/** @type {Array<() => void>} */
const outros = [];
for (const transition of transitions) {
const direction = transition.r;
const effect = transition.e;
if (target_direction === 'in') {
if (direction === 'in' || direction === 'both') {
transition.in();
} else {
transition.c();
}
transition.d.inert = false;
mark_subtree_inert(effect, false);
} else if (target_direction === 'key') {
if (direction === 'key') {
if (!transition.p) {
transition.p = transition.i(/** @type {DOMRect} */ (from));
}
transition.in();
}
} else {
if (direction === 'out' || direction === 'both') {
if (!transition.p) {
transition.p = transition.i();
}
outros.push(transition.o);
}
transition.d.inert = true;
mark_subtree_inert(effect, true);
}
}
if (outros.length > 0) {
// Defer the outros to a microtask
const e = managed_pre_effect(() => {
destroy_effect(e);
const e2 = managed_effect(() => {
destroy_effect(e2);
run_all(outros);
});
});
}
}
/** if (css) {
* @this {import('../../types.js').IfBlock} // WAAPI
* @param {import('../../types.js').Transition} transition var keyframes = [];
* @returns {void} var n = duration / (1000 / 60);
*/
function if_block_transition(transition) { for (var i = 0; i <= n; i += 1) {
const block = this; var t = t1 + delta * easing(i / n);
// block.value === true var styles = css(t, 1 - t);
if (block.v) { keyframes.push(css_to_keyframe(styles));
const consequent_transitions = (block.c ??= new Set());
consequent_transitions.add(transition);
transition.f(() => {
const c = /** @type {Set<import('../../types.js').Transition>} */ (consequent_transitions);
c.delete(transition);
// If the block has changed to falsy and has transitions
if (!block.v && c.size === 0) {
const consequent_effect = block.ce;
execute_effect(/** @type {import('../../types.js').Effect} */ (consequent_effect));
} }
animation = element.animate(keyframes, {
delay,
duration,
easing: 'linear',
fill: 'forwards'
}); });
animation.finished
.then(() => {
callback?.();
})
.catch(noop);
} else { } else {
const alternate_transitions = (block.a ??= new Set()); // Timer
alternate_transitions.add(transition); if (t1 === 0) {
transition.f(() => { tick?.(0, 1); // TODO put in nested effect, to avoid interleaved reads/writes?
const a = /** @type {Set<import('../../types.js').Transition>} */ (alternate_transitions);
a.delete(transition);
// If the block has changed to truthy and has transitions
if (block.v && a.size === 0) {
const alternate_effect = block.ae;
execute_effect(/** @type {import('../../types.js').Effect} */ (alternate_effect));
}
});
}
} }
/** task = loop((now) => {
* @this {import('../../types.js').EachItemBlock} if (now >= end) {
* @param {import('../../types.js').Transition} transition tick?.(t2, 1 - t2);
* @returns {void} callback?.();
*/ return false;
function each_item_transition(transition) {
const block = this;
const each_block = block.p;
const is_controlled = (each_block.f & EACH_IS_CONTROLLED) !== 0;
// Disable optimization
if (is_controlled) {
const anchor = empty();
each_block.f ^= EACH_IS_CONTROLLED;
append_child(/** @type {Element} */ (each_block.a), anchor);
each_block.a = anchor;
}
if (transition.r === 'key' && (each_block.f & EACH_IS_ANIMATED) === 0) {
each_block.f |= EACH_IS_ANIMATED;
}
const transitions = (block.s ??= new Set());
transition.f(() => {
transitions.delete(transition);
if (transition.r !== 'key') {
for (let other of transitions) {
const type = other.r;
if (type === 'key' || type === 'in') {
transitions.delete(other);
}
}
if (transitions.size === 0) {
block.s = null;
destroy_each_item_block(block, null, true);
} }
if (now >= start) {
var p = t1 + delta * easing((now - start) / duration);
tick?.(p, 1 - p);
} }
return true;
}); });
transitions.add(transition);
} }
/** return {
* abort: () => {
* @param {import('../../types.js').EachItemBlock} block animation?.cancel();
* @param {Set<import('../../types.js').Transition>} transitions task?.abort();
*/ },
function each_item_animate(block, transitions) { deactivate: () => {
const from_dom = /** @type {Element} */ (get_first_element(block)); callback = undefined;
const from = from_dom.getBoundingClientRect(); },
// Cancel any existing key transitions reset: () => {
for (const transition of transitions) { if (t2 === 0) {
const type = transition.r; tick?.(1, 0);
if (type === 'key') {
transition.c();
} }
},
t: (now) => {
var t = t1 + delta * easing((now - start) / duration);
return Math.min(1, Math.max(0, t));
} }
schedule_raf_task(() => { };
trigger_transitions(transitions, 'key', from);
});
} }

@ -73,33 +73,26 @@ export function get_hydration_fragment(node, insert_text = false) {
} }
/** /**
* @param {Text | Comment | Element} anchor_node * @param {Node} node
* @param {boolean} [is_controlled]
* @returns {void} * @returns {void}
*/ */
export function hydrate_block_anchor(anchor_node, is_controlled) { export function hydrate_block_anchor(node) {
if (hydrating) { if (!hydrating) return;
/** @type {Node} */
let target_node = anchor_node;
if (is_controlled) { if (node.nodeType === 8) {
target_node = /** @type {Node} */ (target_node.firstChild);
}
if (target_node.nodeType === 8) {
// @ts-ignore // @ts-ignore
let fragment = target_node.$$fragment; let fragment = node.$$fragment;
if (fragment === undefined) { if (fragment === undefined) {
fragment = get_hydration_fragment(target_node); fragment = get_hydration_fragment(node);
} else { } else {
schedule_task(() => { schedule_task(() => {
// @ts-expect-error clean up memory // @ts-expect-error clean up memory
target_node.$$fragment = undefined; node.$$fragment = undefined;
}); });
} }
set_current_hydration_fragment(fragment); set_current_hydration_fragment(fragment);
} else { } else {
const first_child = /** @type {Element | null} */ (target_node.firstChild); const first_child = /** @type {Element | null} */ (node.firstChild);
set_current_hydration_fragment(first_child === null ? [] : [first_child]); set_current_hydration_fragment(first_child === null ? [] : [first_child]);
} }
} }
}

@ -30,47 +30,31 @@ export function create_fragment_with_script_from_html(html) {
/** /**
* @param {Array<import('../types.js').TemplateNode> | import('../types.js').TemplateNode} current * @param {Array<import('../types.js').TemplateNode> | import('../types.js').TemplateNode} current
* @param {null | Element} parent_element * @param {Text | Element | Comment} sibling
* @param {null | Text | Element | Comment} sibling
* @returns {Text | Element | Comment} * @returns {Text | Element | Comment}
*/ */
export function insert(current, parent_element, sibling) { export function insert(current, sibling) {
if (!current) return sibling;
if (is_array(current)) { if (is_array(current)) {
var i = 0; for (var i = 0; i < current.length; i++) {
var node; sibling.before(/** @type {Node} */ (current[i]));
for (; i < current.length; i++) {
node = current[i];
if (sibling === null) {
append_child(/** @type {Element} */ (parent_element), /** @type {Node} */ (node));
} else {
sibling.before(/** @type {Node} */ (node));
}
} }
return current[0]; return current[0];
} else if (current !== null) {
if (sibling === null) {
append_child(/** @type {Element} */ (parent_element), /** @type {Node} */ (current));
} else {
sibling.before(/** @type {Node} */ (current));
}
} }
sibling.before(/** @type {Node} */ (current));
return /** @type {Text | Element | Comment} */ (current); return /** @type {Text | Element | Comment} */ (current);
} }
/** /**
* @param {Array<import('../types.js').TemplateNode> | import('../types.js').TemplateNode} current * @param {Array<import('../types.js').TemplateNode> | import('../types.js').TemplateNode} current
* @returns {Element | Comment | Text}
*/ */
export function remove(current) { export function remove(current) {
var first_node = current;
if (is_array(current)) { if (is_array(current)) {
var i = 0; for (var i = 0; i < current.length; i++) {
var node; var node = current[i];
for (; i < current.length; i++) {
node = current[i];
if (i === 0) {
first_node = node;
}
if (node.isConnected) { if (node.isConnected) {
node.remove(); node.remove();
} }
@ -78,7 +62,6 @@ export function remove(current) {
} else if (current.isConnected) { } else if (current.isConnected) {
current.remove(); current.remove();
} }
return /** @type {Element | Comment | Text} */ (first_node);
} }
/** /**

@ -90,7 +90,7 @@ export function svg_template_with_script(svg, return_fragment) {
function open_template(is_fragment, use_clone_node, anchor, template_element_fn) { function open_template(is_fragment, use_clone_node, anchor, template_element_fn) {
if (hydrating) { if (hydrating) {
if (anchor !== null) { if (anchor !== null) {
hydrate_block_anchor(anchor, false); hydrate_block_anchor(anchor);
} }
// In ssr+hydration optimization mode, we might remove the template_element, // In ssr+hydration optimization mode, we might remove the template_element,
// so we need to is_fragment flag to properly handle hydrated content accordingly. // so we need to is_fragment flag to properly handle hydrated content accordingly.
@ -178,37 +178,37 @@ export function comment(anchor) {
* @param {Element | Text} dom * @param {Element | Text} dom
* @param {boolean} is_fragment * @param {boolean} is_fragment
* @param {null | Text | Comment | Element} anchor * @param {null | Text | Comment | Element} anchor
* @returns {void} * @returns {import('#client').TemplateNode | import('#client').TemplateNode[]}
*/ */
function close_template(dom, is_fragment, anchor) { function close_template(dom, is_fragment, anchor) {
const block = /** @type {import('#client').Block} */ (current_block);
/** @type {import('#client').TemplateNode | Array<import('#client').TemplateNode>} */ /** @type {import('#client').TemplateNode | Array<import('#client').TemplateNode>} */
const current = is_fragment var current = is_fragment
? is_array(dom) ? is_array(dom)
? dom ? dom
: /** @type {import('#client').TemplateNode[]} */ (Array.from(dom.childNodes)) : /** @type {import('#client').TemplateNode[]} */ (Array.from(dom.childNodes))
: dom; : dom;
if (!hydrating && anchor !== null) { if (!hydrating && anchor !== null) {
insert(current, null, anchor); insert(current, anchor);
} }
block.d = current;
/** @type {import('#client').Block} */ (current_block).d = current;
return current;
} }
/** /**
* @param {null | Text | Comment | Element} anchor * @param {null | Text | Comment | Element} anchor
* @param {Element | Text} dom * @param {Element | Text} dom
* @returns {void}
*/ */
export function close(anchor, dom) { export function close(anchor, dom) {
close_template(dom, false, anchor); return close_template(dom, false, anchor);
} }
/** /**
* @param {null | Text | Comment | Element} anchor * @param {null | Text | Comment | Element} anchor
* @param {Element | Text} dom * @param {Element | Text} dom
* @returns {void}
*/ */
export function close_frag(anchor, dom) { export function close_frag(anchor, dom) {
close_template(dom, true, anchor); return close_template(dom, true, anchor);
} }

@ -1,19 +1,22 @@
import { raf } from './timing.js'; import { raf } from './timing.js';
const tasks = new Set(); // TODO move this into timing.js where it probably belongs
/** /**
* @param {number} now * @param {number} now
* @returns {void} * @returns {void}
*/ */
function run_tasks(now) { function run_tasks(now) {
tasks.forEach((task) => { raf.tasks.forEach((task) => {
if (!task.c(now)) { if (!task.c(now)) {
tasks.delete(task); raf.tasks.delete(task);
task.f(); task.f();
} }
}); });
if (tasks.size !== 0) raf.tick(run_tasks);
if (raf.tasks.size !== 0) {
raf.tick(run_tasks);
}
} }
/** /**
@ -25,13 +28,17 @@ function run_tasks(now) {
export function loop(callback) { export function loop(callback) {
/** @type {import('./types.js').TaskEntry} */ /** @type {import('./types.js').TaskEntry} */
let task; let task;
if (tasks.size === 0) raf.tick(run_tasks);
if (raf.tasks.size === 0) {
raf.tick(run_tasks);
}
return { return {
promise: new Promise((fulfill) => { promise: new Promise((fulfill) => {
tasks.add((task = { c: callback, f: fulfill })); raf.tasks.add((task = { c: callback, f: fulfill }));
}), }),
abort() { abort() {
tasks.delete(task); raf.tasks.delete(task);
} }
}; };
} }

@ -48,7 +48,6 @@ export function proxy(value, immutable = true, owners) {
const prototype = get_prototype_of(value); const prototype = get_prototype_of(value);
// TODO handle Map and Set as well
if (prototype === object_prototype || prototype === array_prototype) { if (prototype === object_prototype || prototype === array_prototype) {
const proxy = new Proxy(value, state_proxy_handler); const proxy = new Proxy(value, state_proxy_handler);

@ -41,7 +41,7 @@ export function derived(fn) {
/** @type {import('#client').DerivedDebug} */ (signal).inspect = new Set(); /** @type {import('#client').DerivedDebug} */ (signal).inspect = new Set();
} }
if (current_reaction !== null) { if (current_reaction !== null && (current_reaction.f & DERIVED) !== 0) {
if (current_reaction.deriveds === null) { if (current_reaction.deriveds === null) {
current_reaction.deriveds = [signal]; current_reaction.deriveds = [signal];
} else { } else {

@ -1,22 +1,34 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { import {
check_dirtiness,
current_block, current_block,
current_component_context, current_component_context,
current_effect, current_effect,
current_reaction, current_reaction,
destroy_children, destroy_children,
execute_effect,
get, get,
remove_reactions, remove_reactions,
schedule_effect, schedule_effect,
set_signal_status, set_signal_status,
untrack untrack
} from '../runtime.js'; } from '../runtime.js';
import { DIRTY, MANAGED, RENDER_EFFECT, EFFECT, PRE_EFFECT, DESTROYED } from '../constants.js'; import {
DIRTY,
MANAGED,
RENDER_EFFECT,
EFFECT,
PRE_EFFECT,
DESTROYED,
INERT,
IS_ELSEIF
} from '../constants.js';
import { set } from './sources.js'; import { set } from './sources.js';
import { noop } from '../../common.js';
/** /**
* @param {import('./types.js').EffectType} type * @param {import('./types.js').EffectType} type
* @param {(() => void | (() => void)) | ((b: import('#client').Block) => void | (() => void))} fn * @param {(() => void | (() => void))} fn
* @param {boolean} sync * @param {boolean} sync
* @param {null | import('#client').Block} block * @param {null | import('#client').Block} block
* @param {boolean} init * @param {boolean} init
@ -25,6 +37,7 @@ import { set } from './sources.js';
function create_effect(type, fn, sync, block = current_block, init = true) { function create_effect(type, fn, sync, block = current_block, init = true) {
/** @type {import('#client').Effect} */ /** @type {import('#client').Effect} */
const signal = { const signal = {
parent: current_effect,
block, block,
deps: null, deps: null,
f: type | DIRTY, f: type | DIRTY,
@ -34,7 +47,8 @@ function create_effect(type, fn, sync, block = current_block, init = true) {
deriveds: null, deriveds: null,
teardown: null, teardown: null,
ctx: current_component_context, ctx: current_component_context,
ondestroy: null ondestroy: null,
transitions: null
}; };
if (current_effect !== null) { if (current_effect !== null) {
@ -201,8 +215,7 @@ export function invalidate_effect(fn) {
} }
/** /**
* @template {import('#client').Block} B * @param {(() => void)} fn
* @param {(block: B) => void | (() => void)} fn
* @param {any} block * @param {any} block
* @param {any} managed * @param {any} managed
* @param {any} sync * @param {any} sync
@ -217,22 +230,128 @@ export function render_effect(fn, block = current_block, managed = false, sync =
} }
/** /**
* @param {import('#client').Effect} signal * @param {import('#client').Effect} effect
* @returns {void} * @returns {void}
*/ */
export function destroy_effect(signal) { export function destroy_effect(effect) {
destroy_children(signal); destroy_children(effect);
remove_reactions(signal, 0); remove_reactions(effect, 0);
set_signal_status(signal, DESTROYED); set_signal_status(effect, DESTROYED);
signal.teardown?.(); if (effect.transitions) {
signal.ondestroy?.(); for (const transition of effect.transitions) {
signal.fn = transition.stop();
signal.effects = }
signal.teardown = }
signal.ondestroy =
signal.ctx = effect.teardown?.();
signal.block = effect.ondestroy?.();
signal.deps =
// @ts-expect-error
effect.fn =
effect.effects =
effect.teardown =
effect.ondestroy =
effect.ctx =
effect.block =
effect.deps =
null; null;
} }
/**
* When a block effect is removed, we don't immediately destroy it or yank it
* out of the DOM, because it might have transitions. Instead, we 'pause' it.
* It stays around (in memory, and in the DOM) until outro transitions have
* completed, and if the state change is reversed then we _resume_ it.
* A paused effect does not update, and the DOM subtree becomes inert.
* @param {import('#client').Effect} effect
* @param {() => void} callback
*/
export function pause_effect(effect, callback = noop) {
/** @type {import('#client').TransitionManager[]} */
const transitions = [];
pause_children(effect, transitions, true);
let remaining = transitions.length;
if (remaining > 0) {
const check = () => {
if (!--remaining) {
destroy_effect(effect);
callback();
}
};
for (const transition of transitions) {
transition.out(check);
}
} else {
destroy_effect(effect);
callback();
}
}
/**
* @param {import('#client').Effect} effect
* @param {import('#client').TransitionManager[]} transitions
* @param {boolean} local
*/
function pause_children(effect, transitions, local) {
if ((effect.f & INERT) !== 0) return;
effect.f ^= INERT;
if (effect.transitions !== null) {
for (const transition of effect.transitions) {
if (transition.is_global || local) {
transitions.push(transition);
}
}
}
if (effect.effects !== null) {
for (const child of effect.effects) {
var transparent = (child.f & IS_ELSEIF) !== 0 || (child.f & MANAGED) !== 0;
pause_children(child, transitions, transparent ? local : false);
}
}
}
/**
* The opposite of `pause_effect`. We call this if (for example)
* `x` becomes falsy then truthy: `{#if x}...{/if}`
* @param {import('#client').Effect} effect
*/
export function resume_effect(effect) {
resume_children(effect, true);
}
/**
* @param {import('#client').Effect} effect
* @param {boolean} local
*/
function resume_children(effect, local) {
if ((effect.f & INERT) === 0) return;
effect.f ^= INERT;
// If a dependency of this effect changed while it was paused,
// apply the change now
if (check_dirtiness(effect)) {
execute_effect(effect);
}
if (effect.effects !== null) {
for (const child of effect.effects) {
var transparent = (child.f & IS_ELSEIF) !== 0 || (child.f & MANAGED) !== 0;
resume_children(child, transparent ? local : false);
}
}
if (effect.transitions !== null) {
for (const transition of effect.transitions) {
if (transition.is_global || local) {
transition.in();
}
}
}
}

@ -8,7 +8,7 @@ import {
import { get_descriptor, is_function } from '../utils.js'; import { get_descriptor, is_function } from '../utils.js';
import { mutable_source, set } from './sources.js'; import { mutable_source, set } from './sources.js';
import { derived } from './deriveds.js'; import { derived } from './deriveds.js';
import { get, inspect_fn, is_signals_recorded } from '../runtime.js'; import { get, inspect_fn, is_signals_recorded, untrack } from '../runtime.js';
import { safe_equals, safe_not_equal } from './equality.js'; import { safe_equals, safe_not_equal } from './equality.js';
/** /**
@ -133,16 +133,30 @@ export function spread_props(...props) {
* @param {Record<string, unknown>} props * @param {Record<string, unknown>} props
* @param {string} key * @param {string} key
* @param {number} flags * @param {number} flags
* @param {V | (() => V)} [initial] * @param {V | (() => V)} [fallback]
* @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))} * @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))}
*/ */
export function prop(props, key, flags, initial) { export function prop(props, key, flags, fallback) {
var immutable = (flags & PROPS_IS_IMMUTABLE) !== 0; var immutable = (flags & PROPS_IS_IMMUTABLE) !== 0;
var runes = (flags & PROPS_IS_RUNES) !== 0; var runes = (flags & PROPS_IS_RUNES) !== 0;
var lazy = (flags & PROPS_IS_LAZY_INITIAL) !== 0;
var prop_value = /** @type {V} */ (props[key]); var prop_value = /** @type {V} */ (props[key]);
var setter = get_descriptor(props, key)?.set; var setter = get_descriptor(props, key)?.set;
if (prop_value === undefined && initial !== undefined) { var fallback_value = /** @type {V} */ (fallback);
var fallback_dirty = true;
var get_fallback = () => {
if (lazy && fallback_dirty) {
fallback_dirty = false;
fallback_value = untrack(/** @type {() => V} */ (fallback));
}
return fallback_value;
};
if (prop_value === undefined && fallback !== undefined) {
if (setter && runes) { if (setter && runes) {
// TODO consolidate all these random runtime errors // TODO consolidate all these random runtime errors
throw new Error( throw new Error(
@ -153,18 +167,21 @@ export function prop(props, key, flags, initial) {
); );
} }
// @ts-expect-error would need a cumbersome method overload to type this prop_value = get_fallback();
if ((flags & PROPS_IS_LAZY_INITIAL) !== 0) initial = initial();
prop_value = /** @type {V} */ (initial);
if (setter) setter(prop_value); if (setter) setter(prop_value);
} }
var getter = () => { var getter = runes
? () => {
var value = /** @type {V} */ (props[key]);
if (value === undefined) return get_fallback();
fallback_dirty = true;
return value;
}
: () => {
var value = /** @type {V} */ (props[key]); var value = /** @type {V} */ (props[key]);
if (value !== undefined) initial = undefined; if (value !== undefined) fallback_value = /** @type {V} */ (undefined);
return value === undefined ? /** @type {V} */ (initial) : value; return value === undefined ? fallback_value : value;
}; };
// easy mode — prop is never written to // easy mode — prop is never written to

@ -1,4 +1,4 @@
import type { Block, ComponentContext, Equals } from '#client'; import type { Block, ComponentContext, Equals, TransitionManager } from '#client';
import type { EFFECT, PRE_EFFECT, RENDER_EFFECT } from '../constants'; import type { EFFECT, PRE_EFFECT, RENDER_EFFECT } from '../constants';
export type EffectType = typeof EFFECT | typeof PRE_EFFECT | typeof RENDER_EFFECT; export type EffectType = typeof EFFECT | typeof PRE_EFFECT | typeof RENDER_EFFECT;
@ -21,7 +21,7 @@ export interface Value<V = unknown> extends Signal {
export interface Reaction extends Signal { export interface Reaction extends Signal {
/** The reaction function */ /** The reaction function */
fn: null | Function; fn: Function;
/** Signals that this signal reads from */ /** Signals that this signal reads from */
deps: null | Value[]; deps: null | Value[];
/** Effects created inside this signal */ /** Effects created inside this signal */
@ -36,6 +36,7 @@ export interface Derived<V = unknown> extends Value<V>, Reaction {
} }
export interface Effect extends Reaction { export interface Effect extends Reaction {
parent: Effect | null;
/** The block associated with this effect */ /** The block associated with this effect */
block: null | Block; block: null | Block;
/** The associated component context */ /** The associated component context */
@ -43,11 +44,13 @@ export interface Effect extends Reaction {
/** Stuff to do when the effect is destroyed */ /** Stuff to do when the effect is destroyed */
ondestroy: null | (() => void); ondestroy: null | (() => void);
/** The effect function */ /** The effect function */
fn: null | (() => void | (() => void)) | ((b: Block, s: Signal) => void | (() => void)); fn: () => void | (() => void);
/** The teardown function returned from the effect function */ /** The teardown function returned from the effect function */
teardown: null | (() => void); teardown: null | (() => void);
/** The depth from the root signal, used for ordering render/pre-effects topologically **/ /** The depth from the root signal, used for ordering render/pre-effects topologically **/
l: number; l: number;
/** Transition managers created with `$.transition` */
transitions: null | TransitionManager[];
} }
export interface ValueDebug<V = unknown> extends Value<V> { export interface ValueDebug<V = unknown> extends Value<V> {

@ -12,7 +12,6 @@ import {
set_current_hydration_fragment set_current_hydration_fragment
} from './dom/hydration.js'; } from './dom/hydration.js';
import { array_from } from './utils.js'; import { array_from } from './utils.js';
import { ROOT_BLOCK } from './constants.js';
import { handle_event_propagation } from './dom/elements/events.js'; import { handle_event_propagation } from './dom/elements/events.js';
/** @type {Set<string>} */ /** @type {Set<string>} */
@ -21,6 +20,18 @@ export const all_registered_events = new Set();
/** @type {Set<(events: Array<string>) => void>} */ /** @type {Set<(events: Array<string>) => void>} */
export const root_event_handles = new Set(); export const root_event_handles = new Set();
/**
* This is normally true — block effects should run their intro transitions —
* but is false during hydration and mounting (unless `options.intro` is `true`)
* and when creating the children of a `<svelte:element>` that just changed tag
*/
export let should_intro = true;
/** @param {boolean} value */
export function set_should_intro(value) {
should_intro = value;
}
/** /**
* @param {Element} dom * @param {Element} dom
* @param {() => string} value * @param {() => string} value
@ -51,19 +62,19 @@ export function text(dom, value) {
} }
/** /**
* @param {Comment} anchor_node * @param {Comment} anchor
* @param {void | ((anchor: Comment, slot_props: Record<string, unknown>) => void)} slot_fn * @param {void | ((anchor: Comment, slot_props: Record<string, unknown>) => void)} slot_fn
* @param {Record<string, unknown>} slot_props * @param {Record<string, unknown>} slot_props
* @param {null | ((anchor: Comment) => void)} fallback_fn * @param {null | ((anchor: Comment) => void)} fallback_fn
*/ */
export function slot(anchor_node, slot_fn, slot_props, fallback_fn) { export function slot(anchor, slot_fn, slot_props, fallback_fn) {
hydrate_block_anchor(anchor_node); hydrate_block_anchor(anchor);
if (slot_fn === undefined) { if (slot_fn === undefined) {
if (fallback_fn !== null) { if (fallback_fn !== null) {
fallback_fn(anchor_node); fallback_fn(anchor);
} }
} else { } else {
slot_fn(anchor_node, slot_props); slot_fn(anchor, slot_props);
} }
} }
@ -198,20 +209,12 @@ function _mount(Component, options) {
const registered_events = new Set(); const registered_events = new Set();
const container = options.target; const container = options.target;
/** @type {import('#client').RootBlock} */ should_intro = options.intro ?? false;
/** @type {import('#client').Block} */
const block = { const block = {
// dom // dom
d: null, d: null
// effect
e: null,
// intro
i: options.intro || false,
// parent
p: null,
// transition
r: null,
// type
t: ROOT_BLOCK
}; };
/** @type {Exports} */ /** @type {Exports} */
@ -242,10 +245,12 @@ function _mount(Component, options) {
block, block,
true true
); );
block.e = effect;
const bound_event_listener = handle_event_propagation.bind(null, container); const bound_event_listener = handle_event_propagation.bind(null, container);
const bound_document_event_listener = handle_event_propagation.bind(null, document); const bound_document_event_listener = handle_event_propagation.bind(null, document);
should_intro = true;
/** @param {Array<string>} events */ /** @param {Array<string>} events */
const event_handle = (events) => { const event_handle = (events) => {
for (let i = 0; i < events.length; i++) { for (let i = 0; i < events.length; i++) {
@ -290,7 +295,7 @@ function _mount(Component, options) {
if (dom !== null) { if (dom !== null) {
remove(dom); remove(dom);
} }
destroy_effect(/** @type {import('./types.js').Effect} */ (block.e)); destroy_effect(effect);
}); });
return component; return component;

@ -21,7 +21,8 @@ import {
DESTROYED, DESTROYED,
INERT, INERT,
MANAGED, MANAGED,
STATE_SYMBOL STATE_SYMBOL,
EFFECT_RAN
} from './constants.js'; } from './constants.js';
import { flush_tasks } from './dom/task.js'; import { flush_tasks } from './dom/task.js';
import { add_owner } from './dev/ownership.js'; import { add_owner } from './dev/ownership.js';
@ -54,9 +55,19 @@ let flush_count = 0;
/** @type {null | import('./types.js').Reaction} */ /** @type {null | import('./types.js').Reaction} */
export let current_reaction = null; export let current_reaction = null;
/** @param {null | import('./types.js').Reaction} reaction */
export function set_current_reaction(reaction) {
current_reaction = reaction;
}
/** @type {null | import('./types.js').Effect} */ /** @type {null | import('./types.js').Effect} */
export let current_effect = null; export let current_effect = null;
/** @param {null | import('./types.js').Effect} effect */
export function set_current_effect(effect) {
current_effect = effect;
}
/** @type {null | import('./types.js').Value[]} */ /** @type {null | import('./types.js').Value[]} */
export let current_dependencies = null; export let current_dependencies = null;
let current_dependencies_index = 0; let current_dependencies_index = 0;
@ -104,6 +115,11 @@ export let current_block = null;
/** @type {import('./types.js').ComponentContext | null} */ /** @type {import('./types.js').ComponentContext | null} */
export let current_component_context = null; export let current_component_context = null;
/** @param {import('./types.js').ComponentContext | null} context */
export function set_current_component_context(context) {
current_component_context = context;
}
/** @returns {boolean} */ /** @returns {boolean} */
export function is_runes() { export function is_runes() {
return current_component_context !== null && current_component_context.r; return current_component_context !== null && current_component_context.r;
@ -147,7 +163,7 @@ export function batch_inspect(target, prop, receiver) {
* @param {import('./types.js').Reaction} reaction * @param {import('./types.js').Reaction} reaction
* @returns {boolean} * @returns {boolean}
*/ */
function check_dirtiness(reaction) { export function check_dirtiness(reaction) {
var flags = reaction.f; var flags = reaction.f;
if ((flags & DIRTY) !== 0) { if ((flags & DIRTY) !== 0) {
@ -200,7 +216,6 @@ function check_dirtiness(reaction) {
export function execute_reaction_fn(signal) { export function execute_reaction_fn(signal) {
const fn = signal.fn; const fn = signal.fn;
const flags = signal.f; const flags = signal.f;
const is_render_effect = (flags & RENDER_EFFECT) !== 0;
const previous_dependencies = current_dependencies; const previous_dependencies = current_dependencies;
const previous_dependencies_index = current_dependencies_index; const previous_dependencies_index = current_dependencies_index;
@ -217,19 +232,7 @@ export function execute_reaction_fn(signal) {
current_untracking = false; current_untracking = false;
try { try {
let res; let res = fn();
if (is_render_effect) {
res = /** @type {(block: import('#client').Block, signal: import('#client').Signal) => V} */ (
fn
)(
/** @type {import('#client').Block} */ (
/** @type {import('#client').Effect} */ (signal).block
),
/** @type {import('#client').Signal} */ (signal)
);
} else {
res = /** @type {() => V} */ (fn)();
}
let dependencies = /** @type {import('./types.js').Value<unknown>[]} **/ (signal.deps); let dependencies = /** @type {import('./types.js').Value<unknown>[]} **/ (signal.deps);
if (current_dependencies !== null) { if (current_dependencies !== null) {
let i; let i;
@ -548,6 +551,8 @@ export function schedule_effect(signal, sync) {
} }
} }
} }
signal.f |= EFFECT_RAN;
} }
/** /**

@ -9,5 +9,6 @@ const now = is_client ? () => performance.now() : () => Date.now();
/** @type {import('./types.js').Raf} */ /** @type {import('./types.js').Raf} */
export const raf = { export const raf = {
tick: /** @param {any} _ */ (_) => request_animation_frame(_), tick: /** @param {any} _ */ (_) => request_animation_frame(_),
now: () => now() now: () => now(),
tasks: new Set()
}; };

@ -1,16 +1,4 @@
import { import { STATE_SYMBOL } from './constants.js';
ROOT_BLOCK,
EACH_BLOCK,
EACH_ITEM_BLOCK,
IF_BLOCK,
AWAIT_BLOCK,
KEY_BLOCK,
HEAD_BLOCK,
DYNAMIC_COMPONENT_BLOCK,
DYNAMIC_ELEMENT_BLOCK,
SNIPPET_BLOCK,
STATE_SYMBOL
} from './constants.js';
import type { Effect, Source, Value } from './reactivity/types.js'; import type { Effect, Source, Value } from './reactivity/types.js';
type EventCallback = (event: Event) => boolean; type EventCallback = (event: Event) => boolean;
@ -59,230 +47,79 @@ export type ComponentContext = {
export type Equals = (this: Value, value: unknown) => boolean; export type Equals = (this: Value, value: unknown) => boolean;
export type BlockType =
| typeof ROOT_BLOCK
| typeof EACH_BLOCK
| typeof EACH_ITEM_BLOCK
| typeof IF_BLOCK
| typeof AWAIT_BLOCK
| typeof KEY_BLOCK
| typeof SNIPPET_BLOCK
| typeof HEAD_BLOCK
| typeof DYNAMIC_COMPONENT_BLOCK
| typeof DYNAMIC_ELEMENT_BLOCK;
export type TemplateNode = Text | Element | Comment; export type TemplateNode = Text | Element | Comment;
export type Transition = { export interface Block {
/** effect */
e: Effect;
/** payload */
p: null | TransitionPayload;
/** init */
i: (from?: DOMRect) => TransitionPayload;
/** finished */
f: (fn: () => void) => void;
in: () => void;
/** out */
o: () => void;
/** cancel */
c: () => void;
/** cleanup */
x: () => void;
/** direction */
r: 'in' | 'out' | 'both' | 'key';
/** dom */
d: HTMLElement;
};
export type RootBlock = {
/** dom */
d: null | TemplateNode | Array<TemplateNode>;
/** effect */
e: null | Effect;
/** intro */
i: boolean;
/** parent */
p: null;
/** transition */
r: null | ((transition: Transition) => void);
/** type */
t: typeof ROOT_BLOCK;
};
export type IfBlock = {
/** value */
v: boolean;
/** dom */
d: null | TemplateNode | Array<TemplateNode>;
/** effect */
e: null | Effect;
/** parent */
p: Block;
/** transition */
r: null | ((transition: Transition) => void);
/** consequent transitions */
c: null | Set<Transition>;
/** alternate transitions */
a: null | Set<Transition>;
/** effect */
ce: null | Effect;
/** effect */
ae: null | Effect;
/** type */
t: typeof IF_BLOCK;
};
export type KeyBlock = {
/** dom */
d: null | TemplateNode | Array<TemplateNode>;
/** effect */
e: null | Effect;
/** parent */
p: Block;
/** transition */
r: null | ((transition: Transition) => void);
/** type */
t: typeof KEY_BLOCK;
};
export type HeadBlock = {
/** dom */
d: null | TemplateNode | Array<TemplateNode>;
/** effect */
e: null | Effect;
/** parent */
p: Block;
/** transition */
r: null | ((transition: Transition) => void);
/** type */
t: typeof HEAD_BLOCK;
};
export type DynamicElementBlock = {
/** dom */ /** dom */
d: null | TemplateNode | Array<TemplateNode>; d: null | TemplateNode | Array<TemplateNode>;
/** effect */ }
e: null | Effect;
/** parent */
p: Block;
/** transition */
r: null | ((transition: Transition) => void);
/** type */
t: typeof DYNAMIC_ELEMENT_BLOCK;
};
export type DynamicComponentBlock = {
/** dom */
d: null | TemplateNode | Array<TemplateNode>;
/** effect */
e: null | Effect;
/** parent */
p: Block;
/** transition */
r: null | ((transition: Transition) => void);
/** type */
t: typeof DYNAMIC_COMPONENT_BLOCK;
};
export type AwaitBlock = {
/** dom */
d: null | TemplateNode | Array<TemplateNode>;
/** effect */
e: null | Effect;
/** parent */
p: Block;
/** pending */
n: boolean;
/** transition */
r: null | ((transition: Transition) => void);
/** type */
t: typeof AWAIT_BLOCK;
};
export type EachBlock = { export type EachState = {
/** anchor */
a: Element | Comment;
/** flags */ /** flags */
f: number; flags: number;
/** dom */
d: null | TemplateNode | Array<TemplateNode>;
/** items */ /** items */
v: EachItemBlock[]; items: EachItem[];
/** effewct */
e: null | Effect;
/** parent */
p: Block;
/** transition */
r: null | ((transition: Transition) => void);
/** transitions */
s: Array<EachItemBlock>;
/** type */
t: typeof EACH_BLOCK;
}; };
export type EachItemBlock = { export type EachItem = {
/** transition */ /** animation manager */
a: null | ((block: EachItemBlock, transitions: Set<Transition>) => void); a: AnimationManager | null;
/** dom */ /** dom */
d: null | TemplateNode | Array<TemplateNode>; d: null | TemplateNode | Array<TemplateNode>;
/** effect */ /** effect */
e: null | Effect; e: Effect;
/** item */ /** item */
v: any | Source<any>; v: any | Source<any>;
/** index */ /** index */
i: number | Source<number>; i: number | Source<number>;
/** key */ /** key */
k: unknown; k: unknown;
/** parent */
p: EachBlock;
/** transition */
r: null | ((transition: Transition) => void);
/** transitions */
s: null | Set<Transition>;
/** type */
t: typeof EACH_ITEM_BLOCK;
}; };
export type SnippetBlock = { export interface TransitionManager {
/** dom */ /** Whether the `global` modifier was used (i.e. `transition:fade|global`) */
d: null | TemplateNode | Array<TemplateNode>; is_global: boolean;
/** parent */ /** Called inside `resume_effect` */
p: Block; in: () => void;
/** effect */ /** Called inside `pause_effect` */
e: null | Effect; out: (callback?: () => void) => void;
/** transition */ /** Called inside `destroy_effect` */
r: null; stop: () => void;
/** type */ }
t: typeof SNIPPET_BLOCK;
};
export type Block = export interface AnimationManager {
| RootBlock /** An element with an `animate:` directive */
| IfBlock element: Element;
| AwaitBlock /** Called during keyed each block reconciliation, before updates */
| DynamicElementBlock measure: () => void;
| DynamicComponentBlock /** Called during keyed each block reconciliation, after updates — this triggers the animation */
| HeadBlock apply: () => void;
| KeyBlock }
| EachBlock
| EachItemBlock export interface Animation {
| SnippetBlock; /** Abort the animation */
abort: () => void;
/** Allow the animation to continue running, but remove any callback. This prevents the removal of an outroing block if the corresponding intro has a `delay` */
deactivate: () => void;
/** Resets an animation to its starting state, if it uses `tick`. Exposed as a separate method so that an aborted `out:` can still reset even if the `outro` had already completed */
reset: () => void;
/** Get the `t` value (between `0` and `1`) of the animation, so that its counterpart can start from the right place */
t: (now: number) => number;
}
export type TransitionFn<P> = ( export type TransitionFn<P> = (
element: Element, element: Element,
props: P, props: P,
options: { direction?: 'in' | 'out' | 'both' } options: { direction?: 'in' | 'out' | 'both' }
) => TransitionPayload; ) => AnimationConfig | ((options: { direction?: 'in' | 'out' }) => AnimationConfig);
export type AnimateFn<P> = ( export type AnimateFn<P> = (
element: Element, element: Element,
rects: { from: DOMRect; to: DOMRect }, rects: { from: DOMRect; to: DOMRect },
props: P, props: P
options: {} ) => AnimationConfig;
) => TransitionPayload;
export type TransitionPayload = { export type AnimationConfig = {
delay?: number; delay?: number;
duration?: number; duration?: number;
easing?: (t: number) => number; easing?: (t: number) => number;
@ -307,15 +144,17 @@ export type Render = {
d: null | TemplateNode | Array<TemplateNode>; d: null | TemplateNode | Array<TemplateNode>;
/** effect */ /** effect */
e: null | Effect; e: null | Effect;
/** transitions */
s: Set<Transition>;
/** prev */ /** prev */
p: Render | null; p: Render | null;
}; };
export type Raf = { export type Raf = {
/** Alias for `requestAnimationFrame`, exposed in such a way that we can override in tests */
tick: (callback: (time: DOMHighResTimeStamp) => void) => any; tick: (callback: (time: DOMHighResTimeStamp) => void) => any;
/** Alias for `performance.now()`, exposed in such a way that we can override in tests */
now: () => number; now: () => number;
/** A set of tasks that will run to completion, unless aborted */
tasks: Set<TaskEntry>;
}; };
export interface Task { export interface Task {

@ -11,13 +11,13 @@ import * as $ from '../internal/index.js';
* @template {Record<string, any>} Events * @template {Record<string, any>} Events
* @template {Record<string, any>} Slots * @template {Record<string, any>} Slots
* *
* @param {import('../main/public.js').ComponentConstructorOptions<Props> & { * @param {import('svelte').ComponentConstructorOptions<Props> & {
* component: import('../main/public.js').SvelteComponent<Props, Events, Slots>; * component: import('svelte').ComponentType<import('svelte').SvelteComponent<Props, Events, Slots>>;
* immutable?: boolean; * immutable?: boolean;
* hydrate?: boolean; * hydrate?: boolean;
* recover?: boolean; * recover?: boolean;
* }} options * }} options
* @returns {import('../main/public.js').SvelteComponent<Props, Events, Slots> & Exports} * @returns {import('svelte').SvelteComponent<Props, Events, Slots> & Exports}
*/ */
export function createClassComponent(options) { export function createClassComponent(options) {
// @ts-expect-error $$prop_def etc are not actually defined // @ts-expect-error $$prop_def etc are not actually defined
@ -34,8 +34,8 @@ export function createClassComponent(options) {
* @template {Record<string, any>} Events * @template {Record<string, any>} Events
* @template {Record<string, any>} Slots * @template {Record<string, any>} Slots
* *
* @param {import('../main/public.js').SvelteComponent<Props, Events, Slots>} component * @param {import('svelte').SvelteComponent<Props, Events, Slots>} component
* @returns {import('../main/public.js').ComponentType<import('../main/public.js').SvelteComponent<Props, Events, Slots> & Exports>} * @returns {import('svelte').ComponentType<import('svelte').SvelteComponent<Props, Events, Slots> & Exports>}
*/ */
export function asClassComponent(component) { export function asClassComponent(component) {
// @ts-expect-error $$prop_def etc are not actually defined // @ts-expect-error $$prop_def etc are not actually defined
@ -58,7 +58,7 @@ class Svelte4Component {
#instance; #instance;
/** /**
* @param {import('../main/public.js').ComponentConstructorOptions & { * @param {import('svelte').ComponentConstructorOptions & {
* component: any; * component: any;
* immutable?: boolean; * immutable?: boolean;
* hydrate?: boolean; * hydrate?: boolean;
@ -79,6 +79,7 @@ class Svelte4Component {
}); });
for (const key of Object.keys(this.#instance)) { for (const key of Object.keys(this.#instance)) {
if (key === '$set' || key === '$destroy' || key === '$on') continue;
define_property(this, key, { define_property(this, key, {
get() { get() {
return this.#instance[key]; return this.#instance[key];

@ -55,29 +55,30 @@ const write = [
'setYear' 'setYear'
]; ];
var inited = false;
export class ReactiveDate extends Date { export class ReactiveDate extends Date {
#raw_time = source(super.getTime()); #raw_time = source(super.getTime());
static #inited = false;
// We init as part of the first instance so that we can treeshake this class // We init as part of the first instance so that we can treeshake this class
#init() { #init() {
if (!ReactiveDate.#inited) { if (!inited) {
ReactiveDate.#inited = true; inited = true;
const proto = ReactiveDate.prototype; const proto = ReactiveDate.prototype;
const date_proto = Date.prototype; const date_proto = Date.prototype;
for (const method of read) { for (const method of read) {
// @ts-ignore // @ts-ignore
proto[method] = function () { proto[method] = function (...args) {
get(this.#raw_time); get(this.#raw_time);
// @ts-ignore // @ts-ignore
return date_proto[method].call(this); return date_proto[method].apply(this, args);
}; };
} }
for (const method of write) { for (const method of write) {
// @ts-ignore // @ts-ignore
proto[method] = function (/** @type {any} */ ...args) { proto[method] = function (...args) {
// @ts-ignore // @ts-ignore
const v = date_proto[method].apply(this, args); const v = date_proto[method].apply(this, args);
const time = date_proto.getTime.call(this); const time = date_proto.getTime.call(this);

@ -1,2 +1,3 @@
export { ReactiveDate as Date } from './date.js'; export { ReactiveDate as Date } from './date.js';
export { ReactiveSet as Set } from './set.js'; export { ReactiveSet as Set } from './set.js';
export { ReactiveMap as Map } from './map.js';

@ -0,0 +1,157 @@
import { DEV } from 'esm-env';
import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
import { UNINITIALIZED } from '../internal/client/constants.js';
import { map } from './utils.js';
/**
* @template K
* @template V
*/
export class ReactiveMap extends Map {
/** @type {Map<K, import('#client').Source<V>>} */
#sources = new Map();
#version = source(0);
#size = source(0);
/**
* @param {Iterable<readonly [K, V]> | null | undefined} [value]
*/
constructor(value) {
super();
// If the value is invalid then the native exception will fire here
if (DEV) new Map(value);
if (value) {
var sources = this.#sources;
for (var [key, v] of value) {
sources.set(key, source(v));
super.set(key, v);
}
this.#size.v = sources.size;
}
}
#increment_version() {
set(this.#version, this.#version.v + 1);
}
/** @param {K} key */
has(key) {
var s = this.#sources.get(key);
if (s === undefined) {
// We should always track the version in case
// the Set ever gets this value in the future.
get(this.#version);
return false;
}
get(s);
return true;
}
/**
* @param {(value: V, key: K, map: Map<K, V>) => void} callbackfn
* @param {any} [this_arg]
*/
forEach(callbackfn, this_arg) {
get(this.#version);
return super.forEach(callbackfn, this_arg);
}
/** @param {K} key */
get(key) {
var s = this.#sources.get(key);
if (s === undefined) {
// We should always track the version in case
// the Set ever gets this value in the future.
get(this.#version);
return undefined;
}
return get(s);
}
/**
* @param {K} key
* @param {V} value
* */
set(key, value) {
var sources = this.#sources;
var s = sources.get(key);
if (s === undefined) {
sources.set(key, source(value));
set(this.#size, sources.size);
this.#increment_version();
} else {
set(s, value);
}
return super.set(key, value);
}
/** @param {K} key */
delete(key) {
var sources = this.#sources;
var s = sources.get(key);
if (s !== undefined) {
sources.delete(key);
set(this.#size, sources.size);
set(s, /** @type {V} */ (UNINITIALIZED));
this.#increment_version();
}
return super.delete(key);
}
clear() {
var sources = this.#sources;
if (sources.size !== 0) {
set(this.#size, 0);
for (var s of sources.values()) {
set(s, /** @type {V} */ (UNINITIALIZED));
}
this.#increment_version();
}
sources.clear();
super.clear();
}
keys() {
get(this.#version);
return this.#sources.keys();
}
values() {
get(this.#version);
return map(this.#sources.values(), get);
}
entries() {
get(this.#version);
return map(
this.#sources.entries(),
([key, source]) => /** @type {[K, V]} */ ([key, get(source)])
);
}
[Symbol.iterator]() {
return this.entries();
}
get size() {
return get(this.#size);
}
}

@ -0,0 +1,151 @@
import { pre_effect, user_root_effect } from '../internal/client/reactivity/effects.js';
import { flushSync } from '../main/main-client.js';
import { ReactiveMap } from './map.js';
import { assert, test } from 'vitest';
test('map.values()', () => {
const map = new ReactiveMap([
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5]
]);
const log: any = [];
const cleanup = user_root_effect(() => {
pre_effect(() => {
log.push(map.size);
});
pre_effect(() => {
log.push(map.has(3));
});
pre_effect(() => {
log.push(Array.from(map.values()));
});
});
flushSync(() => {
map.delete(3);
});
flushSync(() => {
map.clear();
});
assert.deepEqual(log, [5, true, [1, 2, 3, 4, 5], 4, false, [1, 2, 4, 5], 0, [], false]); // TODO update when we fix effect ordering bug
cleanup();
});
test('map.get(...)', () => {
const map = new ReactiveMap([
[1, 1],
[2, 2],
[3, 3]
]);
const log: any = [];
const cleanup = user_root_effect(() => {
pre_effect(() => {
log.push('get 1', map.get(1));
});
pre_effect(() => {
log.push('get 2', map.get(2));
});
pre_effect(() => {
log.push('get 3', map.get(3));
});
});
flushSync(() => {
map.delete(2);
});
flushSync(() => {
map.set(2, 2);
});
assert.deepEqual(log, ['get 1', 1, 'get 2', 2, 'get 3', 3, 'get 2', undefined, 'get 2', 2]);
cleanup();
});
test('map.has(...)', () => {
const map = new ReactiveMap([
[1, 1],
[2, 2],
[3, 3]
]);
const log: any = [];
const cleanup = user_root_effect(() => {
pre_effect(() => {
log.push('has 1', map.has(1));
});
pre_effect(() => {
log.push('has 2', map.has(2));
});
pre_effect(() => {
log.push('has 3', map.has(3));
});
});
flushSync(() => {
map.delete(2);
});
flushSync(() => {
map.set(2, 2);
});
assert.deepEqual(log, [
'has 1',
true,
'has 2',
true,
'has 3',
true,
'has 2',
false,
'has 2',
true
]);
cleanup();
});
test('map handling of undefined values', () => {
const map = new ReactiveMap();
const log: any = [];
const cleanup = user_root_effect(() => {
map.set(1, undefined);
pre_effect(() => {
log.push(map.get(1));
});
flushSync(() => {
map.delete(1);
});
flushSync(() => {
map.set(1, 1);
});
});
assert.deepEqual(log, [undefined, undefined, 1]);
cleanup();
});

@ -1,33 +1,10 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { source, set } from '../internal/client/reactivity/sources.js'; import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js'; import { get } from '../internal/client/runtime.js';
import { map } from './utils.js';
var read = [ var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf'];
'difference', var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union'];
'forEach',
'intersection',
'isDisjointFrom',
'isSubsetOf',
'isSupersetOf',
'symmetricDifference',
'union'
];
/**
* @template T
* @param {IterableIterator<T>} iterator
*/
function make_iterable(iterator) {
iterator[Symbol.iterator] = get_self;
return iterator;
}
/**
* @this {any}
*/
function get_self() {
return this;
}
var inited = false; var inited = false;
@ -41,7 +18,7 @@ export class ReactiveSet extends Set {
#size = source(0); #size = source(0);
/** /**
* @param {Iterable<T> | null | undefined} value * @param {Iterable<T> | null | undefined} [value]
*/ */
constructor(value) { constructor(value) {
super(); super();
@ -50,9 +27,14 @@ export class ReactiveSet extends Set {
if (DEV) new Set(value); if (DEV) new Set(value);
if (value) { if (value) {
var sources = this.#sources;
for (var element of value) { for (var element of value) {
this.add(element); sources.set(element, source(true));
super.add(element);
} }
this.#size.v = sources.size;
} }
if (!inited) this.#init(); if (!inited) this.#init();
@ -65,7 +47,10 @@ export class ReactiveSet extends Set {
var proto = ReactiveSet.prototype; var proto = ReactiveSet.prototype;
var set_proto = Set.prototype; var set_proto = Set.prototype;
for (var method of read) { /** @type {string} */
var method;
for (method of read_methods) {
// @ts-ignore // @ts-ignore
proto[method] = function (...v) { proto[method] = function (...v) {
get(this.#version); get(this.#version);
@ -73,6 +58,17 @@ export class ReactiveSet extends Set {
return set_proto[method].apply(this, v); return set_proto[method].apply(this, v);
}; };
} }
for (method of set_like_methods) {
// @ts-ignore
proto[method] = function (...v) {
get(this.#version);
// @ts-ignore
var set = /** @type {Set<T>} */ (set_proto[method].apply(this, v));
return new ReactiveSet(set);
};
}
} }
#increment_version() { #increment_version() {
@ -81,9 +77,9 @@ export class ReactiveSet extends Set {
/** @param {T} value */ /** @param {T} value */
has(value) { has(value) {
var source = this.#sources.get(value); var s = this.#sources.get(value);
if (source === undefined) { if (s === undefined) {
// We should always track the version in case // We should always track the version in case
// the Set ever gets this value in the future. // the Set ever gets this value in the future.
get(this.#version); get(this.#version);
@ -91,7 +87,7 @@ export class ReactiveSet extends Set {
return false; return false;
} }
return get(source); return get(s);
} }
/** @param {T} value */ /** @param {T} value */
@ -110,12 +106,12 @@ export class ReactiveSet extends Set {
/** @param {T} value */ /** @param {T} value */
delete(value) { delete(value) {
var sources = this.#sources; var sources = this.#sources;
var source = sources.get(value); var s = sources.get(value);
if (source !== undefined) { if (s !== undefined) {
sources.delete(value); sources.delete(value);
set(this.#size, sources.size); set(this.#size, sources.size);
set(source, false); set(s, false);
this.#increment_version(); this.#increment_version();
} }
@ -127,8 +123,8 @@ export class ReactiveSet extends Set {
if (sources.size !== 0) { if (sources.size !== 0) {
set(this.#size, 0); set(this.#size, 0);
for (var source of sources.values()) { for (var s of sources.values()) {
set(source, false); set(s, false);
} }
this.#increment_version(); this.#increment_version();
} }
@ -138,45 +134,20 @@ export class ReactiveSet extends Set {
} }
keys() { keys() {
return this.values();
}
values() {
get(this.#version); get(this.#version);
return this.#sources.keys();
var iterator = this.#sources.keys();
return make_iterable(
/** @type {IterableIterator<T>} */ ({
next() {
for (var value of iterator) {
return { value, done: false };
} }
return { done: true }; values() {
} return this.keys();
})
);
} }
entries() { entries() {
var iterator = this.values(); return map(this.keys(), (key) => /** @type {[T, T]} */ ([key, key]));
return make_iterable(
/** @type {IterableIterator<[T, T]>} */ ({
next() {
for (var value of iterator) {
return { value: [value, value], done: false };
}
return { done: true };
}
})
);
} }
[Symbol.iterator]() { [Symbol.iterator]() {
return this.values(); return this.keys();
} }
get size() { get size() {

@ -0,0 +1,24 @@
/**
* @template T
* @template U
* @param {Iterable<T>} iterable
* @param {(value: T) => U} fn
* @returns {IterableIterator<U>}
*/
export function map(iterable, fn) {
return {
[Symbol.iterator]: get_this,
next() {
for (const value of iterable) {
return { done: false, value: fn(value) };
}
return { done: true, value: undefined };
}
};
}
/** @this {any} */
function get_this() {
return this;
}

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version * https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string} * @type {string}
*/ */
export const VERSION = '5.0.0-next.78'; export const VERSION = '5.0.0-next.81';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

@ -14,6 +14,7 @@ export const raf = {
raf.ticks.add(f); raf.ticks.add(f);
}; };
svelte_raf.now = () => raf.time; svelte_raf.now = () => raf.time;
svelte_raf.tasks.clear();
} }
}; };
@ -31,125 +32,144 @@ function tick(time) {
} }
class Animation { class Animation {
#target;
#keyframes; #keyframes;
#duration; #duration;
#timeline_offset;
#reversed; #offset = raf.time;
#target;
#paused; #finished = () => {};
#cancelled = () => {};
currentTime = 0;
/** /**
* @param {HTMLElement} target * @param {HTMLElement} target
* @param {Keyframe[]} keyframes * @param {Keyframe[]} keyframes
* @param {{duration?: number}} options * @param {{ duration: number }} options // TODO add delay
*/ */
constructor(target, keyframes, options = {}) { constructor(target, keyframes, { duration }) {
this.#target = target; this.#target = target;
this.#keyframes = keyframes; this.#keyframes = keyframes;
this.#duration = options.duration || 0; this.#duration = duration;
this.#timeline_offset = 0;
this.#reversed = false; // Promise-like semantics, but call callbacks immediately on raf.tick
this.#paused = false; this.finished = {
this.onfinish = () => {}; /** @param {() => void} callback */
this.pending = true; then: (callback) => {
this.currentTime = 0; this.#finished = callback;
this.playState = 'running';
this.effect = { return {
setKeyframes: (/** @type {Keyframe[]} */ keyframes) => { /** @param {() => void} callback */
this.#keyframes = keyframes; catch: (callback) => {
this.#cancelled = callback;
} }
}; };
} }
};
play() {
this.#paused = false;
raf.animations.add(this);
this.playState = 'running';
this._update(); this._update();
} }
_update() { _update() {
if (this.#reversed) { this.currentTime = raf.time - this.#offset;
if (this.#timeline_offset === 0) {
this.currentTime = this.#duration - raf.time;
} else {
this.currentTime = this.#timeline_offset + (this.#timeline_offset - raf.time);
}
} else {
this.currentTime = raf.time - this.#timeline_offset;
}
const target_frame = this.currentTime / this.#duration; const target_frame = this.currentTime / this.#duration;
this._applyKeyFrame(target_frame); this.#apply_keyframe(target_frame);
if (this.currentTime >= this.#duration) {
this.#finished();
raf.animations.delete(this);
}
} }
/** /**
* @param {number} target_frame * @param {number} t
*/ */
_applyKeyFrame(target_frame) { #apply_keyframe(t) {
const keyframes = this.#keyframes; const n = Math.min(1, Math.max(0, t)) * (this.#keyframes.length - 1);
const keyframes_size = keyframes.length - 1;
const frame = keyframes[Math.min(keyframes_size, Math.floor(keyframes.length * target_frame))]; const lower = this.#keyframes[Math.floor(n)];
for (let prop in frame) { const upper = this.#keyframes[Math.ceil(n)];
// @ts-ignore
this.#target.style[prop] = frame[prop]; let frame = lower;
if (lower !== upper) {
frame = {};
for (const key in lower) {
frame[key] = interpolate(
/** @type {string} */ (lower[key]),
/** @type {string} */ (upper[key]),
n % 1
);
}
} }
if (this.#reversed) {
if (this.currentTime <= 0) {
this.finish();
for (let prop in frame) { for (let prop in frame) {
// @ts-ignore // @ts-ignore
this.#target.style[prop] = null; this.#target.style[prop] = frame[prop];
}
} }
} else {
if (this.currentTime >= this.#duration) { if (this.currentTime >= this.#duration) {
this.finish(); this.currentTime = this.#duration;
for (let prop in frame) { for (let prop in frame) {
// @ts-ignore // @ts-ignore
this.#target.style[prop] = null; this.#target.style[prop] = null;
} }
} }
} }
cancel() {
if (this.currentTime > 0 && this.currentTime < this.#duration) {
this.#apply_keyframe(0);
} }
finish() { this.#cancelled();
this.onfinish();
this.currentTime = this.#reversed ? 0 : this.#duration;
if (this.#reversed) {
raf.animations.delete(this); raf.animations.delete(this);
} }
this.playState = 'idle';
} }
cancel() { /**
this.#paused = true; * @param {string} a
if (this.currentTime > 0 && this.currentTime < this.#duration) { * @param {string} b
this._applyKeyFrame(this.#reversed ? this.#keyframes.length - 1 : 0); * @param {number} p
} */
} function interpolate(a, b, p) {
if (a === b) return a;
pause() { const fallback = p < 0.5 ? a : b;
this.#paused = true;
this.playState = 'paused'; const a_match = a.match(/[\d.]+|[^\d.]+/g);
} const b_match = b.match(/[\d.]+|[^\d.]+/g);
reverse() { if (!a_match || !b_match) return fallback;
if (this.#paused && !raf.animations.has(this)) { if (a_match.length !== b_match.length) return fallback;
raf.animations.add(this);
let result = '';
for (let i = 0; i < a_match.length; i += 2) {
const a_num = parseFloat(a_match[i]);
const b_num = parseFloat(b_match[i]);
result += a_num + (b_num - a_num) * p;
if (a_match[i + 1] !== b_match[i + 1]) {
// bail
return fallback;
} }
this.#timeline_offset = this.currentTime;
this.#reversed = !this.#reversed; result += a_match[i + 1] ?? '';
this.playState = 'running';
} }
return result;
} }
/** /**
* @param {Keyframe[]} keyframes * @param {Keyframe[]} keyframes
* @param {{duration?: number}} options * @param {{duration: number}} options
* @returns {globalThis.Animation} * @returns {globalThis.Animation}
*/ */
HTMLElement.prototype.animate = function (keyframes, options) { HTMLElement.prototype.animate = function (keyframes, options) {
const animation = new Animation(this, keyframes, options); const animation = new Animation(this, keyframes, options);
raf.animations.add(animation);
// @ts-ignore // @ts-ignore
return animation; return animation;
}; };

@ -1 +1 @@
<!--ssr:0--><div class="foo"></div><!--ssr:0--> <!--ssr:0--><div id="foo"></div><!--ssr:0-->

@ -1,5 +1,5 @@
<script> <script>
export let className; export let id;
</script> </script>
<div class={className}></div> <div id={id}></div>

@ -1,7 +1,9 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
export let logs; export let logs;
export let state; export let state;
onMount(() => { onMount(() => {
logs.push(`mount ${state}`); logs.push(`mount ${state}`);
return () => { return () => {

@ -20,7 +20,7 @@
export async function test() { export async function test() {
resolve_promise(); resolve_promise();
await Promise.resolve(); await tick();
new_promise(); new_promise();
resolve_promise(); resolve_promise();
return tick(); return tick();

@ -14,9 +14,11 @@ export default test({
<p>selected: two</p> <p>selected: two</p>
`, `,
test({ assert, component, target }) { async test({ assert, component, target }) {
component.items = ['one', 'two', 'three']; component.items = ['one', 'two', 'three'];
await Promise.resolve(); // mutation observer
const options = target.querySelectorAll('option'); const options = target.querySelectorAll('option');
assert.ok(!options[0].selected); assert.ok(!options[0].selected);
assert.ok(options[1].selected); assert.ok(options[1].selected);

@ -19,9 +19,11 @@ export default test({
<p>selected: two</p> <p>selected: two</p>
`, `,
test({ assert, component, target }) { async test({ assert, component, target }) {
component.items = ['one', 'two', 'three']; component.items = ['one', 'two', 'three'];
await Promise.resolve(); // mutation observer
const options = target.querySelectorAll('option'); const options = target.querySelectorAll('option');
assert.ok(!options[0].selected); assert.ok(!options[0].selected);
assert.ok(options[1].selected); assert.ok(options[1].selected);

@ -0,0 +1,12 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await new Promise((r) => setTimeout(r, 200)); // wait for await block to resolve
const options = target.querySelectorAll('option');
assert.ok(!options[0].selected);
assert.ok(options[1].selected);
assert.ok(!options[2].selected);
}
});

@ -0,0 +1,19 @@
<script>
let promise = getNumbers();
let selected = 2;
async function getNumbers() {
await new Promise(resolve => setTimeout(resolve, 100));
return [1, 2, 3];
}
</script>
<select bind:value={selected}>
{#await promise}
<option>-1</option>
{:then numbers}
{#each numbers as number}
<option>{number}</option>
{/each}
{/await}
</select>

@ -0,0 +1,35 @@
import { ok, test } from '../../test';
// test select binding behavior when a selected option is removed
export default test({
skip_if_ssr: 'permanent',
html: `<p>selected: a</p><select><option value="a">a</option><option value="b">b</option><option value="c">c</option></select>`,
async test({ assert, component, target }) {
const select = target.querySelector('select');
ok(select);
const options = target.querySelectorAll('option');
// first option should be selected by default since no value was bound
assert.equal(component.selected, 'a');
assert.equal(select.value, 'a');
assert.ok(options[0].selected);
// remove the selected item, so the bound value no longer matches anything
component.items = ['b', 'c'];
// There's a MutationObserver
await Promise.resolve();
// now no option should be selected
assert.equal(select.value, '');
assert.equal(select.selectedIndex, -1);
// model of selected value should be kept around, even if it is not in the list
assert.htmlEqual(
target.innerHTML,
`<p>selected: a</p><select><option value="b">b</option><option value="c">c</option></select>`
);
}
});

@ -0,0 +1,12 @@
<script>
export let selected;
export let items = ['a', 'b', 'c'];
</script>
<p>selected: {selected}</p>
<select bind:value={selected}>
{#each items as letter}
<option>{letter}</option>
{/each}
</select>

@ -18,7 +18,7 @@ export default test({
raf.tick(150); raf.tick(150);
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
'<p>foo</p><p class="red svelte-1yszte8 border" style="overflow: hidden; opacity: 0; border-top-width: 0.5000600024000317px; border-bottom-width: 0.5000600024000317px;">bar</p>' '<p>foo</p><p class="red svelte-1yszte8 border" style="overflow: hidden; opacity: 0; border-top-width: 0.5px; border-bottom-width: 0.5px;">bar</p>'
); );
component.open = true; component.open = true;
raf.tick(250); raf.tick(250);

@ -13,7 +13,7 @@
<span <span
on:click="{toggle}" on:click="{toggle}"
class="{isCurrentlySelected ? 'selected' : ''}" class="{isCurrentlySelected ? 'selected' : null}"
> >
<slot></slot> <slot></slot>
</span> </span>

@ -0,0 +1,9 @@
<script>
export let text;
</script>
<div>
{text}
<hr />
<slot name="footer" />
</div>

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
html: `
<div>hello world <hr> <div slot="footer">hello world</div></div>
`
});

@ -0,0 +1,12 @@
<script>
import Nested from "./Nested.svelte"
import Nested2 from "./Nested2.svelte"
</script>
<Nested>
<Nested2 slot="inner" let:text {text}>
<div slot="footer">
{text}
</div>
</Nested2>
</Nested>

@ -23,7 +23,6 @@ export default test({
`, `,
test({ assert, component, target, raf }) { test({ assert, component, target, raf }) {
raf.tick(0);
component.tag = 'p'; component.tag = 'p';
assert.equal(target.querySelectorAll('p').length, 5); assert.equal(target.querySelectorAll('p').length, 5);
@ -52,11 +51,11 @@ export default test({
]; ];
divs = target.querySelectorAll('div'); divs = target.querySelectorAll('div');
assert.ok(~divs[0].style.transform); assert.equal(divs[0].style.transform, 'translate(0px, 120px)');
assert.equal(divs[1].style.transform, 'translate(1px, 0px)'); assert.equal(divs[1].style.transform, '');
assert.equal(divs[2].style.transform, 'translate(1px, 0px)'); assert.equal(divs[2].style.transform, '');
assert.equal(divs[3].style.transform, 'translate(1px, 0px)'); assert.equal(divs[3].style.transform, '');
assert.ok(~divs[4].style.transform); assert.equal(divs[4].style.transform, 'translate(0px, -120px)');
raf.tick(100); raf.tick(100);
assert.deepEqual([divs[0].style.transform, divs[4].style.transform], ['', '']); assert.deepEqual([divs[0].style.transform, divs[4].style.transform], ['', '']);

@ -8,7 +8,7 @@
return { return {
duration: 100, duration: 100,
css: (t, u) => `transform: translate(${u + dx}px, ${u * dy}px)` css: (t, u) => `transform: translate(${u * dx}px, ${u * dy}px)`
}; };
} }
</script> </script>

@ -15,8 +15,9 @@ export default test({
assert.equal(h1.style.opacity, ''); assert.equal(h1.style.opacity, '');
assert.equal(h2.style.opacity, ''); assert.equal(h2.style.opacity, '');
raf.tick(50); raf.tick(200);
component.visible = false; component.visible = false;
assert.equal(h2.style.opacity, '0.49998000000000004'); raf.tick(250);
assert.equal(h2.style.opacity, '0.5');
} }
}); });

@ -0,0 +1,13 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
skip: true, // failing test for https://github.com/sveltejs/svelte/issues/10787
html: `<button>3</button>`,
async test({ assert, target }) {
target.querySelector('button')?.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
}
});

@ -0,0 +1,16 @@
<script>
let x = 1;
let y = true;
$: array = y ? [1, 2] : [1];
$: count = array.length === 2 && x ? 1 : 0;
$: sum = count + array.length;
</script>
<button
on:click={() => {
// order is important here: x must be updated before y
// in order to test that $: still runs in the correct order
x = 2;
y = false;
}}>{sum}</button
>

@ -0,0 +1,16 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await target.querySelector('button')?.click();
await Promise.resolve();
const options = target.querySelectorAll('option');
assert.equal(options[0].selected, false);
assert.equal(options[1].selected, true);
assert.equal(options[2].selected, false);
assert.equal(options[3].selected, false);
assert.equal(options[4].selected, true);
assert.equal(options[5].selected, false);
}
});

@ -0,0 +1,33 @@
<script>
let value = 'bar';
let value_bound = 'bar';
let options = {};
function loadOptions() {
options = {
foo: 'Foo',
bar: 'Bar',
baz: 'Baz',
};
}
</script>
<select {value}>
{#each Object.entries(options) as [key, value] (key)}
<option value={key}>
{value}
</option>
{/each}
</select>
<select bind:value={value_bound}>
{#each Object.entries(options) as [key, value] (key)}
<option value={key}>
{value}
</option>
{/each}
</select>
<button on:click={loadOptions}>
Load options
</button>

@ -0,0 +1,21 @@
import { test } from '../../test';
const items = [{ id: 'a' }, { id: 'b' }];
export default test({
get props() {
return { foo: [items[0]], items };
},
test({ assert, component, target }) {
const options = target.querySelectorAll('option');
assert.equal(options[0].selected, true);
assert.equal(options[1].selected, false);
component.foo = [component.items[1]];
assert.equal(options[0].selected, false);
assert.equal(options[1].selected, true);
}
});

@ -0,0 +1,9 @@
<script>
export let foo;
export let items;
</script>
<select value="{foo}" multiple>
<option value='{items[0]}'>a</option>
<option value='{items[1]}'>b</option>
</select>

@ -1,4 +1,4 @@
import { ok, test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
get props() { get props() {
@ -8,14 +8,11 @@ export default test({
test({ assert, component, target, raf }) { test({ assert, component, target, raf }) {
component.visible = false; component.visible = false;
raf.tick(150);
const outer = /** @type {HTMLSpanElement} */ (target.querySelector('.outer')); const outer = /** @type {HTMLSpanElement} */ (target.querySelector('.outer'));
const inner = /** @type {HTMLSpanElement} */ (target.querySelector('.inner')); const inner = /** @type {HTMLSpanElement} */ (target.querySelector('.inner'));
raf.tick(150); assert.deepEqual([outer.style.cssText, inner.style.cssText], ['opacity: 0.25;', '']);
assert.deepEqual(
[outer.style.cssText, inner.style.cssText],
['opacity: 0.24999000000000002;', '']
);
} }
}); });

@ -6,14 +6,12 @@ export default test({
const div = target.querySelector('div'); const div = target.querySelector('div');
ok(div); ok(div);
raf.tick(25); raf.tick(50);
assert.equal(div.style.opacity, '0.5');
assert.equal(div.style.opacity, '0.16666');
component.visible = false; component.visible = false;
raf.tick(40); raf.tick(75);
assert.equal(div.style.opacity, '0.25');
assert.ok(div.style.opacity === '0');
} }
}); });

@ -5,6 +5,7 @@ export default test({
async test({ assert, component, target, raf }) { async test({ assert, component, target, raf }) {
const frame = /** @type {HTMLIFrameElement} */ (target.querySelector('iframe')); const frame = /** @type {HTMLIFrameElement} */ (target.querySelector('iframe'));
await tick(); await tick();
await tick(); // TODO investigate why this second tick is necessary. without it, `Foo.svelte` initializes with `visible = true`, incorrectly
component.visible = true; component.visible = true;
const div = frame.contentDocument?.querySelector('div'); const div = frame.contentDocument?.querySelector('div');
@ -14,8 +15,10 @@ export default test({
component.visible = false; component.visible = false;
raf.tick(26); raf.tick(25);
// The exact number doesn't matter here, this test is about ensuring that transitions work in iframes assert.equal(div.style.opacity, '0.25');
assert.equal(Number(div.style.opacity).toFixed(4), '0.8333');
raf.tick(35);
assert.equal(div.style.opacity, '0.18333333333333335');
} }
}); });

@ -6,18 +6,27 @@ export default test({
const div = target.querySelector('div'); const div = target.querySelector('div');
ok(div); ok(div);
assert.equal(div.style.opacity, '0'); assert.equal(div.style.scale, '0');
raf.tick(50); raf.tick(50);
component.visible = false; component.visible = false;
// both in and out styles // both in and out styles
assert.equal(div.style.opacity, '0.49998000000000004'); assert.equal(div.style.scale, '0.5');
assert.equal(div.style.opacity, '1');
assert.equal(div.style.rotate, '360deg');
raf.tick(75); raf.tick(75);
assert.equal(div.style.scale, '0.75'); // intro continues while outro plays
assert.equal(div.style.opacity, '0.75');
assert.equal(div.style.rotate, '270deg');
component.visible = true; component.visible = true;
// reset original styles // reset original styles
assert.equal(div.style.scale, '0');
assert.equal(div.style.opacity, '1'); assert.equal(div.style.opacity, '1');
assert.equal(div.style.rotate, '360deg');
} }
}); });

@ -5,7 +5,7 @@
return { return {
duration: 100, duration: 100,
css: t => { css: t => {
return `opacity: ${t}`; return `scale: ${t}`;
} }
}; };
} }
@ -14,7 +14,7 @@
return { return {
duration: 100, duration: 100,
css: t => { css: t => {
return `opacity: ${t}`; return `rotate: ${t * 360}deg; opacity: ${t}`;
} }
}; };
} }

@ -6,29 +6,26 @@ export default test({
const b = target.querySelector('button.b'); const b = target.querySelector('button.b');
ok(a); ok(a);
ok(b); ok(b);
// jsdom doesn't set the inert attribute, and the transition checks if it exists, so set it manually to trigger the inert logic
a.inert = false;
b.inert = false;
// check and abort halfway through the outro transition // check and abort halfway through the outro transition
component.visible = false; component.visible = false;
raf.tick(50); raf.tick(50);
assert.strictEqual(target.querySelector('button.a')?.inert, true); assert.ok(target.querySelector('button.a')?.inert);
assert.strictEqual(target.querySelector('button.b')?.inert, true); assert.ok(target.querySelector('button.b')?.inert);
component.visible = true; component.visible = true;
assert.strictEqual(target.querySelector('button.a')?.inert, false); assert.ok(!target.querySelector('button.a')?.inert);
assert.strictEqual(target.querySelector('button.b')?.inert, false); assert.ok(!target.querySelector('button.b')?.inert);
// let it transition out completely and then back in // let it transition out completely and then back in
component.visible = false; component.visible = false;
raf.tick(101); raf.tick(101);
component.visible = true; component.visible = true;
raf.tick(150); raf.tick(150);
assert.strictEqual(target.querySelector('button.a')?.inert, false); assert.ok(!target.querySelector('button.a')?.inert);
assert.strictEqual(target.querySelector('button.b')?.inert, false); assert.ok(!target.querySelector('button.b')?.inert);
raf.tick(151); raf.tick(151);
assert.strictEqual(target.querySelector('button.a')?.inert, false); assert.ok(!target.querySelector('button.a')?.inert);
assert.strictEqual(target.querySelector('button.b')?.inert, false); assert.ok(!target.querySelector('button.b')?.inert);
} }
}); });

@ -23,8 +23,6 @@ export default test({
assert.equal(spans[1].foo, 0.25); assert.equal(spans[1].foo, 0.25);
assert.equal(spans[2].foo, 0.75); assert.equal(spans[2].foo, 0.75);
raf.tick(7);
component.things = things; component.things = things;
raf.tick(225); raf.tick(225);
@ -42,8 +40,8 @@ export default test({
target.querySelectorAll('span') target.querySelectorAll('span')
); );
assert.equal(spans[0].foo, undefined); assert.equal(spans[0].foo, 1);
assert.equal(spans[1].foo, undefined); assert.equal(spans[1].foo, 1);
assert.equal(spans[2].foo, undefined); assert.equal(spans[2].foo, 1);
} }
}); });

@ -14,8 +14,6 @@ export default test({
intro: true, intro: true,
async test({ assert, target, component, raf }) { async test({ assert, target, component, raf }) {
raf.tick(0);
assert.htmlEqual(target.innerHTML, '<p class="pending" foo="0.0">loading...</p>'); assert.htmlEqual(target.innerHTML, '<p class="pending" foo="0.0">loading...</p>');
let time = 0; let time = 0;
@ -24,10 +22,6 @@ export default test({
assert.htmlEqual(target.innerHTML, '<p class="pending" foo="0.5">loading...</p>'); assert.htmlEqual(target.innerHTML, '<p class="pending" foo="0.5">loading...</p>');
await fulfil(42); await fulfil(42);
await Promise.resolve();
await Promise.resolve();
raf.tick(time);
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
@ -61,9 +55,6 @@ export default test({
fulfil = f; fulfil = f;
}); });
await Promise.resolve(); await Promise.resolve();
await Promise.resolve();
raf.tick(time);
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
@ -83,11 +74,6 @@ export default test({
); );
await fulfil(43); await fulfil(43);
await Promise.resolve();
await Promise.resolve();
raf.tick(time);
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
` `
@ -109,9 +95,6 @@ export default test({
fulfil = f; fulfil = f;
}); });
await Promise.resolve(); await Promise.resolve();
await Promise.resolve();
raf.tick(time);
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
@ -132,11 +115,6 @@ export default test({
); );
await fulfil(44); await fulfil(44);
await Promise.resolve();
await Promise.resolve();
raf.tick(time);
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
` `
@ -159,10 +137,6 @@ export default test({
fulfil = f; fulfil = f;
}); });
await Promise.resolve(); await Promise.resolve();
await Promise.resolve();
raf.tick(time);
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
` `
@ -172,7 +146,6 @@ export default test({
); );
raf.tick((time += 40)); raf.tick((time += 40));
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
` `
@ -182,10 +155,6 @@ export default test({
); );
await fulfil(45); await fulfil(45);
await Promise.resolve();
await Promise.resolve();
raf.tick(time);
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
@ -197,7 +166,6 @@ export default test({
); );
raf.tick((time += 20)); raf.tick((time += 20));
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
` `
@ -211,9 +179,6 @@ export default test({
fulfil = f; fulfil = f;
}); });
await Promise.resolve(); await Promise.resolve();
await Promise.resolve();
raf.tick(time);
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
@ -238,10 +203,6 @@ export default test({
); );
await fulfil(46); await fulfil(46);
await Promise.resolve();
await Promise.resolve();
raf.tick(time);
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
@ -255,7 +216,6 @@ export default test({
); );
raf.tick((time += 10)); raf.tick((time += 10));
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
` `
@ -265,7 +225,6 @@ export default test({
); );
raf.tick((time += 20)); raf.tick((time += 20));
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
` `
@ -274,7 +233,6 @@ export default test({
); );
raf.tick((time += 70)); raf.tick((time += 70));
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
` `

@ -1,4 +1,4 @@
import { ok, test } from '../../test'; import { test } from '../../test';
/** @type {(value: any) => void} */ /** @type {(value: any) => void} */
let fulfil; let fulfil;
@ -36,8 +36,8 @@ export default test({
); );
assert.equal(ps[0].className, 'pending'); assert.equal(ps[0].className, 'pending');
assert.equal(ps[1].className, 'then'); assert.equal(ps[1].className, 'then');
assert.equal(ps[0].foo, 0.8); assert.equal(ps[0].foo, 0.2);
assert.equal(ps[1].foo, undefined); assert.equal(ps[1].foo, 0.3);
raf.tick(100); raf.tick(100);
}); });
} }

@ -35,9 +35,9 @@ export default test({
component.visible = true; component.visible = true;
raf.tick(100); raf.tick(100);
assert.equal(divs[0].foo, 1); assert.equal(divs[0].foo, 0.3);
assert.equal(divs[1].foo, 1); assert.equal(divs[1].foo, 0.3);
assert.equal(divs[2].foo, 1); assert.equal(divs[2].foo, 0.3);
assert.equal(divs[0].bar, 1); assert.equal(divs[0].bar, 1);
assert.equal(divs[1].bar, 1); assert.equal(divs[1].bar, 1);

@ -15,7 +15,7 @@ export default test({
raf.tick(500); raf.tick(500);
component.x = true; component.x = true;
assert.equal(component.no, null); assert.equal(component.no, null);
assert.equal(component.yes.foo, undefined); assert.equal(component.yes.foo, 0);
raf.tick(700); raf.tick(700);
assert.equal(component.yes.foo, 0.5); assert.equal(component.yes.foo, 0.5);

@ -0,0 +1,30 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
let [btn1, btn2] = target.querySelectorAll('button');
flushSync(() => {
btn1.click();
});
flushSync(() => {
btn2.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>Activate</button><button>Toggle</button>\nneighba\nneighba`
);
flushSync(() => {
btn2.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>Activate</button><button>Toggle</button>\nzeeba\nzeeba`
);
}
});

@ -0,0 +1,49 @@
<script>
import { untrack } from 'svelte';
class Model {
data = $state();
constructor(data) {
this.data = data;
}
name = $derived(this.data?.name);
source = $derived(this.data?.source);
toggle() {
this.data.name = this.data.name === 'zeeba' ? 'neighba' : 'zeeba';
}
}
let model = $state(new Model({ name: 'zeeba', source: 'initial' }));
let setModel = (source) => {
let next = new Model({ name: 'zeeba', source });
model = next;
}
let needsSet = $state(false);
$effect(() => {
if(needsSet) {
setModel('effect');
untrack(() => {
needsSet = false;
});
}
});
let setWithEffect = () => {
needsSet = true;
};
let toggle = () => {
model.toggle();
}
</script>
<button onclick={setWithEffect}>Activate</button>
<button onclick={toggle}>Toggle</button>
{model.name}
{model.data.name}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save