Skip to content

Commit bc2d30c

Browse files
authored
chore: refactor set_attributes code generation (#13353)
* chore: refactor `set_attributes` code generation * simplify * simplify
1 parent 13cb385 commit bc2d30c

File tree

6 files changed

+163
-233
lines changed

6 files changed

+163
-233
lines changed

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

+45-115
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import {
2424
get_attribute_name,
2525
build_attribute_value,
2626
build_class_directives,
27-
build_style_directives
27+
build_style_directives,
28+
build_set_attributes
2829
} from './shared/element.js';
2930
import { process_children } from './shared/fragment.js';
3031
import {
@@ -95,7 +96,7 @@ export function RegularElement(node, context) {
9596
/** @type {Map<string, AST.BindDirective>} */
9697
const bindings = new Map();
9798

98-
let has_spread = false;
99+
let has_spread = node.metadata.has_spread;
99100
let has_use = false;
100101

101102
for (const attribute of node.attributes) {
@@ -105,6 +106,16 @@ export function RegularElement(node, context) {
105106
break;
106107

107108
case 'Attribute':
109+
// `is` attributes need to be part of the template, otherwise they break
110+
if (attribute.name === 'is' && context.state.metadata.namespace === 'html') {
111+
const { value } = build_attribute_value(attribute.value, context);
112+
113+
if (value.type === 'Literal' && typeof value.value === 'string') {
114+
context.state.template.push(` is="${escape_html(value.value, true)}"`);
115+
continue;
116+
}
117+
}
118+
108119
attributes.push(attribute);
109120
lookup.set(attribute.name, attribute);
110121
break;
@@ -129,7 +140,6 @@ export function RegularElement(node, context) {
129140

130141
case 'SpreadAttribute':
131142
attributes.push(attribute);
132-
has_spread = true;
133143
break;
134144

135145
case 'StyleDirective':
@@ -194,17 +204,40 @@ export function RegularElement(node, context) {
194204
const node_id = context.state.node;
195205

196206
// Then do attributes
197-
let is_attributes_reactive = false;
198-
if (node.metadata.has_spread) {
199-
build_element_spread_attributes(
207+
let is_attributes_reactive = has_spread;
208+
209+
if (has_spread) {
210+
const attributes_id = b.id(context.state.scope.generate('attributes'));
211+
212+
build_set_attributes(
200213
attributes,
201214
context,
202215
node,
203216
node_id,
204-
// If value binding exists, that one takes care of calling $.init_select
205-
node.name === 'select' && !bindings.has('value')
217+
attributes_id,
218+
(node.metadata.svg || node.metadata.mathml || is_custom_element_node(node)) && b.true,
219+
node.name.includes('-') && b.true
206220
);
207-
is_attributes_reactive = true;
221+
222+
// If value binding exists, that one takes care of calling $.init_select
223+
if (node.name === 'select' && !bindings.has('value')) {
224+
context.state.init.push(
225+
b.stmt(b.call('$.init_select', node_id, b.thunk(b.member(attributes_id, 'value'))))
226+
);
227+
228+
context.state.update.push(
229+
b.if(
230+
b.binary('in', b.literal('value'), attributes_id),
231+
b.block([
232+
// This ensures a one-way street to the DOM in case it's <select {value}>
233+
// and not <select bind:value>. We need it in addition to $.init_select
234+
// because the select value is not reflected as an attribute, so the
235+
// mutation observer wouldn't notice.
236+
b.stmt(b.call('$.select_option', node_id, b.member(attributes_id, 'value')))
237+
])
238+
)
239+
);
240+
}
208241
} else {
209242
/** If true, needs `__value` for inputs */
210243
const needs_special_value_handling =
@@ -229,7 +262,7 @@ export function RegularElement(node, context) {
229262
attribute.name !== 'autofocus' &&
230263
(attribute.value === true || is_text_attribute(attribute))
231264
) {
232-
const name = get_attribute_name(node, attribute, context);
265+
const name = get_attribute_name(node, attribute);
233266
const value = is_text_attribute(attribute) ? attribute.value[0].data : true;
234267

235268
if (name !== 'class' || value) {
@@ -258,7 +291,7 @@ export function RegularElement(node, context) {
258291
node_id,
259292
context,
260293
is_attributes_reactive,
261-
lookup.has('style') || node.metadata.has_spread
294+
lookup.has('style') || has_spread
262295
);
263296

264297
// Apply the src and loading attributes for <img> elements after the element is appended to the document
@@ -448,109 +481,6 @@ function setup_select_synchronization(value_binding, context) {
448481
);
449482
}
450483

451-
/**
452-
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
453-
* @param {ComponentContext} context
454-
* @param {AST.RegularElement} element
455-
* @param {Identifier} element_id
456-
* @param {boolean} needs_select_handling
457-
*/
458-
function build_element_spread_attributes(
459-
attributes,
460-
context,
461-
element,
462-
element_id,
463-
needs_select_handling
464-
) {
465-
let needs_isolation = false;
466-
467-
/** @type {ObjectExpression['properties']} */
468-
const values = [];
469-
470-
for (const attribute of attributes) {
471-
if (attribute.type === 'Attribute') {
472-
const name = get_attribute_name(element, attribute, context);
473-
// TODO: handle has_call
474-
const { value } = build_attribute_value(attribute.value, context);
475-
476-
if (
477-
name === 'is' &&
478-
value.type === 'Literal' &&
479-
context.state.metadata.namespace === 'html'
480-
) {
481-
context.state.template.push(` is="${escape_html(value.value, true)}"`);
482-
continue;
483-
}
484-
485-
if (
486-
is_event_attribute(attribute) &&
487-
(get_attribute_expression(attribute).type === 'ArrowFunctionExpression' ||
488-
get_attribute_expression(attribute).type === 'FunctionExpression')
489-
) {
490-
// Give the event handler a stable ID so it isn't removed and readded on every update
491-
const id = context.state.scope.generate('event_handler');
492-
context.state.init.push(b.var(id, value));
493-
values.push(b.init(attribute.name, b.id(id)));
494-
} else {
495-
values.push(b.init(name, value));
496-
}
497-
} else {
498-
values.push(b.spread(/** @type {Expression} */ (context.visit(attribute))));
499-
}
500-
501-
needs_isolation ||=
502-
attribute.type === 'SpreadAttribute' && attribute.metadata.expression.has_call;
503-
}
504-
505-
const preserve_attribute_case =
506-
element.metadata.svg || element.metadata.mathml || is_custom_element_node(element);
507-
const id = b.id(context.state.scope.generate('attributes'));
508-
509-
const update = b.stmt(
510-
b.assignment(
511-
'=',
512-
id,
513-
b.call(
514-
'$.set_attributes',
515-
element_id,
516-
id,
517-
b.object(values),
518-
context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash),
519-
preserve_attribute_case && b.true,
520-
is_ignored(element, 'hydration_attribute_changed') && b.true,
521-
element.name.includes('-') && b.true
522-
)
523-
)
524-
);
525-
526-
context.state.init.push(b.let(id));
527-
528-
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
529-
if (needs_isolation) {
530-
context.state.init.push(build_update(update));
531-
} else {
532-
context.state.update.push(update);
533-
}
534-
535-
if (needs_select_handling) {
536-
context.state.init.push(
537-
b.stmt(b.call('$.init_select', element_id, b.thunk(b.member(id, 'value'))))
538-
);
539-
context.state.update.push(
540-
b.if(
541-
b.binary('in', b.literal('value'), id),
542-
b.block([
543-
// This ensures a one-way street to the DOM in case it's <select {value}>
544-
// and not <select bind:value>. We need it in addition to $.init_select
545-
// because the select value is not reflected as an attribute, so the
546-
// mutation observer wouldn't notice.
547-
b.stmt(b.call('$.select_option', element_id, b.member(id, 'value')))
548-
])
549-
)
550-
);
551-
}
552-
}
553-
554484
/**
555485
* Serializes an assignment to an element property by adding relevant statements to either only
556486
* the init or the the init and update arrays, depending on whether or not the value is dynamic.
@@ -581,7 +511,7 @@ function build_element_spread_attributes(
581511
*/
582512
function build_element_attribute_update_assignment(element, node_id, attribute, context) {
583513
const state = context.state;
584-
const name = get_attribute_name(element, attribute, context);
514+
const name = get_attribute_name(element, attribute);
585515
const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
586516
const is_mathml = context.state.metadata.namespace === 'mathml';
587517
let { has_call, value } = build_attribute_value(attribute.value, context);

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

+24-106
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { determine_namespace_for_children } from '../../utils.js';
1212
import {
1313
build_attribute_value,
1414
build_class_directives,
15+
build_set_attributes,
1516
build_style_directives
1617
} from './shared/element.js';
1718
import { build_render_statement, build_update } from './shared/utils.js';
@@ -81,10 +82,29 @@ export function SvelteElement(node, context) {
8182
context.state.init.push(...lets); // create computeds in the outer context; the dynamic element is the single child of this slot
8283

8384
// Then do attributes
84-
// Always use spread because we don't know whether the element is a custom element or not,
85-
// therefore we need to do the "how to set an attribute" logic at runtime.
86-
const is_attributes_reactive =
87-
build_dynamic_element_attributes(node, attributes, inner_context, element_id) !== null;
85+
let is_attributes_reactive = false;
86+
87+
if (attributes.length === 0) {
88+
if (context.state.analysis.css.hash) {
89+
inner_context.state.init.push(
90+
b.stmt(b.call('$.set_class', element_id, b.literal(context.state.analysis.css.hash)))
91+
);
92+
}
93+
} else {
94+
const attributes_id = b.id(context.state.scope.generate('attributes'));
95+
96+
// Always use spread because we don't know whether the element is a custom element or not,
97+
// therefore we need to do the "how to set an attribute" logic at runtime.
98+
is_attributes_reactive = build_set_attributes(
99+
attributes,
100+
inner_context,
101+
node,
102+
element_id,
103+
attributes_id,
104+
b.binary('!==', b.member(element_id, 'namespaceURI'), b.id('$.NAMESPACE_SVG')),
105+
b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-'))
106+
);
107+
}
88108

89109
// class/style directives must be applied last since they could override class/style attributes
90110
build_class_directives(class_directives, element_id, inner_context, is_attributes_reactive);
@@ -133,105 +153,3 @@ export function SvelteElement(node, context) {
133153
)
134154
);
135155
}
136-
137-
/**
138-
* Serializes dynamic element attribute assignments.
139-
* Returns the `true` if spread is deemed reactive.
140-
* @param {AST.SvelteElement} element
141-
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
142-
* @param {ComponentContext} context
143-
* @param {Identifier} element_id
144-
* @returns {boolean}
145-
*/
146-
function build_dynamic_element_attributes(element, attributes, context, element_id) {
147-
if (attributes.length === 0) {
148-
if (context.state.analysis.css.hash) {
149-
context.state.init.push(
150-
b.stmt(b.call('$.set_class', element_id, b.literal(context.state.analysis.css.hash)))
151-
);
152-
}
153-
return false;
154-
}
155-
156-
// TODO why are we always treating this as a spread? needs docs, if that's not an error
157-
158-
let needs_isolation = false;
159-
let is_reactive = false;
160-
161-
/** @type {ObjectExpression['properties']} */
162-
const values = [];
163-
164-
for (const attribute of attributes) {
165-
if (attribute.type === 'Attribute') {
166-
const { value } = build_attribute_value(attribute.value, context);
167-
168-
if (
169-
is_event_attribute(attribute) &&
170-
(get_attribute_expression(attribute).type === 'ArrowFunctionExpression' ||
171-
get_attribute_expression(attribute).type === 'FunctionExpression')
172-
) {
173-
// Give the event handler a stable ID so it isn't removed and readded on every update
174-
const id = context.state.scope.generate('event_handler');
175-
context.state.init.push(b.var(id, value));
176-
values.push(b.init(attribute.name, b.id(id)));
177-
} else {
178-
values.push(b.init(attribute.name, value));
179-
}
180-
} else {
181-
values.push(b.spread(/** @type {Expression} */ (context.visit(attribute))));
182-
}
183-
184-
is_reactive ||=
185-
attribute.metadata.expression.has_state ||
186-
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
187-
attribute.type === 'SpreadAttribute';
188-
needs_isolation ||=
189-
attribute.type === 'SpreadAttribute' && attribute.metadata.expression.has_call;
190-
}
191-
192-
if (needs_isolation || is_reactive) {
193-
const id = context.state.scope.generate('attributes');
194-
context.state.init.push(b.let(id));
195-
196-
const update = b.stmt(
197-
b.assignment(
198-
'=',
199-
b.id(id),
200-
b.call(
201-
'$.set_attributes',
202-
element_id,
203-
b.id(id),
204-
b.object(values),
205-
context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash),
206-
b.binary('!==', b.member(element_id, 'namespaceURI'), b.id('$.NAMESPACE_SVG')),
207-
is_ignored(element, 'hydration_attribute_changed') && b.true,
208-
b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-'))
209-
)
210-
)
211-
);
212-
213-
if (needs_isolation) {
214-
context.state.init.push(build_update(update));
215-
return false;
216-
}
217-
218-
context.state.update.push(update);
219-
return true;
220-
}
221-
222-
context.state.init.push(
223-
b.stmt(
224-
b.call(
225-
'$.set_attributes',
226-
element_id,
227-
b.literal(null),
228-
b.object(values),
229-
context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash),
230-
b.binary('!==', b.member(element_id, 'namespaceURI'), b.id('$.NAMESPACE_SVG')),
231-
is_ignored(element, 'hydration_attribute_changed') && b.true,
232-
b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-'))
233-
)
234-
)
235-
);
236-
return false;
237-
}

0 commit comments

Comments
 (0)