mirror of https://github.com/sveltejs/svelte
feat: custom elements rework (#8457)
This is an overhaul of custom elements in Svelte. Instead of compiling to a custom element class, the Svelte component class is mostly preserved as-is. Instead a wrapper is introduced which wraps a Svelte component constructor and returns a HTML element constructor. This has a couple of advantages: - component can be used both as a custom element as well as a regular component. This allows creating one wrapper custom element and using regular Svelte components inside. Fixes #3594, fixes #3128, fixes #4274, fixes #5486, fixes #3422, fixes #2969, helps with https://github.com/sveltejs/kit/issues/4502 - all components are compiled with injected styles (inlined through Javascript), fixes #4274 - the wrapper instantiates the component in `connectedCallback` and disconnects it in `disconnectedCallback` (but only after one tick, because this could be a element move). Mount/destroy works as expected inside, fixes #5989, fixes #8191 - the wrapper forwards `addEventListener` calls to `component.$on`, which allows to listen to custom events, fixes #3119, closes #4142 - some things are hard to auto-configure, like attribute hyphen preferences or whether or not setting a property should reflect back to the attribute. This is why `<svelte:options customElement={..}>` can also take an object to modify such aspects. This option allows to specify whether setting a prop should be reflected back to the attribute (default `false`), what to use when converting the property to the attribute value and vice versa (through `type`, default `String`, or when `export let prop = false` then `Boolean`), and what the corresponding attribute for the property is (`attribute`, default lowercased prop name). These options are heavily inspired by lit: https://lit.dev/docs/components/properties. Closes #7638, fixes #5705 - adds a `shadowdom` option to control whether or not encapsulate the custom element. Closes #4330, closes #1748 Breaking changes: - Wrapped Svelte component now stays as a regular Svelte component (invokeing it like before with `new Component({ target: ..})` won't create a custom element). Its custom element constructor is now a static property named `element` on the class (`Component.element`) and should be regularly invoked through setting it in the html. - The timing of mount/destroy/update is different. Mount/destroy/updating a prop all happen after a tick, so `shadowRoot.innerHTML` won't immediately reflect the change (Lit does this too). If you rely on it, you need to await a promisepull/8566/head
parent
0677d89fff
commit
d083f8a3f2
@ -0,0 +1,10 @@
|
||||
<svelte:options customElement={null} />
|
||||
|
||||
<script>
|
||||
import "./my-widget.svelte";
|
||||
export let name;
|
||||
</script>
|
||||
|
||||
<my-widget>
|
||||
<p>default {name}</p>
|
||||
</my-widget>
|
@ -0,0 +1,4 @@
|
||||
<svelte:options customElement="my-widget" />
|
||||
|
||||
<slot>fallback</slot>
|
||||
<slot name="named"><p>named fallback</p></slot>
|
@ -0,0 +1,22 @@
|
||||
import * as assert from 'assert';
|
||||
import { tick } from 'svelte';
|
||||
import Component from './main.svelte';
|
||||
|
||||
export default async function (target) {
|
||||
const component = new Component({ target, props: { name: 'slot' } });
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
const ce = target.querySelector('my-widget');
|
||||
|
||||
assert.htmlEqual(ce.shadowRoot.innerHTML, `
|
||||
<slot></slot>
|
||||
<p>named fallback</p>
|
||||
`);
|
||||
|
||||
component.name = 'slot2';
|
||||
assert.htmlEqual(ce.shadowRoot.innerHTML, `
|
||||
<slot></slot>
|
||||
<p>named fallback</p>
|
||||
`);
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<svelte:options customElement="custom-element" />
|
||||
|
||||
<script>
|
||||
export let name;
|
||||
export let events = [];
|
||||
|
||||
function action(_node, name) {
|
||||
events.push(name);
|
||||
return {
|
||||
update(name) {
|
||||
events.push(name);
|
||||
},
|
||||
destroy() {
|
||||
events.push("destroy");
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div use:action={name}>action</div>
|
@ -0,0 +1,19 @@
|
||||
import * as assert from 'assert';
|
||||
import { tick } from 'svelte';
|
||||
import './main.svelte';
|
||||
|
||||
export default async function (target) {
|
||||
target.innerHTML = '<custom-element name="foo"></custom-element>';
|
||||
await tick();
|
||||
const el = target.querySelector('custom-element');
|
||||
const events = el.events; // need to get the array reference, else it's gone when destroyed
|
||||
assert.deepEqual(events, ['foo']);
|
||||
|
||||
el.name = 'bar';
|
||||
await tick();
|
||||
assert.deepEqual(events, ['foo', 'bar']);
|
||||
|
||||
target.innerHTML = '';
|
||||
await tick();
|
||||
assert.deepEqual(events, ['foo', 'bar', 'destroy']);
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
<svelte:options
|
||||
customElement={{
|
||||
tag: "custom-element",
|
||||
props: {
|
||||
camelCase: { attribute: "camel-case" },
|
||||
camelCase2: { reflect: true },
|
||||
anArray: { attribute: "an-array", type: "Array", reflect: true },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<script>
|
||||
export let camelCase;
|
||||
export let camelCase2;
|
||||
export let anArray;
|
||||
</script>
|
||||
|
||||
<h1>{camelCase2} {camelCase}!</h1>
|
||||
{#each anArray as item}
|
||||
<p>{item}</p>
|
||||
{/each}
|
@ -0,0 +1,25 @@
|
||||
import * as assert from 'assert';
|
||||
import { tick } from 'svelte';
|
||||
import './main.svelte';
|
||||
|
||||
export default async function (target) {
|
||||
target.innerHTML = '<custom-element camelcase2="Hello" camel-case="world" an-array="[1,2]"></custom-element>';
|
||||
await tick();
|
||||
const el = target.querySelector('custom-element');
|
||||
|
||||
assert.equal(el.shadowRoot.innerHTML, '<h1>Hello world!</h1> <p>1</p><p>2</p>');
|
||||
|
||||
el.setAttribute('camel-case', 'universe');
|
||||
el.setAttribute('an-array', '[3,4]');
|
||||
el.setAttribute('camelcase2', 'Hi');
|
||||
await tick();
|
||||
assert.equal(el.shadowRoot.innerHTML, '<h1>Hi universe!</h1> <p>3</p><p>4</p>');
|
||||
assert.equal(target.innerHTML, '<custom-element camelcase2="Hi" camel-case="universe" an-array="[3,4]"></custom-element>');
|
||||
|
||||
el.camelCase = 'galaxy';
|
||||
el.camelCase2 = 'Hey';
|
||||
el.anArray = [5, 6];
|
||||
await tick();
|
||||
assert.equal(el.shadowRoot.innerHTML, '<h1>Hey galaxy!</h1> <p>5</p><p>6</p>');
|
||||
assert.equal(target.innerHTML, '<custom-element camelcase2="Hey" camel-case="universe" an-array="[5,6]"></custom-element>');
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
<svelte:options
|
||||
customElement={{
|
||||
tag: "custom-element",
|
||||
props: {
|
||||
name: { reflect: false, type: "String", attribute: "name" },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<script>
|
||||
export let name;
|
||||
</script>
|
||||
|
||||
<h1>Hello {name}!</h1>
|
@ -0,0 +1,13 @@
|
||||
import * as assert from 'assert';
|
||||
import { tick } from 'svelte';
|
||||
import './main.svelte';
|
||||
|
||||
export default async function (target) {
|
||||
target.innerHTML = '<custom-element name="world"></custom-element>';
|
||||
await tick();
|
||||
|
||||
const el = target.querySelector('custom-element');
|
||||
const h1 = el.shadowRoot.querySelector('h1');
|
||||
|
||||
assert.equal(h1.textContent, 'Hello world!');
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
<svelte:options tag="custom-element"/>
|
||||
<svelte:options customElement="custom-element" />
|
||||
|
||||
<span class='icon'></span>
|
||||
<span class="icon" />
|
||||
|
||||
<style>
|
||||
.icon::before {
|
||||
content: '\ff'
|
||||
content: "\ff";
|
||||
}
|
||||
</style>
|
||||
|
@ -0,0 +1,9 @@
|
||||
<svelte:options customElement="custom-element" />
|
||||
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
<button on:click={() => dispatch("custom", "foo")}>bubble click</button>
|
@ -0,0 +1,35 @@
|
||||
import * as assert from 'assert';
|
||||
import { tick } from 'svelte';
|
||||
import './main.svelte';
|
||||
|
||||
export default async function (target) {
|
||||
target.innerHTML = '<custom-element></custom-element>';
|
||||
const el = target.querySelector('custom-element');
|
||||
|
||||
const events = [];
|
||||
const custom_before = () => {
|
||||
events.push('before');
|
||||
};
|
||||
const click_before = () => {
|
||||
events.push('click_before');
|
||||
};
|
||||
el.addEventListener('custom', custom_before);
|
||||
el.addEventListener('click', click_before);
|
||||
|
||||
await tick();
|
||||
|
||||
el.addEventListener('custom', e => {
|
||||
events.push(e.detail);
|
||||
});
|
||||
el.addEventListener('click', () => {
|
||||
events.push('click');
|
||||
});
|
||||
|
||||
el.shadowRoot.querySelector('button').click();
|
||||
assert.deepEqual(events, ['before', 'foo', 'click_before', 'click']);
|
||||
|
||||
el.removeEventListener('custom', custom_before);
|
||||
el.removeEventListener('click', click_before);
|
||||
el.shadowRoot.querySelector('button').click();
|
||||
assert.deepEqual(events, ['before', 'foo', 'click_before', 'click', 'foo', 'click']);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
<svelte:options tag="custom-element"/>
|
||||
<svelte:options customElement="custom-element" />
|
||||
|
||||
<script>
|
||||
import './custom-button.js';
|
||||
import "./custom-button.js";
|
||||
</script>
|
||||
|
||||
<button is="custom-button">click me</button>
|
||||
<button is="custom-button">click me</button>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<svelte:options tag="custom-element"/>
|
||||
<svelte:options customElement="custom-element" />
|
||||
|
||||
<div>
|
||||
<slot>
|
||||
<p>default fallback content</p>
|
||||
</slot>
|
||||
|
||||
<slot name='foo'>
|
||||
<slot name="foo">
|
||||
<p>foo fallback content</p>
|
||||
</slot>
|
||||
</div>
|
||||
|
@ -1,7 +0,0 @@
|
||||
<svelte:options tag="my-counter"/>
|
||||
|
||||
<script>
|
||||
export let count = 0;
|
||||
</script>
|
||||
|
||||
<button on:click='{() => count += 1}'>count: {count}</button>
|
@ -1,10 +0,0 @@
|
||||
<svelte:options tag="my-app"/>
|
||||
|
||||
<script>
|
||||
import Counter from './Counter.svelte';
|
||||
|
||||
export let count;
|
||||
</script>
|
||||
|
||||
<Counter bind:count/>
|
||||
<p>clicked {count} times</p>
|
@ -1,17 +0,0 @@
|
||||
import * as assert from 'assert';
|
||||
import './main.svelte';
|
||||
|
||||
export default async function (target) {
|
||||
target.innerHTML = '<my-app/>';
|
||||
const el = target.querySelector('my-app');
|
||||
const counter = el.shadowRoot.querySelector('my-counter');
|
||||
const button = counter.shadowRoot.querySelector('button');
|
||||
|
||||
assert.equal(counter.count, 0);
|
||||
assert.equal(counter.shadowRoot.innerHTML, '<button>count: 0</button>');
|
||||
|
||||
await button.dispatchEvent(new MouseEvent('click'));
|
||||
|
||||
assert.equal(counter.count, 1);
|
||||
assert.equal(counter.shadowRoot.innerHTML, '<button>count: 1</button>');
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<svelte:options customElement="my-counter" />
|
||||
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
|
||||
export let count = 0;
|
||||
|
||||
const context = getContext("context");
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
<button on:click={() => (count += 1)}>count: {count}</button>
|
||||
<p>Context {context}</p>
|
||||
|
||||
<style>
|
||||
button {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,16 @@
|
||||
<svelte:options customElement="my-app" />
|
||||
|
||||
<script>
|
||||
import { setContext } from "svelte";
|
||||
import Counter from "./Counter.svelte";
|
||||
|
||||
export let count;
|
||||
export let counter;
|
||||
|
||||
setContext("context", "works");
|
||||
</script>
|
||||
|
||||
<Counter bind:count bind:this={counter}>
|
||||
<span>slot {count}</span>
|
||||
</Counter>
|
||||
<p>clicked {count} times</p>
|
@ -0,0 +1,24 @@
|
||||
import * as assert from 'assert';
|
||||
import { tick } from 'svelte';
|
||||
import './main.svelte';
|
||||
|
||||
export default async function (target) {
|
||||
target.innerHTML = '<my-app/>';
|
||||
await tick();
|
||||
const el = target.querySelector('my-app');
|
||||
const button = el.shadowRoot.querySelector('button');
|
||||
const span = el.shadowRoot.querySelector('span');
|
||||
const p = el.shadowRoot.querySelector('p');
|
||||
|
||||
assert.equal(el.counter.count, 0);
|
||||
assert.equal(button.innerHTML, 'count: 0');
|
||||
assert.equal(span.innerHTML, 'slot 0');
|
||||
assert.equal(p.innerHTML, 'Context works');
|
||||
assert.equal(getComputedStyle(button).color, 'rgb(255, 0, 0)');
|
||||
|
||||
await button.dispatchEvent(new MouseEvent('click'));
|
||||
|
||||
assert.equal(el.counter.count, 1);
|
||||
assert.equal(button.innerHTML, 'count: 1');
|
||||
assert.equal(span.innerHTML, 'slot 1');
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
<svelte:options tag="custom-element"/>
|
||||
|
||||
<script>
|
||||
export let name;
|
||||
</script>
|
||||
|
||||
<h1>Hello {name}!</h1>
|
@ -1,18 +0,0 @@
|
||||
import * as assert from 'assert';
|
||||
import CustomElement from './main.svelte';
|
||||
|
||||
export default function (target) {
|
||||
new CustomElement({
|
||||
target,
|
||||
props: {
|
||||
name: 'world'
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(target.innerHTML, '<custom-element></custom-element>');
|
||||
|
||||
const el = target.querySelector('custom-element');
|
||||
const h1 = el.shadowRoot.querySelector('h1');
|
||||
|
||||
assert.equal(h1.textContent, 'Hello world!');
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<svelte:options customElement={{ tag: "custom-element", shadow: "none" }} />
|
||||
|
||||
<script>
|
||||
export let name;
|
||||
</script>
|
||||
|
||||
<h1>Hello {name}!</h1>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,16 @@
|
||||
import * as assert from 'assert';
|
||||
import { tick } from 'svelte';
|
||||
import './main.svelte';
|
||||
|
||||
export default async function (target) {
|
||||
target.innerHTML = '<custom-element name="world"></custom-element>';
|
||||
await tick();
|
||||
|
||||
const el = target.querySelector('custom-element');
|
||||
const h1 = el.querySelector('h1');
|
||||
|
||||
assert.equal(el.name, 'world');
|
||||
assert.equal(el.shadowRoot, null);
|
||||
assert.equal(h1.innerHTML, 'Hello world!');
|
||||
assert.equal(getComputedStyle(h1).color, 'rgb(255, 0, 0)');
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
export default {
|
||||
warnings: [{
|
||||
code: 'custom-element-no-tag',
|
||||
message: "No custom element 'tag' option was specified. To automatically register a custom element, specify a name with a hyphen in it, e.g. <svelte:options tag=\"my-thing\"/>. To hide this warning, use <svelte:options tag={null}/>",
|
||||
pos: 0,
|
||||
start: {
|
||||
character: 0,
|
||||
column: 0,
|
||||
line: 1
|
||||
},
|
||||
end: {
|
||||
character: 0,
|
||||
column: 0,
|
||||
line: 1
|
||||
}
|
||||
}]
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
<script>
|
||||
export let name;
|
||||
</script>
|
||||
|
||||
<h1>Hello {name}!</h1>
|
@ -1,12 +0,0 @@
|
||||
import * as assert from 'assert';
|
||||
import CustomElement from './main.svelte';
|
||||
|
||||
export default function (target) {
|
||||
customElements.define('no-tag', CustomElement);
|
||||
target.innerHTML = '<no-tag name="world"></no-tag>';
|
||||
|
||||
const el = target.querySelector('no-tag');
|
||||
const h1 = el.shadowRoot.querySelector('h1');
|
||||
|
||||
assert.equal(h1.textContent, 'Hello world!');
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
export default {
|
||||
warnings: [{
|
||||
code: 'custom-element-no-tag',
|
||||
message: "No custom element 'tag' option was specified. To automatically register a custom element, specify a name with a hyphen in it, e.g. <svelte:options tag=\"my-thing\"/>. To hide this warning, use <svelte:options tag={null}/>",
|
||||
pos: 0,
|
||||
start: {
|
||||
character: 0,
|
||||
column: 0,
|
||||
line: 1
|
||||
},
|
||||
end: {
|
||||
character: 18,
|
||||
column: 18,
|
||||
line: 1
|
||||
}
|
||||
}]
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
<svelte:options />
|
||||
|
||||
<script>
|
||||
export let name;
|
||||
</script>
|
||||
|
||||
<h1>Hello {name}!</h1>
|
@ -1,12 +0,0 @@
|
||||
import * as assert from 'assert';
|
||||
import CustomElement from './main.svelte';
|
||||
|
||||
export default function (target) {
|
||||
customElements.define('no-tag', CustomElement);
|
||||
target.innerHTML = '<no-tag name="world"></no-tag>';
|
||||
|
||||
const el = target.querySelector('no-tag');
|
||||
const h1 = el.shadowRoot.querySelector('h1');
|
||||
|
||||
assert.equal(h1.textContent, 'Hello world!');
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
<svelte:options tag="my-app"/>
|
||||
<svelte:options customElement="my-app" />
|
||||
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let prop = false;
|
||||
export let propsInitialized;
|
||||
export let wasCreated;
|
||||
export let prop = false;
|
||||
export let propsInitialized;
|
||||
export let wasCreated;
|
||||
|
||||
onMount(() => {
|
||||
propsInitialized = prop !== false;
|
||||
wasCreated = true;
|
||||
});
|
||||
onMount(() => {
|
||||
propsInitialized = prop !== false;
|
||||
wasCreated = true;
|
||||
});
|
||||
</script>
|
||||
|
@ -1,10 +1,14 @@
|
||||
import * as assert from 'assert';
|
||||
import { tick } from 'svelte';
|
||||
import './main.svelte';
|
||||
|
||||
export default function (target) {
|
||||
export default async function (target) {
|
||||
target.innerHTML = '<my-app prop/>';
|
||||
await tick();
|
||||
const el = target.querySelector('my-app');
|
||||
|
||||
await tick();
|
||||
|
||||
assert.ok(el.wasCreated);
|
||||
assert.ok(el.propsInitialized);
|
||||
}
|
||||
|
@ -1,11 +1,15 @@
|
||||
import * as assert from 'assert';
|
||||
import { tick } from 'svelte';
|
||||
import './main.svelte';
|
||||
|
||||
export default function (target) {
|
||||
export default async function (target) {
|
||||
target.innerHTML = '<my-app/>';
|
||||
await tick();
|
||||
const el = target.querySelector('my-app');
|
||||
target.removeChild(el);
|
||||
|
||||
await tick();
|
||||
|
||||
assert.ok(target.dataset.onMountDestroyed);
|
||||
assert.equal(target.dataset.destroyed, undefined);
|
||||
assert.ok(target.dataset.destroyed);
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
<svelte:options
|
||||
customElement={{
|
||||
tag: "custom-element",
|
||||
props: { red: { reflect: true, type: "Boolean" } },
|
||||
}}
|
||||
/>
|
||||
|
||||
<script>
|
||||
import "./my-widget.svelte";
|
||||
export let red;
|
||||
red;
|
||||
</script>
|
||||
|
||||
<div>hi</div>
|
||||
<p>hi</p>
|
||||
<my-widget red white />
|
||||
|
||||
<style>
|
||||
:host([red]) div {
|
||||
color: red;
|
||||
}
|
||||
:host([white]) p {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,23 @@
|
||||
<svelte:options
|
||||
customElement={{
|
||||
tag: "my-widget",
|
||||
props: { red: { reflect: true } },
|
||||
}}
|
||||
/>
|
||||
|
||||
<script>
|
||||
export let red = false;
|
||||
red;
|
||||
</script>
|
||||
|
||||
<div>hi</div>
|
||||
<p>hi</p>
|
||||
|
||||
<style>
|
||||
:host([red]) div {
|
||||
color: red;
|
||||
}
|
||||
:host([white]) p {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,22 @@
|
||||
import * as assert from 'assert';
|
||||
import { tick } from 'svelte';
|
||||
import './main.svelte';
|
||||
|
||||
export default async function (target) {
|
||||
target.innerHTML = '<custom-element red white></custom-element>';
|
||||
await tick();
|
||||
await tick();
|
||||
const ceRoot = target.querySelector('custom-element').shadowRoot;
|
||||
const div = ceRoot.querySelector('div');
|
||||
const p = ceRoot.querySelector('p');
|
||||
|
||||
assert.equal(getComputedStyle(div).color, 'rgb(255, 0, 0)');
|
||||
assert.equal(getComputedStyle(p).color, 'rgb(255, 255, 255)');
|
||||
|
||||
const innerRoot = ceRoot.querySelector('my-widget').shadowRoot;
|
||||
const innerDiv = innerRoot.querySelector('div');
|
||||
const innerP = innerRoot.querySelector('p');
|
||||
|
||||
assert.equal(getComputedStyle(innerDiv).color, 'rgb(255, 0, 0)');
|
||||
assert.equal(getComputedStyle(innerP).color, 'rgb(255, 255, 255)');
|
||||
}
|
@ -1 +1 @@
|
||||
<svelte:options tag="custom-element" />
|
||||
<svelte:options customElement="custom-element" />
|
||||
|
@ -1 +1 @@
|
||||
<svelte:options tag="custom-element"/>
|
||||
<svelte:options customElement="custom-element" />
|
||||
|
@ -1,12 +1,12 @@
|
||||
[{
|
||||
"code": "missing-custom-element-compile-options",
|
||||
"message": "The 'tag' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?",
|
||||
"message": "The 'customElement' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?",
|
||||
"start": {
|
||||
"line": 1,
|
||||
"column": 16
|
||||
},
|
||||
"end": {
|
||||
"line": 1,
|
||||
"column": 36
|
||||
"column": 46
|
||||
}
|
||||
}]
|
||||
|
@ -1 +1 @@
|
||||
<svelte:options tag="custom-element"/>
|
||||
<svelte:options customElement="custom-element" />
|
||||
|
@ -1 +1 @@
|
||||
<svelte:options tag="invalid"/>
|
||||
<svelte:options customElement="invalid" />
|
||||
|
Loading…
Reference in new issue