Skip to content

feat: allow dom elements as svelte:element this attribute #15477

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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/loud-mayflies-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: allow dom elements as `svelte:element` `this` attribute
6 changes: 6 additions & 0 deletions documentation/docs/98-reference/.generated/client-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,9 @@ Reading state that was created inside the same derived is forbidden. Consider us
```
Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
```

### svelte_element_already_connected

```
You can't use an HTML element as the `this` attribute of `svelte:element` if it's already connected to a document
```
4 changes: 4 additions & 0 deletions packages/svelte/messages/client-errors/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,7 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
## state_unsafe_mutation

> Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`

## svelte_element_already_connected

> You can't use an HTML element as the `this` attribute of `svelte:element` if it's already connected to a document
30 changes: 22 additions & 8 deletions packages/svelte/src/internal/client/dom/blocks/svelte-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ import { DEV } from 'esm-env';
import { EFFECT_TRANSPARENT } from '../../constants.js';
import { assign_nodes } from '../template.js';
import { is_raw_text_element } from '../../../../utils.js';
import * as e from '../../errors.js';

/**
* @param {Comment | Element} node
* @param {() => string} get_tag
* @param {() => string | HTMLElement | SVGElement} get_tag
* @param {boolean} is_svg
* @param {undefined | ((element: Element, anchor: Node | null) => void)} render_fn,
* @param {undefined | (() => string)} get_namespace
Expand All @@ -42,10 +43,10 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio

var filename = DEV && location && component_context?.function[FILENAME];

/** @type {string | null} */
/** @type {string | HTMLElement | SVGElement | null} */
var tag;

/** @type {string | null} */
/** @type {string | HTMLElement | SVGElement | null} */
var current_tag;

/** @type {null | Element} */
Expand All @@ -70,11 +71,19 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio

block(() => {
const next_tag = get_tag() || null;
var ns = get_namespace ? get_namespace() : is_svg || next_tag === 'svg' ? NAMESPACE_SVG : null;
var ns = get_namespace
? get_namespace()
: is_svg || (typeof next_tag === 'string' ? next_tag === 'svg' : next_tag?.tagName === 'svg')
? NAMESPACE_SVG
: null;

// Assumption: Noone changes the namespace but not the tag (what would that even mean?)
if (next_tag === tag) return;

if (typeof next_tag !== 'string' && next_tag?.isConnected) {
e.svelte_element_already_connected();
}

// See explanation of `each_item_block` above
var previous_each_item = current_each_item;
set_current_each_item(each_item_block);
Expand All @@ -100,9 +109,11 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
effect = branch(() => {
element = hydrating
? /** @type {Element} */ (element)
: ns
? document.createElementNS(ns, next_tag)
: document.createElement(next_tag);
: typeof next_tag === 'string'
? ns
? document.createElementNS(ns, next_tag)
: document.createElement(next_tag)
: next_tag;

if (DEV && location) {
// @ts-expect-error
Expand All @@ -118,7 +129,10 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
assign_nodes(element, element);

if (render_fn) {
if (hydrating && is_raw_text_element(next_tag)) {
if (
hydrating &&
is_raw_text_element(typeof next_tag === 'string' ? next_tag : next_tag.nodeName)
) {
// prevent hydration glitches
element.append(document.createComment(''));
}
Expand Down
15 changes: 15 additions & 0 deletions packages/svelte/src/internal/client/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -335,4 +335,19 @@ export function state_unsafe_mutation() {
} else {
throw new Error(`https://svelte.dev/e/state_unsafe_mutation`);
}
}

/**
* You can't use an HTML element as the `this` attribute of `svelte:element` if it's already connected to a document
* @returns {never}
*/
export function svelte_element_already_connected() {
if (DEV) {
const error = new Error(`svelte_element_already_connected\nYou can't use an HTML element as the \`this\` attribute of \`svelte:element\` if it's already connected to a document\nhttps://svelte.dev/e/svelte_element_already_connected`);

error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/svelte_element_already_connected`);
}
}
10 changes: 6 additions & 4 deletions packages/svelte/src/internal/shared/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,23 @@ import * as e from './errors.js';
export { invalid_default_snippet } from './errors.js';

/**
* @param {() => string} tag_fn
* @param {() => string | HTMLElement | SVGElement} tag_fn
* @returns {void}
*/
export function validate_void_dynamic_element(tag_fn) {
const tag = tag_fn();
if (tag && is_void(tag)) {
w.dynamic_void_element_content(tag);
const tag_name = typeof tag === 'string' ? tag : tag?.tagName;
if (tag_name && is_void(tag_name)) {
w.dynamic_void_element_content(tag_name);
}
}

/** @param {() => unknown} tag_fn */
export function validate_dynamic_element_tag(tag_fn) {
const tag = tag_fn();
const is_string = typeof tag === 'string';
if (tag && !is_string) {
const is_element = tag instanceof HTMLElement || tag instanceof SVGElement;
if (tag && !(is_string || is_element)) {
e.svelte_element_invalid_this_value();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { flushSync } from 'svelte';
import { test } from '../../test';

export default test({
test({ assert, target }) {
const btn = target.querySelector('button');
assert.throws(() => {
flushSync(() => {
btn?.click();
});
}, 'svelte_element_already_connected');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script>
let div = $state(null);
let show = $state(false);
</script>

<div bind:this={div}></div>
<button onclick={() => show = !show}></button>
<svelte:element this={show && div}>
</svelte:element>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { test } from '../../test';

export default test({
ssrHtml: ``,
test({ assert, target }) {
assert.htmlEqual(target.innerHTML, `<div><b>children</b><p>children</p></div>`);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script>
let div = $state(null);

$effect(()=>{
const to_add = document.createElement("div");
to_add.innerHTML=`<b>children</b>`;
div = to_add;
})
</script>

<svelte:element this={div}>
<p>children</p>
</svelte:element>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { test } from '../../test';

export default test({
ssrHtml: `<main><p>children</p></main>`,
test({ assert, target }) {
assert.htmlEqual(target.innerHTML, `<div><b>children</b><p>children</p></div>`);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script>
let div = $state(null);

$effect(()=>{
const to_add = document.createElement("div");
to_add.innerHTML=`<b>children</b>`;
div = to_add;
})
</script>

<svelte:element this={div ?? "main"}>
<p>children</p>
</svelte:element>