Skip to content

Commit 13cb385

Browse files
authored
chore: refactor RegularElement visitor (#13350)
* WIP * tidy * simplify * simplify * more * more * use a switch * alphabetize * simplify * group stuff * rename * simplify * shuffle * shuffle * doh
1 parent 9e511b1 commit 13cb385

File tree

1 file changed

+134
-128
lines changed

1 file changed

+134
-128
lines changed

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

+134-128
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,20 @@ export function RegularElement(node, context) {
5858
return;
5959
}
6060

61+
const is_custom_element = is_custom_element_node(node);
62+
63+
if (is_custom_element) {
64+
// cloneNode is faster, but it does not instantiate the underlying class of the
65+
// custom element until the template is connected to the dom, which would
66+
// cause problems when setting properties on the custom element.
67+
// Therefore we need to use importNode instead, which doesn't have this caveat.
68+
context.state.metadata.context.template_needs_import_node = true;
69+
}
70+
6171
if (node.name === 'script') {
6272
context.state.metadata.context.template_contains_script_tag = true;
6373
}
6474

65-
const metadata = context.state.metadata;
66-
const child_metadata = {
67-
...context.state.metadata,
68-
namespace: determine_namespace_for_children(node, context.state.metadata.namespace)
69-
};
70-
7175
context.state.template.push(`<${node.name}`);
7276

7377
/** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
@@ -79,151 +83,138 @@ export function RegularElement(node, context) {
7983
/** @type {AST.StyleDirective[]} */
8084
const style_directives = [];
8185

86+
/** @type {Array<AST.AnimateDirective | AST.BindDirective | AST.OnDirective | AST.TransitionDirective | AST.UseDirective>} */
87+
const other_directives = [];
88+
8289
/** @type {ExpressionStatement[]} */
8390
const lets = [];
8491

85-
const is_custom_element = is_custom_element_node(node);
86-
let needs_input_reset = false;
87-
let needs_content_reset = false;
88-
89-
/** @type {AST.BindDirective | null} */
90-
let value_binding = null;
92+
/** @type {Map<string, AST.Attribute>} */
93+
const lookup = new Map();
9194

92-
/** If true, needs `__value` for inputs */
93-
let needs_special_value_handling = node.name === 'option' || node.name === 'select';
94-
let is_content_editable = false;
95-
let has_content_editable_binding = false;
96-
let img_might_be_lazy = false;
97-
let might_need_event_replaying = false;
98-
let has_direction_attribute = false;
99-
let has_style_attribute = false;
95+
/** @type {Map<string, AST.BindDirective>} */
96+
const bindings = new Map();
10097

101-
if (is_custom_element) {
102-
// cloneNode is faster, but it does not instantiate the underlying class of the
103-
// custom element until the template is connected to the dom, which would
104-
// cause problems when setting properties on the custom element.
105-
// Therefore we need to use importNode instead, which doesn't have this caveat.
106-
metadata.context.template_needs_import_node = true;
107-
}
98+
let has_spread = false;
99+
let has_use = false;
108100

109-
// visit let directives first, to set state
110101
for (const attribute of node.attributes) {
111-
if (attribute.type === 'LetDirective') {
112-
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
102+
switch (attribute.type) {
103+
case 'AnimateDirective':
104+
other_directives.push(attribute);
105+
break;
106+
107+
case 'Attribute':
108+
attributes.push(attribute);
109+
lookup.set(attribute.name, attribute);
110+
break;
111+
112+
case 'BindDirective':
113+
bindings.set(attribute.name, attribute);
114+
other_directives.push(attribute);
115+
break;
116+
117+
case 'ClassDirective':
118+
class_directives.push(attribute);
119+
break;
120+
121+
case 'LetDirective':
122+
// visit let directives before everything else, to set state
123+
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
124+
break;
125+
126+
case 'OnDirective':
127+
other_directives.push(attribute);
128+
break;
129+
130+
case 'SpreadAttribute':
131+
attributes.push(attribute);
132+
has_spread = true;
133+
break;
134+
135+
case 'StyleDirective':
136+
style_directives.push(attribute);
137+
break;
138+
139+
case 'TransitionDirective':
140+
other_directives.push(attribute);
141+
break;
142+
143+
case 'UseDirective':
144+
has_use = true;
145+
other_directives.push(attribute);
146+
break;
113147
}
114148
}
115149

116-
for (const attribute of node.attributes) {
117-
if (attribute.type === 'Attribute') {
118-
attributes.push(attribute);
119-
if (node.name === 'img' && attribute.name === 'loading') {
120-
img_might_be_lazy = true;
121-
}
122-
if (attribute.name === 'dir') {
123-
has_direction_attribute = true;
124-
}
125-
if (attribute.name === 'style') {
126-
has_style_attribute = true;
127-
}
128-
if (
129-
(attribute.name === 'value' || attribute.name === 'checked') &&
130-
!is_text_attribute(attribute)
131-
) {
132-
needs_input_reset = true;
133-
needs_content_reset = true;
134-
} else if (
135-
attribute.name === 'contenteditable' &&
136-
(attribute.value === true ||
137-
(is_text_attribute(attribute) && attribute.value[0].data === 'true'))
138-
) {
139-
is_content_editable = true;
140-
}
141-
} else if (attribute.type === 'SpreadAttribute') {
142-
attributes.push(attribute);
143-
needs_input_reset = true;
144-
needs_content_reset = true;
145-
if (is_load_error_element(node.name)) {
146-
might_need_event_replaying = true;
147-
}
148-
} else if (attribute.type === 'ClassDirective') {
149-
class_directives.push(attribute);
150-
} else if (attribute.type === 'StyleDirective') {
151-
style_directives.push(attribute);
152-
} else if (attribute.type === 'OnDirective') {
150+
for (const attribute of other_directives) {
151+
if (attribute.type === 'OnDirective') {
153152
const handler = /** @type {Expression} */ (context.visit(attribute));
154-
const has_action_directive = node.attributes.find((a) => a.type === 'UseDirective');
155153

156154
context.state.after_update.push(
157-
b.stmt(has_action_directive ? b.call('$.effect', b.thunk(handler)) : handler)
155+
b.stmt(has_use ? b.call('$.effect', b.thunk(handler)) : handler)
158156
);
159-
} else if (attribute.type !== 'LetDirective') {
160-
if (attribute.type === 'BindDirective') {
161-
if (attribute.name === 'group' || attribute.name === 'checked') {
162-
needs_special_value_handling = true;
163-
needs_input_reset = true;
164-
} else if (attribute.name === 'value') {
165-
value_binding = attribute;
166-
needs_content_reset = true;
167-
needs_input_reset = true;
168-
} else if (
169-
attribute.name === 'innerHTML' ||
170-
attribute.name === 'innerText' ||
171-
attribute.name === 'textContent'
172-
) {
173-
has_content_editable_binding = true;
174-
}
175-
} else if (attribute.type === 'UseDirective' && is_load_error_element(node.name)) {
176-
might_need_event_replaying = true;
177-
}
157+
} else {
178158
context.visit(attribute);
179159
}
180160
}
181161

182-
if (is_content_editable && has_content_editable_binding) {
183-
child_metadata.bound_contenteditable = true;
184-
}
185-
186-
if (needs_input_reset && node.name === 'input') {
162+
if (
163+
node.name === 'input' &&
164+
(has_spread ||
165+
bindings.has('value') ||
166+
bindings.has('checked') ||
167+
bindings.has('group') ||
168+
attributes.some(
169+
(attribute) =>
170+
attribute.type === 'Attribute' &&
171+
(attribute.name === 'value' || attribute.name === 'checked') &&
172+
!is_text_attribute(attribute)
173+
))
174+
) {
187175
context.state.init.push(b.stmt(b.call('$.remove_input_defaults', context.state.node)));
188176
}
189177

190-
if (needs_content_reset && node.name === 'textarea') {
191-
context.state.init.push(b.stmt(b.call('$.remove_textarea_child', context.state.node)));
192-
}
178+
if (node.name === 'textarea') {
179+
const attribute = lookup.get('value') ?? lookup.get('checked');
180+
const needs_content_reset = attribute && !is_text_attribute(attribute);
193181

194-
if (value_binding !== null && node.name === 'select') {
195-
setup_select_synchronization(value_binding, context);
182+
if (has_spread || bindings.has('value') || needs_content_reset) {
183+
context.state.init.push(b.stmt(b.call('$.remove_textarea_child', context.state.node)));
184+
}
196185
}
197186

198-
const node_id = context.state.node;
187+
if (node.name === 'select' && bindings.has('value')) {
188+
setup_select_synchronization(/** @type {AST.BindDirective} */ (bindings.get('value')), context);
189+
}
199190

200191
// Let bindings first, they can be used on attributes
201192
context.state.init.push(...lets);
202193

194+
const node_id = context.state.node;
195+
203196
// Then do attributes
204197
let is_attributes_reactive = false;
205198
if (node.metadata.has_spread) {
206-
if (node.name === 'img') {
207-
img_might_be_lazy = true;
208-
}
209199
build_element_spread_attributes(
210200
attributes,
211201
context,
212202
node,
213203
node_id,
214204
// If value binding exists, that one takes care of calling $.init_select
215-
value_binding === null && node.name === 'select'
205+
node.name === 'select' && !bindings.has('value')
216206
);
217207
is_attributes_reactive = true;
218208
} else {
209+
/** If true, needs `__value` for inputs */
210+
const needs_special_value_handling =
211+
node.name === 'option' ||
212+
node.name === 'select' ||
213+
bindings.has('group') ||
214+
bindings.has('checked');
215+
219216
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
220217
if (is_event_attribute(attribute)) {
221-
if (
222-
(attribute.name === 'onload' || attribute.name === 'onerror') &&
223-
is_load_error_element(node.name)
224-
) {
225-
might_need_event_replaying = true;
226-
}
227218
visit_event_attribute(attribute, context);
228219
continue;
229220
}
@@ -260,35 +251,52 @@ export function RegularElement(node, context) {
260251
}
261252
}
262253

263-
// Apply the src and loading attributes for <img> elements after the element is appended to the document
264-
if (img_might_be_lazy) {
265-
context.state.after_update.push(b.stmt(b.call('$.handle_lazy_img', node_id)));
266-
}
267-
268254
// class/style directives must be applied last since they could override class/style attributes
269255
build_class_directives(class_directives, node_id, context, is_attributes_reactive);
270256
build_style_directives(
271257
style_directives,
272258
node_id,
273259
context,
274260
is_attributes_reactive,
275-
has_style_attribute || node.metadata.has_spread
261+
lookup.has('style') || node.metadata.has_spread
276262
);
277263

278-
if (might_need_event_replaying) {
264+
// Apply the src and loading attributes for <img> elements after the element is appended to the document
265+
if (node.name === 'img' && (has_spread || lookup.has('loading'))) {
266+
context.state.after_update.push(b.stmt(b.call('$.handle_lazy_img', node_id)));
267+
}
268+
269+
if (
270+
is_load_error_element(node.name) &&
271+
(has_spread || has_use || lookup.has('onload') || lookup.has('onerror'))
272+
) {
279273
context.state.after_update.push(b.stmt(b.call('$.replay_events', node_id)));
280274
}
281275

282276
context.state.template.push('>');
283277

284-
/** @type {SourceLocation[]} */
285-
const child_locations = [];
278+
const metadata = {
279+
...context.state.metadata,
280+
namespace: determine_namespace_for_children(node, context.state.metadata.namespace)
281+
};
282+
283+
if (bindings.has('innerHTML') || bindings.has('innerText') || bindings.has('textContent')) {
284+
const contenteditable = lookup.get('contenteditable');
285+
286+
if (
287+
contenteditable &&
288+
(contenteditable.value === true ||
289+
(is_text_attribute(contenteditable) && contenteditable.value[0].data === 'true'))
290+
) {
291+
metadata.bound_contenteditable = true;
292+
}
293+
}
286294

287295
/** @type {ComponentClientTransformState} */
288296
const state = {
289297
...context.state,
290-
metadata: child_metadata,
291-
locations: child_locations,
298+
metadata,
299+
locations: [],
292300
scope: /** @type {Scope} */ (context.state.scopes.get(node.fragment)),
293301
preserve_whitespace:
294302
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea'
@@ -298,15 +306,12 @@ export function RegularElement(node, context) {
298306
node,
299307
node.fragment.nodes,
300308
context.path,
301-
child_metadata.namespace,
309+
state.metadata.namespace,
302310
state,
303311
node.name === 'script' || state.preserve_whitespace,
304312
state.options.preserveComments
305313
);
306314

307-
/** Whether or not we need to wrap the children in `{...}` to avoid declaration conflicts */
308-
const has_declaration = node.fragment.nodes.some((node) => node.type === 'SnippetBlock');
309-
310315
/** @type {typeof state} */
311316
const child_state = { ...state, init: [], update: [], after_update: [] };
312317

@@ -357,7 +362,8 @@ export function RegularElement(node, context) {
357362
}
358363
}
359364

360-
if (has_declaration) {
365+
if (node.fragment.nodes.some((node) => node.type === 'SnippetBlock')) {
366+
// Wrap children in `{...}` to avoid declaration conflicts
361367
context.state.init.push(
362368
b.block([
363369
...child_state.init,
@@ -371,16 +377,16 @@ export function RegularElement(node, context) {
371377
context.state.after_update.push(...child_state.after_update);
372378
}
373379

374-
if (has_direction_attribute) {
380+
if (lookup.has('dir')) {
375381
// This fixes an issue with Chromium where updates to text content within an element
376382
// does not update the direction when set to auto. If we just re-assign the dir, this fixes it.
377383
const dir = b.member(node_id, 'dir');
378384
context.state.update.push(b.stmt(b.assignment('=', dir, dir)));
379385
}
380386

381-
if (child_locations.length > 0) {
387+
if (state.locations.length > 0) {
382388
// @ts-expect-error
383-
location.push(child_locations);
389+
location.push(state.locations);
384390
}
385391

386392
if (!is_void(node.name)) {

0 commit comments

Comments
 (0)