@@ -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 */
225205function 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 (
@@ -2082,11 +2080,15 @@ export const template_visitors = {
20822080 // Then do attributes
20832081 let is_attributes_reactive = false ;
20842082 if ( node . metadata . has_spread ) {
2085- const spread_id = serialize_element_spread_attributes ( attributes , context , node , node_id ) ;
2086- if ( child_metadata . namespace !== 'foreign' ) {
2087- add_select_to_spread_update ( spread_id , node , context , node_id ) ;
2088- }
2089- is_attributes_reactive = spread_id !== null ;
2083+ serialize_element_spread_attributes (
2084+ attributes ,
2085+ context ,
2086+ node ,
2087+ node_id ,
2088+ // If value binding exists, that one takes care of calling $.init_select
2089+ value_binding === null && node . name === 'select' && child_metadata . namespace !== 'foreign'
2090+ ) ;
2091+ is_attributes_reactive = true ;
20902092 } else {
20912093 for ( const attribute of /** @type {import('#compiler').Attribute[] } */ ( attributes ) ) {
20922094 if ( is_event_attribute ( attribute ) ) {
0 commit comments