Skip to content

Commit 6f8a451

Browse files
committed
Merge branch 'main' into props-bindable
2 parents 6f274ac + 86c57f9 commit 6f8a451

File tree

26 files changed

+466
-139
lines changed

26 files changed

+466
-139
lines changed

.changeset/beige-cobras-smoke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
feat: add support for webkitdirectory DOM boolean attribute

.changeset/mighty-cooks-scream.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: adjust scope parent for named slots

.changeset/olive-mice-fix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: improve handling of unowned derived signals

.changeset/smart-turkeys-tell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: ensure select value is updated upon select option removal

packages/svelte/elements.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,7 @@ export interface HTMLInputAttributes extends HTMLAttributes<HTMLInputElement> {
10561056
type?: HTMLInputTypeAttribute | undefined | null;
10571057
value?: any;
10581058
width?: number | string | undefined | null;
1059+
webkitdirectory?: boolean | undefined | null;
10591060

10601061
'on:change'?: ChangeEventHandler<HTMLInputElement> | undefined | null;
10611062
onchange?: ChangeEventHandler<HTMLInputElement> | undefined | null;

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 103 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -158,26 +158,6 @@ function serialize_class_directives(class_directives, element_id, context, is_at
158158
}
159159
}
160160

161-
/**
162-
*
163-
* @param {string | null} spread_id
164-
* @param {import('#compiler').RegularElement} node
165-
* @param {import('../types.js').ComponentContext} context
166-
* @param {import('estree').Identifier} node_id
167-
*/
168-
function add_select_to_spread_update(spread_id, node, context, node_id) {
169-
if (spread_id !== null && node.name === 'select') {
170-
context.state.update.push({
171-
grouped: b.if(
172-
b.binary('in', b.literal('value'), b.id(spread_id)),
173-
b.block([
174-
b.stmt(b.call('$.select_option', node_id, b.member(b.id(spread_id), b.id('value'))))
175-
])
176-
)
177-
});
178-
}
179-
}
180-
181161
/**
182162
* @param {import('#compiler').Binding[]} references
183163
* @param {import('../types.js').ComponentContext} context
@@ -223,6 +203,8 @@ function collect_transitive_dependencies(binding, seen = new Set()) {
223203
* @param {import('../types.js').ComponentContext} context
224204
*/
225205
function setup_select_synchronization(value_binding, context) {
206+
if (context.state.analysis.runes) return;
207+
226208
let bound = value_binding.expression;
227209
while (bound.type === 'MemberExpression') {
228210
bound = /** @type {import('estree').Identifier | import('estree').MemberExpression} */ (
@@ -243,59 +225,49 @@ function setup_select_synchronization(value_binding, context) {
243225
}
244226
}
245227

246-
if (!context.state.analysis.runes) {
247-
const invalidator = b.call(
248-
'$.invalidate_inner_signals',
249-
b.thunk(
250-
b.block(
251-
names.map((name) => {
252-
const serialized = serialize_get_binding(b.id(name), context.state);
253-
return b.stmt(serialized);
254-
})
255-
)
228+
const invalidator = b.call(
229+
'$.invalidate_inner_signals',
230+
b.thunk(
231+
b.block(
232+
names.map((name) => {
233+
const serialized = serialize_get_binding(b.id(name), context.state);
234+
return b.stmt(serialized);
235+
})
256236
)
257-
);
237+
)
238+
);
258239

259-
context.state.init.push(
260-
b.stmt(
261-
b.call(
262-
'$.invalidate_effect',
263-
b.thunk(
264-
b.block([
265-
b.stmt(
266-
/** @type {import('estree').Expression} */ (context.visit(value_binding.expression))
267-
),
268-
b.stmt(invalidator)
269-
])
270-
)
240+
context.state.init.push(
241+
b.stmt(
242+
b.call(
243+
'$.invalidate_effect',
244+
b.thunk(
245+
b.block([
246+
b.stmt(
247+
/** @type {import('estree').Expression} */ (context.visit(value_binding.expression))
248+
),
249+
b.stmt(invalidator)
250+
])
271251
)
272252
)
273-
);
274-
}
253+
)
254+
);
275255
}
276256

277257
/**
278-
* Serializes element attribute assignments that contain spreads to either only
279-
* the init or the the init and update arrays, depending on whether or not the value is dynamic.
280-
* Resulting code for static looks something like this:
281-
* ```js
282-
* $.spread_attributes(element, null, [...]);
283-
* ```
284-
* Resulting code for dynamic looks something like this:
285-
* ```js
286-
* let value;
287-
* $.render_effect(() => {
288-
* value = $.spread_attributes(element, value, [...])
289-
* });
290-
* ```
291-
* Returns the id of the spread_attribute variable if spread isn't isolated, `null` otherwise.
292258
* @param {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} attributes
293259
* @param {import('../types.js').ComponentContext} context
294260
* @param {import('#compiler').RegularElement} element
295261
* @param {import('estree').Identifier} element_id
296-
* @returns {string | null}
262+
* @param {boolean} needs_select_handling
297263
*/
298-
function serialize_element_spread_attributes(attributes, context, element, element_id) {
264+
function serialize_element_spread_attributes(
265+
attributes,
266+
context,
267+
element,
268+
element_id,
269+
needs_select_handling
270+
) {
299271
let needs_isolation = false;
300272

301273
/** @type {import('estree').Expression[]} */
@@ -317,8 +289,9 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
317289

318290
const lowercase_attributes =
319291
element.metadata.svg || is_custom_element_node(element) ? b.false : b.true;
292+
const id = context.state.scope.generate('spread_attributes');
320293

321-
const isolated = b.stmt(
294+
const standalone = b.stmt(
322295
b.call(
323296
'$.spread_attributes_effect',
324297
element_id,
@@ -327,32 +300,57 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
327300
b.literal(context.state.analysis.css.hash)
328301
)
329302
);
303+
const inside_effect = b.stmt(
304+
b.assignment(
305+
'=',
306+
b.id(id),
307+
b.call(
308+
'$.spread_attributes',
309+
element_id,
310+
b.id(id),
311+
b.array(values),
312+
lowercase_attributes,
313+
b.literal(context.state.analysis.css.hash)
314+
)
315+
)
316+
);
317+
318+
if (!needs_isolation || needs_select_handling) {
319+
context.state.init.push(b.let(id));
320+
}
330321

331322
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
332323
if (needs_isolation) {
333-
context.state.update_effects.push(isolated);
334-
return null;
324+
if (needs_select_handling) {
325+
context.state.update_effects.push(
326+
b.stmt(b.call('$.render_effect', b.arrow([], b.block([inside_effect]))))
327+
);
328+
} else {
329+
context.state.update_effects.push(standalone);
330+
}
335331
} else {
336-
const id = context.state.scope.generate('spread_attributes');
337-
context.state.init.push(b.let(id));
338332
context.state.update.push({
339-
singular: isolated,
340-
grouped: b.stmt(
341-
b.assignment(
342-
'=',
343-
b.id(id),
344-
b.call(
345-
'$.spread_attributes',
346-
element_id,
347-
b.id(id),
348-
b.array(values),
349-
lowercase_attributes,
350-
b.literal(context.state.analysis.css.hash)
351-
)
352-
)
333+
singular: needs_select_handling ? undefined : standalone,
334+
grouped: inside_effect
335+
});
336+
}
337+
338+
if (needs_select_handling) {
339+
context.state.init.push(
340+
b.stmt(b.call('$.init_select', element_id, b.thunk(b.member(b.id(id), b.id('value')))))
341+
);
342+
context.state.update.push({
343+
grouped: b.if(
344+
b.binary('in', b.literal('value'), b.id(id)),
345+
b.block([
346+
// This ensures a one-way street to the DOM in case it's <select {value}>
347+
// and not <select bind:value>. We need it in addition to $.init_select
348+
// because the select value is not reflected as an attribute, so the
349+
// mutation observer wouldn't notice.
350+
b.stmt(b.call('$.select_option', element_id, b.member(b.id(id), b.id('value'))))
351+
])
353352
)
354353
});
355-
return id;
356354
}
357355
}
358356

@@ -644,27 +642,27 @@ function serialize_element_special_value_attribute(element, node_id, attribute,
644642
)
645643
);
646644
const is_reactive = attribute.metadata.dynamic;
647-
const needs_selected_call =
648-
element === 'option' && (is_reactive || collect_parent_each_blocks(context).length > 0);
649-
const needs_option_call = element === 'select' && is_reactive;
645+
const is_select_with_value =
646+
// attribute.metadata.dynamic would give false negatives because even if the value does not change,
647+
// the inner options could still change, so we need to always treat it as reactive
648+
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
650649
const assignment = b.stmt(
651-
needs_selected_call
650+
is_select_with_value
652651
? b.sequence([
653652
inner_assignment,
654-
// This ensures things stay in sync with the select binding
655-
// in case of updates to the option value or new values appearing
656-
b.call('$.selected', node_id)
653+
// This ensures a one-way street to the DOM in case it's <select {value}>
654+
// and not <select bind:value>. We need it in addition to $.init_select
655+
// because the select value is not reflected as an attribute, so the
656+
// mutation observer wouldn't notice.
657+
b.call('$.select_option', node_id, value)
657658
])
658-
: needs_option_call
659-
? b.sequence([
660-
inner_assignment,
661-
// This ensures a one-way street to the DOM in case it's <select {value}>
662-
// and not <select bind:value>
663-
b.call('$.select_option', node_id, value)
664-
])
665-
: inner_assignment
659+
: inner_assignment
666660
);
667661

662+
if (is_select_with_value) {
663+
state.init.push(b.stmt(b.call('$.init_select', node_id, b.thunk(value))));
664+
}
665+
668666
if (is_reactive) {
669667
const id = state.scope.generate(`${node_id.name}_value`);
670668
serialize_update_assignment(
@@ -2083,11 +2081,15 @@ export const template_visitors = {
20832081
// Then do attributes
20842082
let is_attributes_reactive = false;
20852083
if (node.metadata.has_spread) {
2086-
const spread_id = serialize_element_spread_attributes(attributes, context, node, node_id);
2087-
if (child_metadata.namespace !== 'foreign') {
2088-
add_select_to_spread_update(spread_id, node, context, node_id);
2089-
}
2090-
is_attributes_reactive = spread_id !== null;
2084+
serialize_element_spread_attributes(
2085+
attributes,
2086+
context,
2087+
node,
2088+
node_id,
2089+
// If value binding exists, that one takes care of calling $.init_select
2090+
value_binding === null && node.name === 'select' && child_metadata.namespace !== 'foreign'
2091+
);
2092+
is_attributes_reactive = true;
20912093
} else {
20922094
for (const attribute of /** @type {import('#compiler').Attribute[]} */ (attributes)) {
20932095
if (is_event_attribute(attribute)) {

packages/svelte/src/compiler/phases/scope.js

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -400,9 +400,9 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
400400
(attribute) => attribute.type === 'Attribute' && attribute.name === 'slot'
401401
)
402402
) {
403-
// <div slot="..."> inherits the scope above the component, because slots are hella weird
404-
scopes.set(child, state.scope);
405-
visit(child);
403+
// <div slot="..."> inherits the scope above the component unless the component is a named slot itself, because slots are hella weird
404+
scopes.set(child, is_default_slot ? state.scope : scope);
405+
visit(child, { scope: is_default_slot ? state.scope : scope });
406406
} else {
407407
if (child.type === 'ExpressionTag') {
408408
// 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) {
599599
},
600600

601601
AwaitBlock(node, context) {
602-
context.next();
602+
context.visit(node.expression);
603+
604+
if (node.pending) {
605+
context.visit(node.pending);
606+
}
603607

604-
if (node.then && node.value !== null) {
605-
const then_scope = /** @type {Scope} */ (scopes.get(node.then));
606-
const value_scope = context.state.scope.child();
607-
for (const id of extract_identifiers(node.value)) {
608-
then_scope.declare(id, 'normal', 'const');
609-
value_scope.declare(id, 'normal', 'const');
608+
if (node.then) {
609+
context.visit(node.then);
610+
if (node.value) {
611+
const then_scope = /** @type {Scope} */ (scopes.get(node.then));
612+
const value_scope = context.state.scope.child();
613+
scopes.set(node.value, value_scope);
614+
context.visit(node.value, { scope: value_scope });
615+
for (const id of extract_identifiers(node.value)) {
616+
then_scope.declare(id, 'normal', 'const');
617+
value_scope.declare(id, 'normal', 'const');
618+
}
610619
}
611-
scopes.set(node.value, value_scope);
612620
}
613621

614-
if (node.catch && node.error !== null) {
615-
const catch_scope = /** @type {Scope} */ (scopes.get(node.catch));
616-
const error_scope = context.state.scope.child();
617-
for (const id of extract_identifiers(node.error)) {
618-
catch_scope.declare(id, 'normal', 'const');
619-
error_scope.declare(id, 'normal', 'const');
622+
if (node.catch) {
623+
context.visit(node.catch);
624+
if (node.error) {
625+
const catch_scope = /** @type {Scope} */ (scopes.get(node.catch));
626+
const error_scope = context.state.scope.child();
627+
scopes.set(node.error, error_scope);
628+
context.visit(node.error, { scope: error_scope });
629+
for (const id of extract_identifiers(node.error)) {
630+
catch_scope.declare(id, 'normal', 'const');
631+
error_scope.declare(id, 'normal', 'const');
632+
}
620633
}
621-
scopes.set(node.error, error_scope);
622634
}
623635
},
624636

packages/svelte/src/constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ export const DOMBooleanAttributes = [
8383
'required',
8484
'reversed',
8585
'seamless',
86-
'selected'
86+
'selected',
87+
'webkitdirectory'
8788
];
8889

8990
export const namespace_svg = 'http://www.w3.org/2000/svg';

0 commit comments

Comments
 (0)