Skip to content

chore: refactor set_attributes code generation #13353

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {
get_attribute_name,
build_attribute_value,
build_class_directives,
build_style_directives
build_style_directives,
build_set_attributes
} from './shared/element.js';
import { process_children } from './shared/fragment.js';
import {
Expand Down Expand Up @@ -95,7 +96,7 @@ export function RegularElement(node, context) {
/** @type {Map<string, AST.BindDirective>} */
const bindings = new Map();

let has_spread = false;
let has_spread = node.metadata.has_spread;
let has_use = false;

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

case 'Attribute':
// `is` attributes need to be part of the template, otherwise they break
if (attribute.name === 'is' && context.state.metadata.namespace === 'html') {
const { value } = build_attribute_value(attribute.value, context);

if (value.type === 'Literal' && typeof value.value === 'string') {
context.state.template.push(` is="${escape_html(value.value, true)}"`);
continue;
}
}

attributes.push(attribute);
lookup.set(attribute.name, attribute);
break;
Expand All @@ -129,7 +140,6 @@ export function RegularElement(node, context) {

case 'SpreadAttribute':
attributes.push(attribute);
has_spread = true;
break;

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

// Then do attributes
let is_attributes_reactive = false;
if (node.metadata.has_spread) {
build_element_spread_attributes(
let is_attributes_reactive = has_spread;

if (has_spread) {
const attributes_id = b.id(context.state.scope.generate('attributes'));

build_set_attributes(
attributes,
context,
node,
node_id,
// If value binding exists, that one takes care of calling $.init_select
node.name === 'select' && !bindings.has('value')
attributes_id,
(node.metadata.svg || node.metadata.mathml || is_custom_element_node(node)) && b.true,
node.name.includes('-') && b.true
);
is_attributes_reactive = true;

// If value binding exists, that one takes care of calling $.init_select
if (node.name === 'select' && !bindings.has('value')) {
context.state.init.push(
b.stmt(b.call('$.init_select', node_id, b.thunk(b.member(attributes_id, 'value'))))
);

context.state.update.push(
b.if(
b.binary('in', b.literal('value'), attributes_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', node_id, b.member(attributes_id, 'value')))
])
)
);
}
} else {
/** If true, needs `__value` for inputs */
const needs_special_value_handling =
Expand All @@ -229,7 +262,7 @@ export function RegularElement(node, context) {
attribute.name !== 'autofocus' &&
(attribute.value === true || is_text_attribute(attribute))
) {
const name = get_attribute_name(node, attribute, context);
const name = get_attribute_name(node, attribute);
const value = is_text_attribute(attribute) ? attribute.value[0].data : true;

if (name !== 'class' || value) {
Expand Down Expand Up @@ -258,7 +291,7 @@ export function RegularElement(node, context) {
node_id,
context,
is_attributes_reactive,
lookup.has('style') || node.metadata.has_spread
lookup.has('style') || has_spread
);

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

/**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
* @param {ComponentContext} context
* @param {AST.RegularElement} element
* @param {Identifier} element_id
* @param {boolean} needs_select_handling
*/
function build_element_spread_attributes(
attributes,
context,
element,
element_id,
needs_select_handling
) {
let needs_isolation = false;

/** @type {ObjectExpression['properties']} */
const values = [];

for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const name = get_attribute_name(element, attribute, context);
// TODO: handle has_call
const { value } = build_attribute_value(attribute.value, context);

if (
name === 'is' &&
value.type === 'Literal' &&
context.state.metadata.namespace === 'html'
) {
context.state.template.push(` is="${escape_html(value.value, true)}"`);
continue;
}

if (
is_event_attribute(attribute) &&
(get_attribute_expression(attribute).type === 'ArrowFunctionExpression' ||
get_attribute_expression(attribute).type === 'FunctionExpression')
) {
// Give the event handler a stable ID so it isn't removed and readded on every update
const id = context.state.scope.generate('event_handler');
context.state.init.push(b.var(id, value));
values.push(b.init(attribute.name, b.id(id)));
} else {
values.push(b.init(name, value));
}
} else {
values.push(b.spread(/** @type {Expression} */ (context.visit(attribute))));
}

needs_isolation ||=
attribute.type === 'SpreadAttribute' && attribute.metadata.expression.has_call;
}

const preserve_attribute_case =
element.metadata.svg || element.metadata.mathml || is_custom_element_node(element);
const id = b.id(context.state.scope.generate('attributes'));

const update = b.stmt(
b.assignment(
'=',
id,
b.call(
'$.set_attributes',
element_id,
id,
b.object(values),
context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash),
preserve_attribute_case && b.true,
is_ignored(element, 'hydration_attribute_changed') && b.true,
element.name.includes('-') && b.true
)
)
);

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) {
context.state.init.push(build_update(update));
} else {
context.state.update.push(update);
}

if (needs_select_handling) {
context.state.init.push(
b.stmt(b.call('$.init_select', element_id, b.thunk(b.member(id, 'value'))))
);
context.state.update.push(
b.if(
b.binary('in', b.literal('value'), 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(id, 'value')))
])
)
);
}
}

/**
* Serializes an assignment to an element property by adding relevant statements to either only
* the init or the the init and update arrays, depending on whether or not the value is dynamic.
Expand Down Expand Up @@ -581,7 +511,7 @@ function build_element_spread_attributes(
*/
function build_element_attribute_update_assignment(element, node_id, attribute, context) {
const state = context.state;
const name = get_attribute_name(element, attribute, context);
const name = get_attribute_name(element, attribute);
const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';
let { has_call, value } = build_attribute_value(attribute.value, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { determine_namespace_for_children } from '../../utils.js';
import {
build_attribute_value,
build_class_directives,
build_set_attributes,
build_style_directives
} from './shared/element.js';
import { build_render_statement, build_update } from './shared/utils.js';
Expand Down Expand Up @@ -81,10 +82,29 @@ export function SvelteElement(node, context) {
context.state.init.push(...lets); // create computeds in the outer context; the dynamic element is the single child of this slot

// Then do attributes
// Always use spread because we don't know whether the element is a custom element or not,
// therefore we need to do the "how to set an attribute" logic at runtime.
const is_attributes_reactive =
build_dynamic_element_attributes(node, attributes, inner_context, element_id) !== null;
let is_attributes_reactive = false;

if (attributes.length === 0) {
if (context.state.analysis.css.hash) {
inner_context.state.init.push(
b.stmt(b.call('$.set_class', element_id, b.literal(context.state.analysis.css.hash)))
);
}
} else {
const attributes_id = b.id(context.state.scope.generate('attributes'));

// Always use spread because we don't know whether the element is a custom element or not,
// therefore we need to do the "how to set an attribute" logic at runtime.
is_attributes_reactive = build_set_attributes(
attributes,
inner_context,
node,
element_id,
attributes_id,
b.binary('!==', b.member(element_id, 'namespaceURI'), b.id('$.NAMESPACE_SVG')),
b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-'))
);
}

// class/style directives must be applied last since they could override class/style attributes
build_class_directives(class_directives, element_id, inner_context, is_attributes_reactive);
Expand Down Expand Up @@ -133,105 +153,3 @@ export function SvelteElement(node, context) {
)
);
}

/**
* Serializes dynamic element attribute assignments.
* Returns the `true` if spread is deemed reactive.
* @param {AST.SvelteElement} element
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
* @param {ComponentContext} context
* @param {Identifier} element_id
* @returns {boolean}
*/
function build_dynamic_element_attributes(element, attributes, context, element_id) {
if (attributes.length === 0) {
if (context.state.analysis.css.hash) {
context.state.init.push(
b.stmt(b.call('$.set_class', element_id, b.literal(context.state.analysis.css.hash)))
);
}
return false;
}

// TODO why are we always treating this as a spread? needs docs, if that's not an error

let needs_isolation = false;
let is_reactive = false;

/** @type {ObjectExpression['properties']} */
const values = [];

for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const { value } = build_attribute_value(attribute.value, context);

if (
is_event_attribute(attribute) &&
(get_attribute_expression(attribute).type === 'ArrowFunctionExpression' ||
get_attribute_expression(attribute).type === 'FunctionExpression')
) {
// Give the event handler a stable ID so it isn't removed and readded on every update
const id = context.state.scope.generate('event_handler');
context.state.init.push(b.var(id, value));
values.push(b.init(attribute.name, b.id(id)));
} else {
values.push(b.init(attribute.name, value));
}
} else {
values.push(b.spread(/** @type {Expression} */ (context.visit(attribute))));
}

is_reactive ||=
attribute.metadata.expression.has_state ||
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
attribute.type === 'SpreadAttribute';
needs_isolation ||=
attribute.type === 'SpreadAttribute' && attribute.metadata.expression.has_call;
}

if (needs_isolation || is_reactive) {
const id = context.state.scope.generate('attributes');
context.state.init.push(b.let(id));

const update = b.stmt(
b.assignment(
'=',
b.id(id),
b.call(
'$.set_attributes',
element_id,
b.id(id),
b.object(values),
context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash),
b.binary('!==', b.member(element_id, 'namespaceURI'), b.id('$.NAMESPACE_SVG')),
is_ignored(element, 'hydration_attribute_changed') && b.true,
b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-'))
)
)
);

if (needs_isolation) {
context.state.init.push(build_update(update));
return false;
}

context.state.update.push(update);
return true;
}

context.state.init.push(
b.stmt(
b.call(
'$.set_attributes',
element_id,
b.literal(null),
b.object(values),
context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash),
b.binary('!==', b.member(element_id, 'namespaceURI'), b.id('$.NAMESPACE_SVG')),
is_ignored(element, 'hydration_attribute_changed') && b.true,
b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-'))
)
)
);
return false;
}
Loading