-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
feat: add <svelte:html>
element
#14397
base: main
Are you sure you want to change the base?
Changes from 13 commits
81bd131
5ba013e
d61291e
a88e814
0c1c8b9
8634231
0a95949
8f8a5b1
6bf6c74
dd748e2
8df29aa
4cf2077
e33e2cb
7c629e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'svelte': minor | ||
--- | ||
|
||
feat: add `<svelte:html>` element |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
--- | ||
title: <svelte:html> | ||
--- | ||
|
||
```svelte | ||
<svelte:html attribute={value} onevent={handler} /> | ||
``` | ||
|
||
Similarly to `<svelte:body>`, this element allows you to add properties and listeners to events on `document.documentElement`. This is useful for attributes such as `lang` which influence how the browser interprets the content. | ||
|
||
As with `<svelte:window>`, `<svelte:document>` and `<svelte:body>`, this element may only appear the top level of your component and must never be inside a block or element. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/** @import { AST } from '#compiler' */ | ||
/** @import { Context } from '../types' */ | ||
import * as e from '../../../errors.js'; | ||
|
||
/** | ||
* @param {AST.SvelteHTML} node | ||
* @param {Context} context | ||
*/ | ||
export function SvelteHTML(node, context) { | ||
for (const attribute of node.attributes) { | ||
if (attribute.type !== 'Attribute') { | ||
e.svelte_html_illegal_attribute(attribute); | ||
} | ||
} | ||
|
||
if (node.fragment.nodes.length > 0) { | ||
e.svelte_meta_invalid_content(node, node.name); | ||
} | ||
|
||
context.next(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/** @import { ExpressionStatement, Property } from 'estree' */ | ||
/** @import { AST } from '#compiler' */ | ||
/** @import { ComponentContext } from '../types' */ | ||
import { normalize_attribute } from '../../../../../utils.js'; | ||
import { is_event_attribute } from '../../../../utils/ast.js'; | ||
import * as b from '../../../../utils/builders.js'; | ||
import { build_attribute_value } from './shared/element.js'; | ||
import { visit_event_attribute } from './shared/events.js'; | ||
|
||
/** | ||
* @param {AST.SvelteHTML} element | ||
* @param {ComponentContext} context | ||
*/ | ||
export function SvelteHTML(element, context) { | ||
/** @type {Property[]} */ | ||
const attributes = []; | ||
|
||
for (const attribute of element.attributes) { | ||
if (attribute.type === 'Attribute') { | ||
if (is_event_attribute(attribute)) { | ||
visit_event_attribute(attribute, context); | ||
} else { | ||
const name = normalize_attribute(attribute.name); | ||
const { value } = build_attribute_value(attribute.value, context); | ||
|
||
attributes.push(b.init(name, value)); | ||
|
||
if (context.state.options.dev) { | ||
context.state.init.push( | ||
b.stmt(b.call('$.validate_svelte_html_attribute', b.literal(name))) | ||
); | ||
} | ||
} | ||
} | ||
} | ||
|
||
if (attributes.length > 0) { | ||
context.state.init.push(b.stmt(b.call('$.svelte_html', b.arrow([], b.object(attributes))))); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
/** @import { Property } from 'estree' */ | ||
/** @import { AST } from '#compiler' */ | ||
/** @import { ComponentContext } from '../types.js' */ | ||
import { normalize_attribute } from '../../../../../utils.js'; | ||
import { is_event_attribute } from '../../../../utils/ast.js'; | ||
import * as b from '../../../../utils/builders.js'; | ||
import { build_attribute_value } from './shared/utils.js'; | ||
|
||
/** | ||
* @param {AST.SvelteHTML} element | ||
* @param {ComponentContext} context | ||
*/ | ||
export function SvelteHTML(element, context) { | ||
/** @type {Property[]} */ | ||
const attributes = []; | ||
|
||
for (const attribute of element.attributes) { | ||
if (attribute.type === 'Attribute' && !is_event_attribute(attribute)) { | ||
const name = normalize_attribute(attribute.name); | ||
const value = build_attribute_value(attribute.value, context); | ||
attributes.push(b.init(name, value)); | ||
} | ||
} | ||
|
||
context.state.template.push( | ||
b.stmt(b.call('$.svelte_html', b.id('$$payload'), b.object(attributes))) | ||
); | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,62 @@ | ||||||||||
import { render_effect, teardown } from '../../reactivity/effects.js'; | ||||||||||
import { set_attribute } from '../elements/attributes.js'; | ||||||||||
import { set_class } from '../elements/class.js'; | ||||||||||
import { hydrating } from '../hydration.js'; | ||||||||||
|
||||||||||
/** | ||||||||||
* @param {() => Record<string, any>} get_attributes | ||||||||||
* @returns {void} | ||||||||||
*/ | ||||||||||
export function svelte_html(get_attributes) { | ||||||||||
const node = document.documentElement; | ||||||||||
const own = {}; | ||||||||||
|
||||||||||
/** @type {Record<string, Array<[any, any]>>} to check who set the last value of each attribute */ | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think tuples are unnecessarily confusing
Suggested change
|
||||||||||
// @ts-expect-error | ||||||||||
const current_setters = (node.__attributes_setters ??= {}); | ||||||||||
|
||||||||||
/** @type {Record<string, any>} */ | ||||||||||
let attributes; | ||||||||||
|
||||||||||
render_effect(() => { | ||||||||||
attributes = get_attributes(); | ||||||||||
|
||||||||||
for (const name in attributes) { | ||||||||||
const current = (current_setters[name] ??= []); | ||||||||||
const idx = current.findIndex(([owner]) => owner === own); | ||||||||||
const old = idx === -1 ? null : current.splice(idx, 1)[0][1]; | ||||||||||
Rich-Harris marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
||||||||||
let value = attributes[name]; | ||||||||||
current.push([own, value]); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
||||||||||
// Do nothing on initial render during hydration: If there are attribute duplicates, the last value | ||||||||||
// wins, which could result in needless hydration repairs from earlier values. | ||||||||||
if (hydrating) continue; | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This results in mismatches — say for example you have something like this (however inadvisable it may be): <script>
let theme = typeof window !== 'undefined'
? matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
: undefined;
</script>
<svelte:html data-theme={theme} /> |
||||||||||
|
||||||||||
if (name === 'class') { | ||||||||||
// Avoid unrelated attribute changes from triggering class changes | ||||||||||
if (old !== value) { | ||||||||||
set_class(node, current_setters[name].map(([_, text]) => text).join(' ')); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
} | ||||||||||
} else { | ||||||||||
set_attribute(node, name, value); | ||||||||||
} | ||||||||||
} | ||||||||||
}); | ||||||||||
|
||||||||||
teardown(() => { | ||||||||||
for (const name in attributes) { | ||||||||||
const old = current_setters[name]; | ||||||||||
current_setters[name] = old.filter(([owner]) => owner !== own); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
const current = current_setters[name]; | ||||||||||
|
||||||||||
if (name === 'class') { | ||||||||||
set_class(node, current.map(([_, text]) => text).join(' ')); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
||||||||||
// If this was the last one setting this attribute, revert to the previous value | ||||||||||
} else if (old[old.length - 1][0] === own) { | ||||||||||
set_attribute(node, name, current[current.length - 1]?.[1]); | ||||||||||
Comment on lines
+57
to
+58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
} | ||||||||||
} | ||||||||||
}); | ||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like 'owner' makes more sense here