From 54083fb9cc2fbb75207d53e59948dc46bd6747e7 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 16 May 2024 16:43:47 +0100 Subject: [PATCH] fix: replay load and error events on load during hydration (#11642) * fix: replay load and error events on load during hydration * oops * fix replacement logic * make less evasive * address feedback * address feedback * address feedback * Update packages/svelte/src/internal/client/dom/elements/events.js Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> * address feedback * Update packages/svelte/src/internal/client/dom/elements/attributes.js Co-authored-by: Rich Harris * Update packages/svelte/src/internal/client/dom/elements/attributes.js Co-authored-by: Rich Harris * address more feedback * address more feedback --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Rich Harris --- .changeset/wise-kids-wash.md | 5 ++++ .../3-transform/client/visitors/template.js | 23 ++++++++++++++- .../3-transform/server/transform-server.js | 28 +++++++++++++++++-- .../svelte/src/compiler/phases/constants.js | 12 ++++++++ .../internal/client/dom/elements/events.js | 28 +++++++++++++++++++ .../src/internal/client/dom/operations.js | 2 ++ packages/svelte/src/internal/client/index.js | 2 +- packages/svelte/tests/html_equal.js | 2 ++ 8 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 .changeset/wise-kids-wash.md diff --git a/.changeset/wise-kids-wash.md b/.changeset/wise-kids-wash.md new file mode 100644 index 0000000000..c7fb745304 --- /dev/null +++ b/.changeset/wise-kids-wash.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: replay load and error events on load during hydration diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 85fcf7afe3..e6f80d0000 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -8,7 +8,12 @@ import { } from '../../../../utils/ast.js'; import { binding_properties } from '../../../bindings.js'; import { clean_nodes, determine_namespace_for_children, infer_namespace } from '../../utils.js'; -import { DOMProperties, PassiveEvents, VoidElements } from '../../../constants.js'; +import { + DOMProperties, + LoadErrorElements, + PassiveEvents, + VoidElements +} from '../../../constants.js'; import { is_custom_element_node, is_element_node } from '../../../nodes.js'; import * as b from '../../../../utils/builders.js'; import { @@ -1904,6 +1909,7 @@ export const template_visitors = { let is_content_editable = false; let has_content_editable_binding = false; let img_might_be_lazy = false; + let might_need_event_replaying = false; if (is_custom_element) { // cloneNode is faster, but it does not instantiate the underlying class of the @@ -1936,6 +1942,9 @@ export const template_visitors = { attributes.push(attribute); needs_input_reset = true; needs_content_reset = true; + if (LoadErrorElements.includes(node.name)) { + might_need_event_replaying = true; + } } else if (attribute.type === 'ClassDirective') { class_directives.push(attribute); } else if (attribute.type === 'StyleDirective') { @@ -1958,6 +1967,8 @@ export const template_visitors = { ) { has_content_editable_binding = true; } + } else if (attribute.type === 'UseDirective' && LoadErrorElements.includes(node.name)) { + might_need_event_replaying = true; } context.visit(attribute); } @@ -2010,6 +2021,12 @@ export const template_visitors = { } else { for (const attribute of /** @type {import('#compiler').Attribute[]} */ (attributes)) { if (is_event_attribute(attribute)) { + if ( + (attribute.name === 'onload' || attribute.name === 'onerror') && + LoadErrorElements.includes(node.name) + ) { + might_need_event_replaying = true; + } serialize_event_attribute(attribute, context); continue; } @@ -2058,6 +2075,10 @@ export const template_visitors = { serialize_class_directives(class_directives, node_id, context, is_attributes_reactive); serialize_style_directives(style_directives, node_id, context, is_attributes_reactive); + if (might_need_event_replaying) { + context.state.after_update.push(b.stmt(b.call('$.replay_events', node_id))); + } + context.state.template.push('>'); /** @type {import('../types.js').SourceLocation[]} */ diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 6da66afba7..2cb251f120 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -11,6 +11,7 @@ import * as b from '../../../utils/builders.js'; import is_reference from 'is-reference'; import { ContentEditableBindings, + LoadErrorElements, VoidElements, WhitespaceInsensitiveAttributes } from '../../constants.js'; @@ -1845,6 +1846,7 @@ function serialize_element_attributes(node, context) { // Use the index to keep the attributes order which is important for spreading let class_attribute_idx = -1; let style_attribute_idx = -1; + let events_to_capture = new Set(); for (const attribute of node.attributes) { if (attribute.type === 'Attribute') { @@ -1861,8 +1863,15 @@ function serialize_element_attributes(node, context) { } content = { escape: true, expression: serialize_attribute_value(attribute.value, context) }; - // omit event handlers - } else if (!is_event_attribute(attribute)) { + // omit event handlers except for special cases + } else if (is_event_attribute(attribute)) { + if ( + (attribute.name === 'onload' || attribute.name === 'onerror') && + LoadErrorElements.includes(node.name) + ) { + events_to_capture.add(attribute.name); + } + } else { if (attribute.name === 'class') { class_attribute_idx = attributes.length; } else if (attribute.name === 'style') { @@ -1960,6 +1969,15 @@ function serialize_element_attributes(node, context) { } else if (attribute.type === 'SpreadAttribute') { attributes.push(attribute); has_spread = true; + if (LoadErrorElements.includes(node.name)) { + events_to_capture.add('onload'); + events_to_capture.add('onerror'); + } + } else if (attribute.type === 'UseDirective') { + if (LoadErrorElements.includes(node.name)) { + events_to_capture.add('onload'); + events_to_capture.add('onerror'); + } } else if (attribute.type === 'ClassDirective') { class_directives.push(attribute); } else if (attribute.type === 'StyleDirective') { @@ -2042,6 +2060,12 @@ function serialize_element_attributes(node, context) { } } + if (events_to_capture.size !== 0) { + for (const event of events_to_capture) { + context.state.template.push(t_string(` ${event}="this.__e=event"`)); + } + } + return content; } diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index 8e3c011395..29474d79fa 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -61,6 +61,18 @@ export const WhitespaceInsensitiveAttributes = ['class', 'style']; export const ContentEditableBindings = ['textContent', 'innerHTML', 'innerText']; +export const LoadErrorElements = [ + 'body', + 'embed', + 'iframe', + 'img', + 'link', + 'object', + 'script', + 'style', + 'track' +]; + export const SVGElements = [ 'altGlyph', 'altGlyphDef', diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index b030996fbc..e0298af92a 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -1,6 +1,34 @@ import { render_effect } from '../../reactivity/effects.js'; import { all_registered_events, root_event_handles } from '../../render.js'; import { define_property, is_array } from '../../utils.js'; +import { hydrating } from '../hydration.js'; + +/** + * SSR adds onload and onerror attributes to catch those events before the hydration. + * This function detects those cases, removes the attributes and replays the events. + * @param {HTMLElement} dom + */ +export function replay_events(dom) { + if (!hydrating) return; + + if (dom.onload) { + dom.removeAttribute('onload'); + } + if (dom.onerror) { + dom.removeAttribute('onerror'); + } + // @ts-expect-error + const event = dom.__e; + if (event !== undefined) { + // @ts-expect-error + dom.__e = undefined; + queueMicrotask(() => { + if (dom.isConnected) { + dom.dispatchEvent(event); + } + }); + } +} /** * @param {string} event_name diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index b5c4d2613a..2307c013b0 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -71,6 +71,8 @@ export function init_operations() { element_prototype.__className = ''; // @ts-expect-error element_prototype.__attributes = null; + // @ts-expect-error + element_prototype.__e = undefined; if (DEV) { // @ts-expect-error diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index dd0d759d8b..1a7eb86cc5 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -30,7 +30,7 @@ export { handle_lazy_img } from './dom/elements/attributes.js'; export { set_class, set_svg_class, set_mathml_class, toggle_class } from './dom/elements/class.js'; -export { event, delegate } from './dom/elements/events.js'; +export { event, delegate, replay_events } from './dom/elements/events.js'; export { autofocus, remove_textarea_child } from './dom/elements/misc.js'; export { set_style } from './dom/elements/style.js'; export { animation, transition } from './dom/elements/transitions.js'; diff --git a/packages/svelte/tests/html_equal.js b/packages/svelte/tests/html_equal.js index a53f3ef35e..c75e3ed4a1 100644 --- a/packages/svelte/tests/html_equal.js +++ b/packages/svelte/tests/html_equal.js @@ -80,6 +80,8 @@ export function normalize_html( .replace(/()/g, preserveComments ? '$1' : '') .replace(/(data-svelte-h="[^"]+")/g, removeDataSvelte ? '' : '$1') .replace(/>[ \t\n\r\f]+<') + // Strip out the special onload/onerror hydration events from the test output + .replace(/\s?onerror="this.__e=event"|\s?onload="this.__e=event"/g, '') .trim(); clean_children(node); return node.innerHTML.replace(/<\/?noscript\/?>/g, '');