Skip to content

Commit

Permalink
fix: replay load and error events on load during hydration (#11642)
Browse files Browse the repository at this point in the history
* 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 <rich.harris@vercel.com>

* Update packages/svelte/src/internal/client/dom/elements/attributes.js

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* address more feedback

* address more feedback

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
  • Loading branch information
3 people committed May 16, 2024
1 parent 7b9fad4 commit 54083fb
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/wise-kids-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

fix: replay load and error events on load during hydration
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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') {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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[]} */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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') {
Expand All @@ -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') {
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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;
}

Expand Down
12 changes: 12 additions & 0 deletions packages/svelte/src/compiler/phases/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
28 changes: 28 additions & 0 deletions packages/svelte/src/internal/client/dom/elements/events.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/svelte/src/internal/client/dom/operations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 2 additions & 0 deletions packages/svelte/tests/html_equal.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export function normalize_html(
.replace(/(<!(--)?.*?\2>)/g, preserveComments ? '$1' : '')
.replace(/(data-svelte-h="[^"]+")/g, removeDataSvelte ? '' : '$1')
.replace(/>[ \t\n\r\f]+</g, '><')
// 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, '');
Expand Down

0 comments on commit 54083fb

Please sign in to comment.