Skip to content

Commit 54083fb

Browse files
trueadmdummdidummRich-Harris
authored
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 <[email protected]> * address feedback * Update packages/svelte/src/internal/client/dom/elements/attributes.js Co-authored-by: Rich Harris <[email protected]> * Update packages/svelte/src/internal/client/dom/elements/attributes.js Co-authored-by: Rich Harris <[email protected]> * address more feedback * address more feedback --------- Co-authored-by: Simon H <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent 7b9fad4 commit 54083fb

File tree

8 files changed

+98
-4
lines changed

8 files changed

+98
-4
lines changed

.changeset/wise-kids-wash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: replay load and error events on load during hydration

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import {
88
} from '../../../../utils/ast.js';
99
import { binding_properties } from '../../../bindings.js';
1010
import { clean_nodes, determine_namespace_for_children, infer_namespace } from '../../utils.js';
11-
import { DOMProperties, PassiveEvents, VoidElements } from '../../../constants.js';
11+
import {
12+
DOMProperties,
13+
LoadErrorElements,
14+
PassiveEvents,
15+
VoidElements
16+
} from '../../../constants.js';
1217
import { is_custom_element_node, is_element_node } from '../../../nodes.js';
1318
import * as b from '../../../../utils/builders.js';
1419
import {
@@ -1904,6 +1909,7 @@ export const template_visitors = {
19041909
let is_content_editable = false;
19051910
let has_content_editable_binding = false;
19061911
let img_might_be_lazy = false;
1912+
let might_need_event_replaying = false;
19071913

19081914
if (is_custom_element) {
19091915
// cloneNode is faster, but it does not instantiate the underlying class of the
@@ -1936,6 +1942,9 @@ export const template_visitors = {
19361942
attributes.push(attribute);
19371943
needs_input_reset = true;
19381944
needs_content_reset = true;
1945+
if (LoadErrorElements.includes(node.name)) {
1946+
might_need_event_replaying = true;
1947+
}
19391948
} else if (attribute.type === 'ClassDirective') {
19401949
class_directives.push(attribute);
19411950
} else if (attribute.type === 'StyleDirective') {
@@ -1958,6 +1967,8 @@ export const template_visitors = {
19581967
) {
19591968
has_content_editable_binding = true;
19601969
}
1970+
} else if (attribute.type === 'UseDirective' && LoadErrorElements.includes(node.name)) {
1971+
might_need_event_replaying = true;
19611972
}
19621973
context.visit(attribute);
19631974
}
@@ -2010,6 +2021,12 @@ export const template_visitors = {
20102021
} else {
20112022
for (const attribute of /** @type {import('#compiler').Attribute[]} */ (attributes)) {
20122023
if (is_event_attribute(attribute)) {
2024+
if (
2025+
(attribute.name === 'onload' || attribute.name === 'onerror') &&
2026+
LoadErrorElements.includes(node.name)
2027+
) {
2028+
might_need_event_replaying = true;
2029+
}
20132030
serialize_event_attribute(attribute, context);
20142031
continue;
20152032
}
@@ -2058,6 +2075,10 @@ export const template_visitors = {
20582075
serialize_class_directives(class_directives, node_id, context, is_attributes_reactive);
20592076
serialize_style_directives(style_directives, node_id, context, is_attributes_reactive);
20602077

2078+
if (might_need_event_replaying) {
2079+
context.state.after_update.push(b.stmt(b.call('$.replay_events', node_id)));
2080+
}
2081+
20612082
context.state.template.push('>');
20622083

20632084
/** @type {import('../types.js').SourceLocation[]} */

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as b from '../../../utils/builders.js';
1111
import is_reference from 'is-reference';
1212
import {
1313
ContentEditableBindings,
14+
LoadErrorElements,
1415
VoidElements,
1516
WhitespaceInsensitiveAttributes
1617
} from '../../constants.js';
@@ -1845,6 +1846,7 @@ function serialize_element_attributes(node, context) {
18451846
// Use the index to keep the attributes order which is important for spreading
18461847
let class_attribute_idx = -1;
18471848
let style_attribute_idx = -1;
1849+
let events_to_capture = new Set();
18481850

18491851
for (const attribute of node.attributes) {
18501852
if (attribute.type === 'Attribute') {
@@ -1861,8 +1863,15 @@ function serialize_element_attributes(node, context) {
18611863
}
18621864
content = { escape: true, expression: serialize_attribute_value(attribute.value, context) };
18631865

1864-
// omit event handlers
1865-
} else if (!is_event_attribute(attribute)) {
1866+
// omit event handlers except for special cases
1867+
} else if (is_event_attribute(attribute)) {
1868+
if (
1869+
(attribute.name === 'onload' || attribute.name === 'onerror') &&
1870+
LoadErrorElements.includes(node.name)
1871+
) {
1872+
events_to_capture.add(attribute.name);
1873+
}
1874+
} else {
18661875
if (attribute.name === 'class') {
18671876
class_attribute_idx = attributes.length;
18681877
} else if (attribute.name === 'style') {
@@ -1960,6 +1969,15 @@ function serialize_element_attributes(node, context) {
19601969
} else if (attribute.type === 'SpreadAttribute') {
19611970
attributes.push(attribute);
19621971
has_spread = true;
1972+
if (LoadErrorElements.includes(node.name)) {
1973+
events_to_capture.add('onload');
1974+
events_to_capture.add('onerror');
1975+
}
1976+
} else if (attribute.type === 'UseDirective') {
1977+
if (LoadErrorElements.includes(node.name)) {
1978+
events_to_capture.add('onload');
1979+
events_to_capture.add('onerror');
1980+
}
19631981
} else if (attribute.type === 'ClassDirective') {
19641982
class_directives.push(attribute);
19651983
} else if (attribute.type === 'StyleDirective') {
@@ -2042,6 +2060,12 @@ function serialize_element_attributes(node, context) {
20422060
}
20432061
}
20442062

2063+
if (events_to_capture.size !== 0) {
2064+
for (const event of events_to_capture) {
2065+
context.state.template.push(t_string(` ${event}="this.__e=event"`));
2066+
}
2067+
}
2068+
20452069
return content;
20462070
}
20472071

packages/svelte/src/compiler/phases/constants.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ export const WhitespaceInsensitiveAttributes = ['class', 'style'];
6161

6262
export const ContentEditableBindings = ['textContent', 'innerHTML', 'innerText'];
6363

64+
export const LoadErrorElements = [
65+
'body',
66+
'embed',
67+
'iframe',
68+
'img',
69+
'link',
70+
'object',
71+
'script',
72+
'style',
73+
'track'
74+
];
75+
6476
export const SVGElements = [
6577
'altGlyph',
6678
'altGlyphDef',

packages/svelte/src/internal/client/dom/elements/events.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,34 @@
11
import { render_effect } from '../../reactivity/effects.js';
22
import { all_registered_events, root_event_handles } from '../../render.js';
33
import { define_property, is_array } from '../../utils.js';
4+
import { hydrating } from '../hydration.js';
5+
6+
/**
7+
* SSR adds onload and onerror attributes to catch those events before the hydration.
8+
* This function detects those cases, removes the attributes and replays the events.
9+
* @param {HTMLElement} dom
10+
*/
11+
export function replay_events(dom) {
12+
if (!hydrating) return;
13+
14+
if (dom.onload) {
15+
dom.removeAttribute('onload');
16+
}
17+
if (dom.onerror) {
18+
dom.removeAttribute('onerror');
19+
}
20+
// @ts-expect-error
21+
const event = dom.__e;
22+
if (event !== undefined) {
23+
// @ts-expect-error
24+
dom.__e = undefined;
25+
queueMicrotask(() => {
26+
if (dom.isConnected) {
27+
dom.dispatchEvent(event);
28+
}
29+
});
30+
}
31+
}
432

533
/**
634
* @param {string} event_name

packages/svelte/src/internal/client/dom/operations.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ export function init_operations() {
7171
element_prototype.__className = '';
7272
// @ts-expect-error
7373
element_prototype.__attributes = null;
74+
// @ts-expect-error
75+
element_prototype.__e = undefined;
7476

7577
if (DEV) {
7678
// @ts-expect-error

packages/svelte/src/internal/client/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export {
3030
handle_lazy_img
3131
} from './dom/elements/attributes.js';
3232
export { set_class, set_svg_class, set_mathml_class, toggle_class } from './dom/elements/class.js';
33-
export { event, delegate } from './dom/elements/events.js';
33+
export { event, delegate, replay_events } from './dom/elements/events.js';
3434
export { autofocus, remove_textarea_child } from './dom/elements/misc.js';
3535
export { set_style } from './dom/elements/style.js';
3636
export { animation, transition } from './dom/elements/transitions.js';

packages/svelte/tests/html_equal.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export function normalize_html(
8080
.replace(/(<!(--)?.*?\2>)/g, preserveComments ? '$1' : '')
8181
.replace(/(data-svelte-h="[^"]+")/g, removeDataSvelte ? '' : '$1')
8282
.replace(/>[ \t\n\r\f]+</g, '><')
83+
// Strip out the special onload/onerror hydration events from the test output
84+
.replace(/\s?onerror="this.__e=event"|\s?onload="this.__e=event"/g, '')
8385
.trim();
8486
clean_children(node);
8587
return node.innerHTML.replace(/<\/?noscript\/?>/g, '');

0 commit comments

Comments
 (0)