custom elements sort of working

pull/1864/head
Rich Harris 7 years ago
parent ae3a7dd369
commit 5030385b1b

@ -133,7 +133,7 @@ export default class Component {
this.walk_module_js(); this.walk_module_js();
this.walk_instance_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.meta = process_meta(this, this.ast.html.children);
this.namespace = namespaces[this.meta.namespace] || this.meta.namespace; this.namespace = namespaces[this.meta.namespace] || this.meta.namespace;
@ -209,7 +209,7 @@ export default class Component {
}); });
const importedHelpers = Array.from(helpers) const importedHelpers = Array.from(helpers)
.concat(options.dev ? '$$ComponentDev' : '$$Component') .concat(options.dev ? 'SvelteComponentDev' : 'SvelteComponent')
.sort() .sort()
.map(name => { .map(name => {
const alias = this.alias(name); const alias = this.alias(name);

@ -58,150 +58,143 @@ export default function dom(
const expectedProperties = Array.from(component.expectedProperties); const expectedProperties = Array.from(component.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop)); const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
if (component.customElement) { const refs = Array.from(component.refs);
// TODO use `export` to determine this
const props = Array.from(component.expectedProperties);
builder.addBlock(deindent` const superclass = component.alias(options.dev ? 'SvelteComponentDev' : 'SvelteComponent');
class ${name} extends HTMLElement {
constructor(options = {}) {
super();
}
static get observedAttributes() {
return ${JSON.stringify(props)};
}
${renderer.slots.size && deindent` if (options.dev && !options.hydratable) {
connectedCallback() { block.builders.claim.addLine(
Object.keys(this.$$.slotted).forEach(key => { 'throw new Error("options.hydrate only works if the component was compiled with the `hydratable: true` option");'
this.appendChild(this.$$.slotted[key]); );
}); }
}`}
attributeChangedCallback(attr, oldValue, newValue) { // TODO injecting CSS this way is kinda dirty. Maybe it should be an
this[attr] = newValue; // 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}); const inject_refs = refs.length > 0
`); ? deindent`
} else { $$refs => {
const refs = Array.from(component.refs); ${refs.map(name => `${name} = $$refs.${name};`)}
}
const superclass = component.alias(options.dev ? '$$ComponentDev' : '$$Component'); `
: null;
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");'
);
}
// TODO injecting CSS this way is kinda dirty. Maybe it should be an const body = [];
// 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 debug_name = `<${component.customElement ? component.tag : name}>`;
const not_equal = component.options.immutable ? `@not_equal` : `@safe_not_equal`;
const set = component.meta.props || props.length > 0 let dev_props_check;
? 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 inject_refs = refs.length > 0 component.exports.forEach(x => {
? deindent` body.push(deindent`
$$refs => { get ${x.as}() {
${refs.map(name => `${name} = $$refs.${name};`)} return this.$$.get().${x.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 => { 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` body.push(deindent`
get ${x.as}() { set ${x.as}(value) {
return this.$$.get().${x.name}; throw new Error("${debug_name}: Cannot set read-only property '${x.as}'");
} }
`); `);
}
});
if (component.writable_declarations.has(x.as) && !renderer.readonly.has(x.as)) { if (component.options.dev) {
body.push(deindent` // TODO check no uunexpected props were passed, as well as
set ${x.as}(value) { // checking that expected ones were passed
this.$set({ ${x.name}: value }); const expected = component.exports
@flush(); .map(x => x.name)
} .filter(name => !component.initialised_declarations.has(name));
`);
} else if (component.options.dev) { if (expected.length) {
body.push(deindent` dev_props_check = deindent`
set ${x.as}(value) { const state = this.$$.get();
throw new Error("${debug_name}: Cannot set read-only property '${x.as}'"); ${expected.map(name => deindent`
}
`); if (state.${name} === undefined) {
} console.warn("${debug_name} was created without expected data property '${name}'");
}); }`)}
`;
}
}
builder.addBlock(deindent` builder.addBlock(deindent`
function create_fragment(${component.alias('component')}, ctx) { function create_fragment(${component.alias('component')}, ctx) {
${block.getContents()} ${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) { ${component.javascript || component.exports.map(x => `let ${x.name};`)}
${should_add_css &&
`if (!document.getElementById("${component.stylesheet.id}-style")) @add_css();`}
${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 ${set && `$$self.$$.set = ${set};`}
$$self.$$.get = () => ({ ${component.declarations.join(', ')} });
${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} { class ${name} extends ${superclass} {
constructor(options) { constructor(options) {
super(${options.dev && `options`}); super(${options.dev && `options`});

@ -110,7 +110,53 @@ export function init(component, options, define, create_fragment, not_equal) {
set_current_component(previous_component); 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() {
destroy(this, true); destroy(this, true);
this.$destroy = noop; this.$destroy = noop;
@ -137,7 +183,7 @@ export class $$Component {
} }
} }
export class $$ComponentDev extends $$Component { export class SvelteComponentDev extends SvelteComponent {
constructor(options) { constructor(options) {
if (!options || (!options.target && !options.$$inline)) { if (!options || (!options.target && !options.$$inline)) {
throw new Error(`'target' is a required option`); throw new Error(`'target' is a required option`);

@ -15,7 +15,7 @@ const page = `
const assert = fs.readFileSync('test/custom-elements/assert.js', 'utf-8'); const assert = fs.readFileSync('test/custom-elements/assert.js', 'utf-8');
describe('custom-elements', function() { describe.only('custom-elements', function() {
this.timeout(10000); this.timeout(10000);
let svelte; let svelte;
@ -104,12 +104,13 @@ describe('custom-elements', function() {
}) })
.then(result => { .then(result => {
if (result) console.log(result); if (result) console.log(result);
nightmare.end();
}) })
.catch(message => { .catch(message => {
console.log(addLineNumbers(bundle)); console.log(addLineNumbers(bundle));
nightmare.end();
throw new Error(message); throw new Error(message);
}) });
.then(() => nightmare.end());
}); });

@ -1,11 +1,11 @@
import * as assert from 'assert'; import * as assert from 'assert';
import './main.html'; import './main.html';
export default function (target) { export default async function (target) {
target.innerHTML = '<custom-element name="world"></custom-element>'; target.innerHTML = '<custom-element></custom-element>';
const el = target.querySelector('custom-element'); const el = target.querySelector('custom-element');
el.updateFoo(42); await el.updateFoo(42);
const p = el.shadowRoot.querySelector('p'); const p = el.shadowRoot.querySelector('p');
assert.equal(p.textContent, '42'); assert.equal(p.textContent, '42');

@ -25,7 +25,7 @@ function getName(filename) {
return base[0].toUpperCase() + base.slice(1); return base[0].toUpperCase() + base.slice(1);
} }
describe.only("runtime", () => { describe("runtime", () => {
before(() => { before(() => {
svelte = loadSvelte(false); svelte = loadSvelte(false);
svelte$ = loadSvelte(true); svelte$ = loadSvelte(true);

Loading…
Cancel
Save