pull/8457/head
Simon Holthausen 3 years ago
parent d42ca041dd
commit 52f911cae4

@ -192,7 +192,7 @@ export default class Component {
this.pop_ignores();
this.elements.forEach(element => this.stylesheet.apply(element));
if (!compile_options.customElement) this.stylesheet.reify();
this.stylesheet.reify();
this.stylesheet.warn_on_unused_selectors(this);
}

@ -407,7 +407,7 @@ export default class Stylesheet {
});
}
render(file: string, should_transform_selectors: boolean) {
render(file: string) {
if (!this.has_styles) {
return { code: null, map: null };
}
@ -421,12 +421,10 @@ export default class Stylesheet {
}
});
if (should_transform_selectors) {
const max = Math.max(...this.children.map(rule => rule.get_max_amount_class_specificity_increased()));
this.children.forEach((child: (Atrule | Rule)) => {
child.transform(code, this.id, this.keyframes, max);
});
}
let c = 0;
this.children.forEach(child => {

@ -6,7 +6,7 @@ import { walk } from 'estree-walker';
import { extract_names, Scope } from 'periscopic';
import { invalidate } from './invalidate';
import Block from './Block';
import { ImportDeclaration, ClassDeclaration, FunctionExpression, Node, Statement, ObjectExpression, Expression } from 'estree';
import { ImportDeclaration, ClassDeclaration, Node, Statement, ObjectExpression, Expression } from 'estree';
import { apply_preprocessor_sourcemap } from '../../utils/mapped_code';
import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types';
import { flatten } from '../../utils/flatten';
@ -25,9 +25,6 @@ export default function dom(
block.has_outro_method = true;
// prevent fragment being created twice (#1063)
if (options.customElement) block.chunks.create.push(b`this.c = @noop;`);
const body = [];
if (renderer.file_var) {
@ -35,7 +32,7 @@ export default function dom(
body.push(b`const ${renderer.file_var} = ${file};`);
}
const css = component.stylesheet.render(options.filename, !options.customElement);
const css = component.stylesheet.render(options.filename);
const css_sourcemap_enabled = check_enable_sourcemap(options.enableSourcemap, 'css');
@ -52,7 +49,6 @@ export default function dom(
const add_css = component.get_unique_name('add_css');
const should_add_css = (
!options.customElement &&
!!styles &&
options.css === 'injected'
);
@ -519,8 +515,35 @@ export default function dom(
}
}
if (options.customElement) {
const superclass = {
type: 'Identifier',
name: options.dev ? '@SvelteComponentDev' : '@SvelteComponent'
};
const optional_parameters = [];
if (should_add_css) {
optional_parameters.push(add_css);
} else if (dirty) {
optional_parameters.push(x`null`);
}
if (dirty) {
optional_parameters.push(dirty);
}
const declaration = b`
class ${name} extends ${superclass} {
constructor(options) {
super(${options.dev && 'options'});
@init(this, options, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${optional_parameters});
${options.dev && b`@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });`}
}
}
`[0] as ClassDeclaration;
push_array(declaration.body.body, accessors);
body.push(declaration);
if (options.customElement && component.tag != null) {
let init_props = x`@attribute_to_object(this.attributes)`;
if (uses_slots) {
init_props = x`{ ...${init_props}, $$slots: @get_custom_elements_slots(this) }`;
@ -553,57 +576,15 @@ export default function dom(
}
`[0] as ClassDeclaration;
if (props.length > 0) {
declaration.body.body.push({
type: 'MethodDefinition',
kind: 'get',
static: true,
computed: false,
key: { type: 'Identifier', name: 'observedAttributes' },
value: x`function() {
return [${props.map(prop => x`"${prop.export_name}"`)}];
}` as FunctionExpression
});
}
push_array(declaration.body.body, accessors);
body.push(declaration);
if (component.tag != null) {
body.push(b`
@_customElements.define("${component.tag}", ${name});
`);
}
} else {
const superclass = {
type: 'Identifier',
name: options.dev ? '@SvelteComponentDev' : '@SvelteComponent'
};
const optional_parameters = [];
if (should_add_css) {
optional_parameters.push(add_css);
} else if (dirty) {
optional_parameters.push(x`null`);
}
if (dirty) {
optional_parameters.push(dirty);
}
const declaration = b`
class ${name} extends ${superclass} {
constructor(options) {
super(${options.dev && 'options'});
@init(this, options, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${optional_parameters});
${options.dev && b`@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });`}
}
}
`[0] as ClassDeclaration;
push_array(declaration.body.body, accessors);
body.push(declaration);
const props_str = writable_props.map(prop => `"${prop.export_name}"`).join(',');
const slots_str = [...component.slots.keys()].map(key => `"${key}"`).join(',');
const accessors_str = accessors
.filter(accessor => !writable_props.some(prop => prop.export_name === accessor.key.name))
.map(accessor => `"${accessor.key.name}"`)
.join(',');
body.push(
b`@_customElements.define("${component.tag}", @create_custom_element(${name}, [${props_str}], [${slots_str}], [${accessors_str}]));`
);
}
return { js: flatten(body), css };

@ -132,6 +132,10 @@ export default class SlotWrapper extends Wrapper {
const ${slot_definition} = ${renderer.reference('#slots')}.${slot_name};
const ${slot} = @create_slot(${slot_definition}, #ctx, ${renderer.reference('$$scope')}, ${get_slot_context_fn});
${has_fallback ? b`const ${slot_or_fallback} = ${slot} || ${this.fallback.name}(#ctx);` : null}
${has_fallback && this.renderer.options.customElement && this.renderer.options.tag
// This ensures that fallback content is rendered into the <slot> element given by the custom element wrapper
? b`if (${slot_or_fallback}.$$c_e) ${this.fallback.name}(#ctx);`
: null}
`);
block.chunks.create.push(

@ -33,7 +33,7 @@ export default function ssr(
// TODO concatenate CSS maps
const css = options.customElement ?
{ code: null, map: null } :
component.stylesheet.render(options.filename, true);
component.stylesheet.render(options.filename);
const uses_rest = component.var_lookup.has('$$restProps');
const props = component.vars.filter(variable => !variable.module && variable.export_name);

@ -115,7 +115,7 @@ export default function tag(parser: Parser) {
: (regex_capital_letter.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent'
: name === 'svelte:fragment' ? 'SlotTemplate'
: name === 'title' && parent_is_head(parser.stack) ? 'Title'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
: name === 'slot' ? 'Slot' : 'Element';
const element: TemplateNode = {
start,

@ -1,9 +1,10 @@
import { add_render_callback, flush, flush_render_callbacks, schedule_update, dirty_components } from './scheduler';
import { add_render_callback, flush, flush_render_callbacks, schedule_update, dirty_components, tick } from './scheduler';
import { current_component, set_current_component } from './lifecycle';
import { blank_object, is_empty, is_function, run, run_all, noop } from './utils';
import { children, detach, start_hydrating, end_hydrating } from './dom';
import { children, detach, start_hydrating, end_hydrating, set_custom_element_data, get_custom_elements_slots, insert } from './dom';
import { transition_in } from './transitions';
import { T$$ } from './types';
import { ComponentType } from './dev';
export function bind(component, name, callback) {
const index = component.$$.props[name];
@ -21,12 +22,11 @@ export function claim_component(block, parent_nodes) {
block && block.l(parent_nodes);
}
export function mount_component(component, target, anchor, customElement) {
export function mount_component(component, target, anchor) {
const { fragment, after_update } = component.$$;
fragment && fragment.m(target, anchor);
if (!customElement) {
// onMount happens before the initial afterUpdate
add_render_callback(() => {
@ -43,7 +43,6 @@ export function mount_component(component, target, anchor, customElement) {
}
component.$$.on_mount = [];
});
}
after_update.forEach(add_render_callback);
}
@ -137,7 +136,7 @@ export function init(component, options, instance, create_fragment, not_equal, p
}
if (options.intro) transition_in(component.$$.fragment);
mount_component(component, options.target, options.anchor, options.customElement);
mount_component(component, options.target, options.anchor);
end_hydrating();
flush();
}
@ -148,59 +147,202 @@ export function init(component, options, instance, create_fragment, not_equal, p
export let SvelteElement;
if (typeof HTMLElement === 'function') {
SvelteElement = class extends HTMLElement {
$$: T$$;
$$set?: ($$props: any) => void;
constructor() {
private $$component?: SvelteComponent;
private $$connected = false;
private $$data = {};
private $$reflecting = false;
constructor(
private $$componentCtor: ComponentType,
private $$slots: string[],
) {
super();
this.attachShadow({ mode: 'open' });
}
addEventListener(type: string, listener: any, options?: any): void {
// We can't determine upfront if the event is a custom event or not, so we have to
// listen to both. If someone uses a custom event with the same name as a regular
// browser event, this fires twice - we can't avoid that.
this.$$component!.$on(type, listener);
super.addEventListener(type, listener, options);
}
connectedCallback() {
const { on_mount } = this.$$;
this.$$.on_disconnect = on_mount.map(run).filter(is_function);
this.$$connected = true;
if (!this.$$component) {
for (const attribute of this.attributes) {
// this.$$data takes precedence over this.attributes
if (!(attribute.name in this.$$data)) {
this.$$data[attribute.name] = attribute.value;
}
}
function create_slot(name: string) {
return () => {
let node: HTMLSlotElement;
return {
c: function create() {
node = document.createElement('slot');
if (name !== 'default') {
node.setAttribute('name', name);
}
},
m: function mount(target: HTMLElement, anchor?: HTMLElement) {
insert(target, node, anchor);
},
d: function destroy(detaching: boolean) {
if (detaching) {
detach(node)
}
},
$$c_e: true
};
};
}
let $$slots: Record<string, any> = {};
const existing_slots = get_custom_elements_slots(this);
for (const name of this.$$slots) {
if (name in existing_slots) {
$$slots[name] = [create_slot(name)];
}
}
// @ts-ignore todo: improve typings
for (const key in this.$$.slotted) {
// @ts-ignore todo: improve typings
this.appendChild(this.$$.slotted[key]);
// Dilemma: We need to set the component props eagerly or they have the wrong value for actions/onMount etc.
// Boolean attributes are represented by the empty string, and we don't know if they represent boolean or string props.
this.$$component = new this.$$componentCtor({
target: this.shadowRoot!,
props: {
$$slots,
$$scope: {
ctx: []
}
}
});
// ensures that <foo-bar boolean-attribute /> works correctly
Object.keys(this.$$data).forEach(key => {
set_custom_element_data(this, key, this.$$data[key]);
this.$$data[key] = this[key]; // "" -> true for boolean attributes
});
}
}
attributeChangedCallback(attr, _oldValue, newValue) {
this[attr] = newValue;
// TODO we don't need this when working within Svelte code, but for compatibility of people using this outside of Svelte
// and setting attributes through setAttribute etc, this is probably helpful
attributeChangedCallback(attr: string, _oldValue: any, newValue: any) {
if (this.$$reflecting) return;
set_custom_element_data(this.$$data, attr, newValue);
this.$$component![attr] = this.$$data;
}
disconnectedCallback() {
run_all(this.$$.on_disconnect);
this.$$connected = false;
// In a microtask, because this could be a move within the DOM
tick().then(() => {
if (!this.$$connected) {
this.$$component!.$destroy();
this.$$component = undefined;
}
});
}
};
}
$destroy() {
destroy_component(this, 1);
this.$destroy = noop;
/**
* Attribute value types that should be reflected to the DOM. Helpful
* for people relying on the custom element's attributes to be present,
* for example when using a CSS selector which relies on an attribute.
*/
const should_reflect = ['string', 'number', 'boolean'];
function camelToHyphen(str: string) {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
$on(type, callback) {
// TODO should this delegate to addEventListener?
if (!is_function(callback)) {
return noop;
/**
* Turn a Svelte component into a custom element.
* @param Component A Svelte component constructor
* @param props The props to observe
* @param slots The slots to create
* @param accessors Other accessors besides the ones for props the component has
* @param styles Additional styles to apply to the shadow root (not needed for Svelte components compiled with `customElement: true`)
* @returns A custom element class
*/
export function create_custom_element(
Component: ComponentType,
props: string[],
slots: string[],
accessors: string[],
styles?: string,
) {
const Class = class extends SvelteElement {
constructor() {
super(Component, slots);
if (styles) {
const style = document.createElement('style');
style.textContent = styles;
this.shadowRoot!.appendChild(style);
}
}
const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
callbacks.push(callback);
return () => {
const index = callbacks.indexOf(callback);
if (index !== -1) callbacks.splice(index, 1);
static get observedAttributes() {
return props;
}
};
function createProperty(name: string, prop: string) {
Object.defineProperty(Class.prototype, name, {
get() {
return this.$$component && prop in this.$$component
? this.$$component[prop]
: this.$$data[prop];
},
set(value) {
this.$$data[prop] = value;
if (this.$$component) {
if(should_reflect.indexOf(typeof value) !== -1) {
this.$$reflecting = true;
if (value === false || value == null) {
this.removeAttribute(prop);
} else {
this.setAttribute(prop, value);
}
this.$$reflecting = false;
}
$set($$props) {
if (this.$$set && !is_empty($$props)) {
this.$$.skip_bound = true;
this.$$set($$props);
this.$$.skip_bound = false;
this.$$component[prop] = value;
}
}
};
})
}
props.forEach((prop) => {
createProperty(prop, prop);
// <c-e camelCase="foo" /> will be ce.camcelcase = "foo"
const lower = prop.toLowerCase();
if (lower !== prop) {
createProperty(lower, prop);
}
// also support hyphenated version where <c-e camel-case="foo" /> will be ce['camel-case'] = "foo"
const hyphen = camelToHyphen(prop);
if (hyphen !== lower) {
createProperty(hyphen, prop)
}
});
accessors.forEach(accessor => {
Object.defineProperty(Class.prototype, accessor, {
get() {
return this.$$component?.[accessor];
},
})
});
return Class;
}
/**

@ -0,0 +1,20 @@
<svelte:options tag="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,14 @@
import * as assert from 'assert';
import './main.svelte';
export default function (target) {
target.innerHTML = '<custom-element name="foo"></custom-element>';
const el = target.querySelector('custom-element');
assert.deepEqual(el.events, ['foo']);
el.name = 'bar';
assert.deepEqual(el.events, ['foo', 'bar']);
target.innerHTML = '';
assert.deepEqual(el.events, ['foo', 'bar', 'destroy']);
}

@ -1,11 +1,8 @@
import * as assert from 'assert';
import CustomElement from './main.svelte';
import './main.svelte';
export default function (target) {
new CustomElement({
target
});
target.innerHTML = '<custom-element></custom-element>';
const icon = target.querySelector('custom-element').shadowRoot.querySelector('.icon');
const before = getComputedStyle(icon, '::before');

@ -0,0 +1,9 @@
<svelte:options tag="custom-element" />
<script>
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
</script>
<button on:click={() => dispatch("custom", "foo")}>bubble click</button>

@ -0,0 +1,18 @@
import * as assert from 'assert';
import './main.svelte';
export default function (target) {
target.innerHTML = '<custom-element></custom-element>';
const el = target.querySelector('custom-element');
const events = [];
el.addEventListener('custom', e => {
events.push(e.detail);
});
el.addEventListener('click', () => {
events.push('click');
});
el.shadowRoot.querySelector('button').click();
assert.deepEqual(events, ['foo', 'click']);
}

@ -1,11 +1,8 @@
import * as assert from 'assert';
import CustomElement from './main.svelte';
import './main.svelte';
export default function (target) {
new CustomElement({
target
});
target.innerHTML = '<custom-element></custom-element>';
assert.equal(target.innerHTML, '<custom-element></custom-element>');
const el = target.querySelector('custom-element');

@ -13,5 +13,5 @@ export default function (target) {
const [slot0, slot1] = div.children;
assert.equal(slot0.assignedNodes()[1], target.querySelector('strong'));
assert.equal(slot1.assignedNodes().length, 0);
assert.equal(slot1.innerHTML, 'foo fallback content');
}

@ -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,14 @@
<svelte:options tag="my-counter" />
<script>
export let count = 0;
</script>
<slot />
<button on:click={() => (count += 1)}>count: {count}</button>
<style>
button {
color: red;
}
</style>

@ -0,0 +1,13 @@
<svelte:options tag="my-app" />
<script>
import Counter from "./Counter.svelte";
export let count;
export let counter;
</script>
<Counter bind:count bind:this={counter}>
<span>slot {count}</span>
</Counter>
<p>clicked {count} times</p>

@ -0,0 +1,20 @@
import * as assert from 'assert';
import './main.svelte';
export default async function (target) {
target.innerHTML = '<my-app/>';
const el = target.querySelector('my-app');
const button = el.shadowRoot.querySelector('button');
const span = el.shadowRoot.querySelector('span');
assert.equal(el.counter.count, 0);
assert.equal(button.innerHTML, 'count: 0');
assert.equal(span.innerHTML, 'slot 0');
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,12 +1,9 @@
import * as assert from 'assert';
import CustomElement from './main.svelte';
import './main.svelte';
export default function (target) {
target.innerHTML = '<p>unstyled</p>';
new CustomElement({
target
});
target.appendChild(document.createElement('custom-element'));
const unstyled = target.querySelector('p');
const styled = target.querySelector('custom-element').shadowRoot.querySelector('p');

@ -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!');
}

@ -1,8 +1,9 @@
import * as assert from 'assert';
import CustomElement from './main.svelte';
import { create_custom_element } from 'svelte/internal';
export default function (target) {
customElements.define('no-tag', CustomElement);
customElements.define('no-tag', create_custom_element(CustomElement, ['name'], [], []));
target.innerHTML = '<no-tag name="world"></no-tag>';
const el = target.querySelector('no-tag');

@ -1,8 +1,9 @@
import * as assert from 'assert';
import CustomElement from './main.svelte';
import { create_custom_element } from 'svelte/internal';
export default function (target) {
customElements.define('no-tag', CustomElement);
customElements.define('no-tag', create_custom_element(CustomElement, ['name'], [], []));
target.innerHTML = '<no-tag name="world"></no-tag>';
const el = target.querySelector('no-tag');

@ -1,8 +1,9 @@
import * as assert from 'assert';
import CustomElement from './main.svelte';
import { create_custom_element } from 'svelte/internal';
export default function (target) {
customElements.define('no-tag', CustomElement);
customElements.define('no-tag', create_custom_element(CustomElement, ['name'], [], []));
target.innerHTML = '<no-tag name="world"></no-tag>';
const el = target.querySelector('no-tag');

@ -1,7 +1,7 @@
<svelte:options tag="my-app" />
<script>
import { onMount } from 'svelte';
import { onMount } from "svelte";
export let prop = false;
export let propsInitialized;

@ -1,10 +1,13 @@
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/>';
const el = target.querySelector('my-app');
await tick();
assert.ok(el.wasCreated);
assert.ok(el.propsInitialized);
}

@ -1,11 +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/>';
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);
}

@ -1,10 +1,8 @@
import * as assert from 'assert';
import CustomElement from './main.svelte';
import './main.svelte';
export default function (target) {
new CustomElement({
target
});
target.innerHTML = '<custom-element></custom-element>';
assert.equal(target.innerHTML, '<custom-element></custom-element>');

@ -0,0 +1,20 @@
<svelte:options tag="custom-element" />
<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,18 @@
<svelte:options tag="my-widget" />
<script>
export let red;
red;
</script>
<div>hi</div>
<p>hi</p>
<style>
:host([red]) div {
color: red;
}
:host([white]) p {
color: white;
}
</style>

@ -0,0 +1,19 @@
import * as assert from 'assert';
import './main.svelte';
export default function (target) {
target.innerHTML = '<custom-element red white></custom-element>';
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,6 +1,7 @@
{
"extends": "../tsconfig.json",
"include": ["."],
"exclude": ["./**/_output/**/*"],
"compilerOptions": {
"allowJs": true,

Loading…
Cancel
Save