Skip to content
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

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft

feat: add <svelte:html> element #14397

wants to merge 11 commits into from

Conversation

dummdidumm
Copy link
Member

@dummdidumm dummdidumm commented Nov 21, 2024

closes #8663

Companion PR in SvelteKit: sveltejs/kit#13065

Before submitting the PR, please make sure you do the following

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • Prefix your PR title with feat:, fix:, chore:, or docs:.
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.
  • If this PR changes code within packages/svelte/src, add a changeset (npx changeset).

Tests and linting

  • Run the tests with pnpm test and lint the project with pnpm lint

Copy link

changeset-bot bot commented Nov 21, 2024

🦋 Changeset detected

Latest commit: 8df29aa

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
svelte Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@Rich-Harris
Copy link
Member

preview: https://svelte-dev-git-preview-svelte-14397-svelte.vercel.app/

this is an automated message

Copy link
Contributor

Playground

pnpm add https://pkg.pr.new/svelte@14397

@Leonidaz
Copy link

Nice! works as expected.

one minor issue that I noticed in the playground, if you add an attribute and then erase it from the code, the attribute stays in the <html>, unless you set the attribute to undefined and then it's removed.

@dummdidumm
Copy link
Member Author

that sounds like something we need to specifically handle in the playground/during hmr

*/
export function svelte_html(payload, attributes) {
for (const name in attributes) {
payload.htmlAttributes.set(name, escape_html(attributes[name], true));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if the attribute already exists? do we error, or overwrite?

what if it's a class attribute? e.g. you have one component like this...

<svelte:html class="{theme === 'dark' ? 'dark' : undefined}" />

...and another like this?

<svelte:html class="{prefers_typescript ? 'prefers-ts' : 'prefers-js'}" />

Neither behaviour would be desirable in that case. Maybe class needs to be treated as a special case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently it's "last one wins" for all attributes, similar to how the last <title> would win if you had multiple of them within <svelte:head> - which I think makes the most sense. Agree that class could use special handling though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we at least warn if attributes are getting clobbered? Seems more likely to be a mistake than anything. (The same is probably true of <title>, and possibly other unique head tags, though we don't need to solve that right now)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we can warn at dev time

Comment on lines +148 to +150
htmlAttributes: [...payload.htmlAttributes]
.map(([name, value]) => `${name}="${value}"`)
.join(' ')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Someone might have an existing classname in their HTML template, in which case giving them a string would make it awkward to combine stuff. Should we return an object instead of a string, so that they have more flexibility?

Copy link
Member Author

@dummdidumm dummdidumm Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I briefly thought about that but it seemed unnecessary - in which case would you have existing attributes on a html tag but in such a way that you know which ones to then merge them in some way? Even if, the regex for adjusting the html attributes string would be straightforward. So I opted for making the simple case more ergonomic.

It is an interesting question for SvelteKit specifically though, which currently sets lang="en" in app.html by default. What would we do here? (regardless of whether we return a string or an object). The easiest would be to have lang="en" after the string and rely on browser being forgiving about it (they ignore duplicate attributes) / the user removing it in case they set it themselves in <svelte:html>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it only really matters for new projects, I think, since we can't retroactively add %htmlAttributes% anyway. I think we just replace lang="en" with %htmlAttributes% in the template project's app.html, and add this to the root layout:

<svelte:html lang="en" />

@dummdidumm
Copy link
Member Author

What should happen when someone has <svelte:html foo="bar" /> in a component, and that component is destroyed? Does foo="bar" stay as the attribute on the HTML tag, or is it removed?

@Rich-Harris
Copy link
Member

I think it should be removed, and if there's another component with <svelte:html foo="baz" /> that was previously overridden, the attribute should change to that instead. Which admittedly could be awkward to implement

@ollema
Copy link

ollema commented Nov 29, 2024

will it be possible to make this work with localStorage to prevent FOUC upon initial page load?

not sure how to describe the problem in the best way. but currently, if you want to configure the dark/light mode, you can achieve this with e.g. the mode-watcher package.

the mode-watcher inserts a stringified function into the head that sets the correct <html> class based on localStorage and/or prefers-color-scheme, basically something along these lines:

<svelte:head>
  {@html `<script>(` +
    setInitialMode.toString() +
    `)(` +
    args +
    `);</script>`}
</svelte:head>

where:

export function setInitialMode(defaultMode: Mode, themeColors?: ThemeColors) {
  const rootEl = document.documentElement;
  const mode = localStorage.getItem('mode-watcher-mode') || defaultMode;
  const light =
    mode === 'light' ||
    (mode === 'system' && window.matchMedia('(prefers-color-scheme: light)').matches);

  rootEl.classList[light ? 'remove' : 'add']('dark');
  rootEl.style.colorScheme = light ? 'light' : 'dark';

  localStorage.setItem('mode-watcher-mode', mode);
}

but this approach is a bit brittle and it would be great if it could be simplified!

it is also complicated if you want to use nonce, then you can not use the mode-watcher component out of the box

@Rich-Harris
Copy link
Member

No, it would still be necessary to inject a <script> into the head. Which... now that I think about it does make me question how valuable this feature really is, especially when we consider all the additional complexity it entails (figuring out how to revert to a previous attribute value when one instance is removed, combining classes from different components, adding new stuff to the render return argument, etc).

Maybe the status quo is actually fine?

@dummdidumm
Copy link
Member Author

I think it's still valuabe:

  • works without JS
  • no idea how browsers behave if you change the lang attribute midway through on the client
  • an API everyone knows, compared to coming up with script tag workarounds which may all look a bit different

The fact that there are tricky things to figure out isn't really an argument against it to me, if we don't answer these questions / figure them out then we're essentially saying "good luck user, you gotta figure it out" - we question of "how to handle duplicate attributes" isn't solved in user land (even harder there, probably).

@Leonidaz
Copy link

Leonidaz commented Nov 30, 2024

document.documentElement

I agree with @dummdidumm It would be very valuable.


will it be possible to make this work with localStorage to prevent FOUC upon initial page load?

not sure how to describe the problem in the best way. but currently, if you want to configure the dark/light mode, you can achieve this with e.g. the mode-watcher package.

the mode-watcher inserts a stringified function into the head that sets the correct <html> class based on localStorage and/or prefers-color-scheme, basically something along these lines:

<svelte:head>
  {@html `<script>(` +
    setInitialMode.toString() +
    `)(` +
    args +
    `);</script>`}
</svelte:head>

where:

export function setInitialMode(defaultMode: Mode, themeColors?: ThemeColors) {
  const rootEl = document.documentElement;
  const mode = localStorage.getItem('mode-watcher-mode') || defaultMode;
  const light =
    mode === 'light' ||
    (mode === 'system' && window.matchMedia('(prefers-color-scheme: light)').matches);

  rootEl.classList[light ? 'remove' : 'add']('dark');
  rootEl.style.colorScheme = light ? 'light' : 'dark';

  localStorage.setItem('mode-watcher-mode', mode);
}

but this approach is a bit brittle and it would be great if it could be simplified!

it is also complicated if you want to use nonce, then you can not use the mode-watcher component out of the box

I think it would be possible to avoid FOUC in this use case given that:

At the very least, if cookies are used, the returning visitors can get the behavior without FOUC.


I created a playground with an emulated <svelte:html> via a component as a proof of concept. The main logic is in the SvelteHtml.svelte component and the rest is just for a visual demo. Just wanted to see how difficult it would be to support. Obviously, this is a contrived example outside of svelte's internal code, perhaps the logic might be useful.

  • The attributes are added in the order of component instantiation.
  • If a component is removed and it was overwriting its "predecessors" / parents' attributes, then the predecessor's attributes are restored.
  • Pre-existing attributes added outside of the emulated <svelte:html> remain unless any one of the components overwrites them. I don't think figuring out how to deal with restoring attributes outside of the <svelte:html> makes sense as it can result in various racing conditions.
  • setting an attribute to undefined or null, removes it from the html attributes, unless a component downstream provides some kind of value
  • setting attribute to an empty string, keeps the attribute, the browser just sets the attribute in this case.
  • class as a special case, attribute remains with the predecessor classes are restored on destroy; can be expanded to support style with a different string parser.

Playground: emulated <svelte:html>

- revert to previous value on unmount, if it was the last one changing the value
- special handling for classes: merge them
@dummdidumm
Copy link
Member Author

Updated the implementation to handle duplicates including always correctly reverting to the previous value upon removal, and handling classes as a special case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add <svelte:html> special element
4 participants