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);