diff --git a/src/compile/Component.ts b/src/compile/Component.ts
index 66b9bc4849..09b7193f6f 100644
--- a/src/compile/Component.ts
+++ b/src/compile/Component.ts
@@ -133,7 +133,7 @@ export default class Component {
this.walk_module_js();
this.walk_instance_js();
- this.name = this.alias(name);
+ this.name = this.getUniqueName(name);
this.meta = process_meta(this, this.ast.html.children);
this.namespace = namespaces[this.meta.namespace] || this.meta.namespace;
@@ -209,7 +209,7 @@ export default class Component {
});
const importedHelpers = Array.from(helpers)
- .concat(options.dev ? '$$ComponentDev' : '$$Component')
+ .concat(options.dev ? 'SvelteComponentDev' : 'SvelteComponent')
.sort()
.map(name => {
const alias = this.alias(name);
diff --git a/src/compile/render-dom/index.ts b/src/compile/render-dom/index.ts
index e2baf28267..61f9186e8b 100644
--- a/src/compile/render-dom/index.ts
+++ b/src/compile/render-dom/index.ts
@@ -58,150 +58,143 @@ export default function dom(
const expectedProperties = Array.from(component.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
- if (component.customElement) {
- // TODO use `export` to determine this
- const props = Array.from(component.expectedProperties);
+ const refs = Array.from(component.refs);
- builder.addBlock(deindent`
- class ${name} extends HTMLElement {
- constructor(options = {}) {
- super();
- }
-
- static get observedAttributes() {
- return ${JSON.stringify(props)};
- }
+ const superclass = component.alias(options.dev ? 'SvelteComponentDev' : 'SvelteComponent');
- ${renderer.slots.size && deindent`
- connectedCallback() {
- Object.keys(this.$$.slotted).forEach(key => {
- this.appendChild(this.$$.slotted[key]);
- });
- }`}
+ if (options.dev && !options.hydratable) {
+ block.builders.claim.addLine(
+ 'throw new Error("options.hydrate only works if the component was compiled with the `hydratable: true` option");'
+ );
+ }
- attributeChangedCallback(attr, oldValue, newValue) {
- this[attr] = newValue;
- }
+ // TODO injecting CSS this way is kinda dirty. Maybe it should be an
+ // explicit opt-in, or something?
+ const should_add_css = (
+ !component.options.customElement &&
+ component.stylesheet.hasStyles &&
+ options.css !== false
+ );
+
+ const props = component.exports.filter(x => component.writable_declarations.has(x.name));
+
+ const set = component.meta.props || props.length > 0
+ ? deindent`
+ $$props => {
+ ${component.meta.props && deindent`
+ if (!${component.meta.props}) ${component.meta.props} = {};
+ @assign(${component.meta.props}, $$props);
+ $$make_dirty('${component.meta.props_object}');
+ `}
+ ${props.map(prop =>
+ `if ('${prop.as}' in $$props) ${prop.name} = $$props.${prop.as};`)}
}
+ `
+ : null;
- customElements.define("${component.customElement.tag}", ${name});
- `);
- } else {
- const refs = Array.from(component.refs);
-
- const superclass = component.alias(options.dev ? '$$ComponentDev' : '$$Component');
-
- if (options.dev && !options.hydratable) {
- block.builders.claim.addLine(
- 'throw new Error("options.hydrate only works if the component was compiled with the `hydratable: true` option");'
- );
- }
+ const inject_refs = refs.length > 0
+ ? deindent`
+ $$refs => {
+ ${refs.map(name => `${name} = $$refs.${name};`)}
+ }
+ `
+ : null;
- // TODO injecting CSS this way is kinda dirty. Maybe it should be an
- // explicit opt-in, or something?
- const should_add_css = (
- !component.options.customElement &&
- component.stylesheet.hasStyles &&
- options.css !== false
- );
+ const body = [];
- const props = component.exports.filter(x => component.writable_declarations.has(x.name));
-
- const set = component.meta.props || props.length > 0
- ? deindent`
- $$props => {
- ${component.meta.props && deindent`
- if (!${component.meta.props}) ${component.meta.props} = {};
- @assign(${component.meta.props}, $$props);
- $$make_dirty('${component.meta.props_object}');
- `}
- ${props.map(prop =>
- `if ('${prop.as}' in $$props) ${prop.name} = $$props.${prop.as};`)}
- }
- `
- : null;
+ const debug_name = `<${component.customElement ? component.tag : name}>`;
+ const not_equal = component.options.immutable ? `@not_equal` : `@safe_not_equal`;
+ let dev_props_check;
- const inject_refs = refs.length > 0
- ? deindent`
- $$refs => {
- ${refs.map(name => `${name} = $$refs.${name};`)}
- }
- `
- : null;
-
- const body = [];
-
- const debug_name = `<${component.customElement ? component.tag : name}>`;
- const not_equal = component.options.immutable ? `@not_equal` : `@safe_not_equal`;
- let dev_props_check;
-
- if (component.options.dev) {
- // TODO check no uunexpected props were passed, as well as
- // checking that expected ones were passed
- const expected = component.exports
- .map(x => x.name)
- .filter(name => !component.initialised_declarations.has(name));
-
- if (expected.length) {
- dev_props_check = deindent`
- const state = this.$$.get();
- ${expected.map(name => deindent`
-
- if (state.${name} === undefined) {
- console.warn("${debug_name} was created without expected data property '${name}'");
- }`)}
- `;
+ component.exports.forEach(x => {
+ body.push(deindent`
+ get ${x.as}() {
+ return this.$$.get().${x.name};
}
- }
+ `);
- component.exports.forEach(x => {
+ if (component.writable_declarations.has(x.as) && !renderer.readonly.has(x.as)) {
+ body.push(deindent`
+ set ${x.as}(value) {
+ this.$set({ ${x.name}: value });
+ @flush();
+ }
+ `);
+ } else if (component.options.dev) {
body.push(deindent`
- get ${x.as}() {
- return this.$$.get().${x.name};
+ set ${x.as}(value) {
+ throw new Error("${debug_name}: Cannot set read-only property '${x.as}'");
}
`);
+ }
+ });
- if (component.writable_declarations.has(x.as) && !renderer.readonly.has(x.as)) {
- body.push(deindent`
- set ${x.as}(value) {
- this.$set({ ${x.name}: value });
- @flush();
- }
- `);
- } else if (component.options.dev) {
- body.push(deindent`
- set ${x.as}(value) {
- throw new Error("${debug_name}: Cannot set read-only property '${x.as}'");
- }
- `);
- }
- });
+ if (component.options.dev) {
+ // TODO check no uunexpected props were passed, as well as
+ // checking that expected ones were passed
+ const expected = component.exports
+ .map(x => x.name)
+ .filter(name => !component.initialised_declarations.has(name));
+
+ if (expected.length) {
+ dev_props_check = deindent`
+ const state = this.$$.get();
+ ${expected.map(name => deindent`
+
+ if (state.${name} === undefined) {
+ console.warn("${debug_name} was created without expected data property '${name}'");
+ }`)}
+ `;
+ }
+ }
- builder.addBlock(deindent`
- function create_fragment(${component.alias('component')}, ctx) {
- ${block.getContents()}
- }
+ builder.addBlock(deindent`
+ function create_fragment(${component.alias('component')}, ctx) {
+ ${block.getContents()}
+ }
+
+ ${component.module_javascript}
- ${component.module_javascript}
+ ${component.fully_hoisted.length > 0 && component.fully_hoisted.join('\n\n')}
- ${component.fully_hoisted.length > 0 && component.fully_hoisted.join('\n\n')}
+ function define($$self, $$make_dirty) {
+ ${should_add_css &&
+ `if (!document.getElementById("${component.stylesheet.id}-style")) @add_css();`}
- function define($$self, $$make_dirty) {
- ${should_add_css &&
- `if (!document.getElementById("${component.stylesheet.id}-style")) @add_css();`}
+ ${component.javascript || component.exports.map(x => `let ${x.name};`)}
- ${component.javascript || component.exports.map(x => `let ${x.name};`)}
+ ${component.partly_hoisted.length > 0 && component.partly_hoisted.join('\n\n')}
- ${component.partly_hoisted.length > 0 && component.partly_hoisted.join('\n\n')}
+ // TODO only what's needed by the template
+ $$self.$$.get = () => ({ ${component.declarations.join(', ')} });
- // TODO only what's needed by the template
- $$self.$$.get = () => ({ ${component.declarations.join(', ')} });
+ ${set && `$$self.$$.set = ${set};`}
- ${set && `$$self.$$.set = ${set};`}
+ ${inject_refs && `$$self.$$.inject_refs = ${inject_refs};`}
+ }
+ `);
+
+ if (component.customElement) {
+ // TODO observedAttributes
- ${inject_refs && `$$self.$$.inject_refs = ${inject_refs};`}
+ builder.addBlock(deindent`
+ class ${name} extends @SvelteElement {
+ constructor() {
+ super();
+ @init(this, { target: this.shadowRoot }, define, create_fragment, ${not_equal});
+ }
+
+ static get observedAttributes() {
+ return [];
+ }
+
+ ${body.join('\n\n')}
}
+ customElements.define("${component.customElement.tag}", ${name});
+ `);
+ } else {
+ builder.addBlock(deindent`
class ${name} extends ${superclass} {
constructor(options) {
super(${options.dev && `options`});
diff --git a/src/internal/Component.js b/src/internal/Component.js
index 08745564dc..d18dcf2093 100644
--- a/src/internal/Component.js
+++ b/src/internal/Component.js
@@ -110,7 +110,53 @@ export function init(component, options, define, create_fragment, not_equal) {
set_current_component(previous_component);
}
-export class $$Component {
+export let SvelteElement;
+if (typeof HTMLElement !== 'undefined') {
+ SvelteElement = class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+ }
+
+ connectedCallback() {
+ for (let key in this.$$.slotted) {
+ this.appendChild(this.$$.slotted[key]);
+ }
+ }
+
+ attributeChangedCallback(attr, oldValue, newValue) {
+ this[attr] = newValue;
+ }
+
+ $destroy() {
+ destroy(this, true);
+ this.$destroy = noop;
+ }
+
+ $on(type, callback) {
+ // TODO should this delegate to addEventListener?
+ const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
+ callbacks.push(callback);
+
+ return () => {
+ const index = callbacks.indexOf(callback);
+ if (index !== -1) callbacks.splice(index, 1);
+ };
+ }
+
+ $set(values) {
+ if (this.$$) {
+ const state = this.$$.get();
+ this.$$.set(values);
+ for (const key in values) {
+ if (this.$$.not_equal(state[key], values[key])) make_dirty(this, key);
+ }
+ }
+ }
+ }
+}
+
+export class SvelteComponent {
$destroy() {
destroy(this, true);
this.$destroy = noop;
@@ -137,7 +183,7 @@ export class $$Component {
}
}
-export class $$ComponentDev extends $$Component {
+export class SvelteComponentDev extends SvelteComponent {
constructor(options) {
if (!options || (!options.target && !options.$$inline)) {
throw new Error(`'target' is a required option`);
diff --git a/test/custom-elements/index.js b/test/custom-elements/index.js
index 7e49845fc3..19d79f328b 100644
--- a/test/custom-elements/index.js
+++ b/test/custom-elements/index.js
@@ -15,7 +15,7 @@ const page = `
const assert = fs.readFileSync('test/custom-elements/assert.js', 'utf-8');
-describe('custom-elements', function() {
+describe.only('custom-elements', function() {
this.timeout(10000);
let svelte;
@@ -104,12 +104,13 @@ describe('custom-elements', function() {
})
.then(result => {
if (result) console.log(result);
+ nightmare.end();
})
.catch(message => {
console.log(addLineNumbers(bundle));
+ nightmare.end();
throw new Error(message);
- })
- .then(() => nightmare.end());
+ });
});
diff --git a/test/custom-elements/samples/custom-method/test.js b/test/custom-elements/samples/custom-method/test.js
index f6ef68c8c0..2233f0b452 100644
--- a/test/custom-elements/samples/custom-method/test.js
+++ b/test/custom-elements/samples/custom-method/test.js
@@ -1,11 +1,11 @@
import * as assert from 'assert';
import './main.html';
-export default function (target) {
- target.innerHTML = '';
+export default async function (target) {
+ target.innerHTML = '';
const el = target.querySelector('custom-element');
- el.updateFoo(42);
+ await el.updateFoo(42);
const p = el.shadowRoot.querySelector('p');
assert.equal(p.textContent, '42');
diff --git a/test/runtime/index.js b/test/runtime/index.js
index ea335e9bd2..d8c31fb4c1 100644
--- a/test/runtime/index.js
+++ b/test/runtime/index.js
@@ -25,7 +25,7 @@ function getName(filename) {
return base[0].toUpperCase() + base.slice(1);
}
-describe.only("runtime", () => {
+describe("runtime", () => {
before(() => {
svelte = loadSvelte(false);
svelte$ = loadSvelte(true);