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

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

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

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

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

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

Loading…
Cancel
Save