Skip to content

Commit

Permalink
Merge pull request #811 from sveltejs/gh-797
Browse files Browse the repository at this point in the history
compile to custom element
  • Loading branch information
Rich-Harris committed Sep 3, 2017
2 parents 3ea3f53 + 81a04ad commit 7c29def
Show file tree
Hide file tree
Showing 44 changed files with 1,577 additions and 128 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
],
"scripts": {
"test": "mocha --opts mocha.opts",
"quicktest": "mocha --opts mocha.opts",
"precoverage": "export COVERAGE=true && nyc mocha --opts mocha.coverage.opts",
"coverage": "nyc report --reporter=text-lcov > coverage.lcov",
"codecov": "codecov",
Expand Down
10 changes: 6 additions & 4 deletions src/css/Stylesheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ export default class Stylesheet {
}
}

render(cssOutputFilename: string) {
render(cssOutputFilename: string, shouldTransformSelectors: boolean) {
if (!this.hasStyles) {
return { css: null, cssMap: null };
}
Expand All @@ -351,9 +351,11 @@ export default class Stylesheet {
}
});

this.children.forEach((child: (Atrule|Rule)) => {
child.transform(code, this.id, this.keyframes, this.cascade);
});
if (shouldTransformSelectors) {
this.children.forEach((child: (Atrule|Rule)) => {
child.transform(code, this.id, this.keyframes, this.cascade);
});
}

let c = 0;
this.children.forEach(child => {
Expand Down
33 changes: 31 additions & 2 deletions src/generators/Generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import clone from '../utils/clone';
import DomBlock from './dom/Block';
import SsrBlock from './server-side-rendering/Block';
import Stylesheet from '../css/Stylesheet';
import { Node, GenerateOptions, Parsed, CompileOptions } from '../interfaces';
import { Node, GenerateOptions, Parsed, CompileOptions, CustomElementOptions } from '../interfaces';

const test = typeof global !== 'undefined' && global.__svelte_test;

Expand All @@ -31,6 +31,10 @@ export default class Generator {
name: string;
options: CompileOptions;

customElement: CustomElementOptions;
tag: string;
props: string[];

defaultExport: Node[];
imports: Node[];
helpers: Set<string>;
Expand Down Expand Up @@ -100,6 +104,19 @@ export default class Generator {

this.parseJs();
this.name = this.alias(name);

if (options.customElement === true) {
this.customElement = {
tag: this.tag,
props: this.props // TODO autofill this in
}
} else {
this.customElement = options.customElement;
}

if (this.customElement && !this.customElement.tag) {
throw new Error(`No tag name specified`); // TODO better error
}
}

addSourcemapLocations(node: Node) {
Expand Down Expand Up @@ -372,7 +389,9 @@ export default class Generator {
addString(finalChunk);
addString('\n\n' + getOutro(format, name, options, this.imports));

const { css, cssMap } = this.stylesheet.render(options.cssOutputFilename);
const { css, cssMap } = this.customElement ?
{ css: null, cssMap: null } :
this.stylesheet.render(options.cssOutputFilename, true);

return {
ast: this.ast,
Expand Down Expand Up @@ -554,6 +573,16 @@ export default class Generator {
templateProperties.ondestroy = templateProperties.onteardown;
}

if (templateProperties.tag) {
this.tag = templateProperties.tag.value.value;
removeObjectKey(this.code, defaultExport.declaration, 'tag');
}

if (templateProperties.props) {
this.props = templateProperties.props.value.elements.map((element: Node) => element.value);
removeObjectKey(this.code, defaultExport.declaration, 'props');
}

// now that we've analysed the default export, we can determine whether or not we need to keep it
let hasDefaultExport = !!defaultExport;
if (defaultExport && defaultExport.declaration.properties.length === 0) {
Expand Down
218 changes: 139 additions & 79 deletions src/generators/dom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,18 +111,17 @@ export default function dom(
`);
}

if (generator.stylesheet.hasStyles && options.css !== false) {
const { css, cssMap } = generator.stylesheet.render(options.filename);

const textContent = stringify(options.dev ?
`${css}\n/*# sourceMappingURL=${cssMap.toUrl()} */` :
css, { onlyEscapeAtSymbol: true });
const { css, cssMap } = generator.stylesheet.render(options.filename, !generator.customElement);
const styles = generator.stylesheet.hasStyles && stringify(options.dev ?
`${css}\n/*# sourceMappingURL=${cssMap.toUrl()} */` :
css, { onlyEscapeAtSymbol: true });

if (styles && generator.options.css !== false && !generator.customElement) {
builder.addBlock(deindent`
function @add_css () {
var style = @createElement( 'style' );
style.id = '${generator.stylesheet.id}-style';
style.textContent = ${textContent};
style.textContent = ${styles};
@appendNode( style, document.head );
}
`);
Expand All @@ -143,95 +142,156 @@ export default function dom(
? `@proto `
: deindent`
{
${['destroy', 'get', 'fire', 'observe', 'on', 'set', '_set', 'teardown']
${['destroy', 'get', 'fire', 'observe', 'on', 'set', 'teardown', '_set', '_mount', '_unmount']
.map(n => `${n}: @${n === 'teardown' ? 'destroy' : n}`)
.join(',\n')}
}`;

// TODO deprecate component.teardown()
builder.addBlock(deindent`
function ${name} ( options ) {
${options.dev &&
`if ( !options || (!options.target && !options._root) ) throw new Error( "'target' is a required option" );`}
this.options = options;
${generator.usesRefs && `this.refs = {};`}
this._state = ${templateProperties.data
? `@assign( @template.data(), options.data )`
: `options.data || {}`};
${generator.metaBindings}
${computations.length && `this._recompute( {}, this._state, {}, true );`}
${options.dev &&
Array.from(generator.expectedProperties).map(
prop =>
`if ( !( '${prop}' in this._state ) ) console.warn( "Component was created without expected data property '${prop}'" );`
)}
${generator.bindingGroups.length &&
`this._bindingGroups = [ ${Array(generator.bindingGroups.length)
.fill('[]')
.join(', ')} ];`}
this._observers = {
pre: Object.create( null ),
post: Object.create( null )
};
const constructorBody = deindent`
${options.dev && !generator.customElement &&
`if ( !options || (!options.target && !options._root) ) throw new Error( "'target' is a required option" );`}
this.options = options;
${generator.usesRefs && `this.refs = {};`}
this._state = ${templateProperties.data
? `@assign( @template.data(), options.data )`
: `options.data || {}`};
${generator.metaBindings}
${computations.length && `this._recompute( {}, this._state, {}, true );`}
${options.dev &&
Array.from(generator.expectedProperties).map(
prop =>
`if ( !( '${prop}' in this._state ) ) console.warn( "Component was created without expected data property '${prop}'" );`
)}
${generator.bindingGroups.length &&
`this._bindingGroups = [ ${Array(generator.bindingGroups.length)
.fill('[]')
.join(', ')} ];`}
this._observers = {
pre: Object.create( null ),
post: Object.create( null )
};
this._handlers = Object.create( null );
${templateProperties.ondestroy && `this._handlers.destroy = [@template.ondestroy]`}
this._root = options._root || this;
this._yield = options._yield;
this._bind = options._bind;
${generator.slots.size && `this._slotted = options.slots || {};`}
${generator.customElement ?
deindent`
this.attachShadow({ mode: 'open' });
${css && `this.shadowRoot.innerHTML = \`<style>${options.dev ? `${css}\n/*# sourceMappingURL=${cssMap.toUrl()} */` : css}</style>\`;`}
` :
(generator.stylesheet.hasStyles && options.css !== false &&
`if ( !document.getElementById( '${generator.stylesheet.id}-style' ) ) @add_css();`)
}
this._handlers = Object.create( null );
${templateProperties.ondestroy && `this._handlers.destroy = [@template.ondestroy]`}
${templateProperties.oncreate && `var oncreate = @template.oncreate.bind( this );`}
this._root = options._root || this;
this._yield = options._yield;
this._bind = options._bind;
${generator.slots.size && `this._slotted = options.slots || {};`}
${(templateProperties.oncreate || generator.hasComponents || generator.hasComplexBindings || generator.hasIntroTransitions) && deindent`
if ( !options._root ) {
this._oncreate = [${templateProperties.oncreate && `oncreate`}];
${(generator.hasComponents || generator.hasComplexBindings) && `this._beforecreate = [];`}
${(generator.hasComponents || generator.hasIntroTransitions) && `this._aftercreate = [];`}
} ${templateProperties.oncreate && deindent`
else {
this._root._oncreate.push(oncreate);
}
`}
`}
${generator.stylesheet.hasStyles &&
options.css !== false &&
`if ( !document.getElementById( '${generator.stylesheet.id}-style' ) ) @add_css();`}
${generator.slots.size && `this.slots = {};`}
${templateProperties.oncreate && `var oncreate = @template.oncreate.bind( this );`}
this._fragment = @create_main_fragment( this._state, this );
${(templateProperties.oncreate || generator.hasComponents || generator.hasComplexBindings || generator.hasIntroTransitions) && deindent`
if ( !options._root ) {
this._oncreate = [${templateProperties.oncreate && `oncreate`}];
${(generator.hasComponents || generator.hasComplexBindings) && `this._beforecreate = [];`}
${(generator.hasComponents || generator.hasIntroTransitions) && `this._aftercreate = [];`}
} ${templateProperties.oncreate && deindent`
else {
this._root._oncreate.push(oncreate);
}
if ( options.target ) {
${generator.hydratable
? deindent`
var nodes = @children( options.target );
options.hydrate ? this._fragment.claim( nodes ) : this._fragment.create();
nodes.forEach( @detachNode );
` :
deindent`
${options.dev && `if ( options.hydrate ) throw new Error( 'options.hydrate only works if the component was compiled with the \`hydratable: true\` option' );`}
this._fragment.create();
`}
${generator.customElement ?
`this._mount( options.target, options.anchor || null );` :
`this._fragment.${block.hasIntroMethod ? 'intro' : 'mount'}( options.target, options.anchor || null );`}
${(generator.hasComponents || generator.hasComplexBindings || templateProperties.oncreate || generator.hasIntroTransitions) && deindent`
${generator.hasComponents && `this._lock = true;`}
${(generator.hasComponents || generator.hasComplexBindings) && `@callAll(this._beforecreate);`}
${(generator.hasComponents || templateProperties.oncreate) && `@callAll(this._oncreate);`}
${(generator.hasComponents || generator.hasIntroTransitions) && `@callAll(this._aftercreate);`}
${generator.hasComponents && `this._lock = false;`}
`}
}
`;

if (generator.customElement) {
const props = generator.props || Array.from(generator.expectedProperties);

builder.addBlock(deindent`
class ${name} extends HTMLElement {
constructor(options = {}) {
super();
${constructorBody}
}
static get observedAttributes() {
return ${JSON.stringify(props)};
}
${generator.slots.size && `this.slots = {};`}
this._fragment = @create_main_fragment( this._state, this );
if ( options.target ) {
${generator.hydratable
? deindent`
var nodes = @children( options.target );
options.hydrate ? this._fragment.claim( nodes ) : this._fragment.create();
nodes.forEach( @detachNode );
` :
deindent`
${options.dev && `if ( options.hydrate ) throw new Error( 'options.hydrate only works if the component was compiled with the \`hydratable: true\` option' );`}
this._fragment.create();
`}
this._fragment.${block.hasIntroMethod ? 'intro' : 'mount'}( options.target, options.anchor || null );
${props.map(prop => deindent`
get ${prop}() {
return this.get('${prop}');
}
set ${prop}(value) {
this.set({ ${prop}: value });
}
`).join('\n\n')}
${generator.slots.size && deindent`
connectedCallback() {
Object.keys(this._slotted).forEach(key => {
this.appendChild(this._slotted[key]);
});
}`}
attributeChangedCallback ( attr, oldValue, newValue ) {
this.set({ [attr]: newValue });
}
}
${(generator.hasComponents || generator.hasComplexBindings || templateProperties.oncreate || generator.hasIntroTransitions) && deindent`
if ( !options._root ) {
${generator.hasComponents && `this._lock = true;`}
${(generator.hasComponents || generator.hasComplexBindings) && `@callAll(this._beforecreate);`}
${(generator.hasComponents || templateProperties.oncreate) && `@callAll(this._oncreate);`}
${(generator.hasComponents || generator.hasIntroTransitions) && `@callAll(this._aftercreate);`}
${generator.hasComponents && `this._lock = false;`}
customElements.define('${generator.tag}', ${name});
@assign( ${prototypeBase}, ${proto}, {
_mount(target, anchor) {
this._fragment.${block.hasIntroMethod ? 'intro' : 'mount'}(this.shadowRoot, null);
target.insertBefore(this, anchor);
},
_unmount() {
this.parentNode.removeChild(this);
}
`}
}
});
`);
} else {
builder.addBlock(deindent`
function ${name} ( options ) {
${constructorBody}
}
@assign( ${prototypeBase}, ${proto});
@assign( ${prototypeBase}, ${proto});
`);
}

// TODO deprecate component.teardown()
builder.addBlock(deindent`
${options.dev && deindent`
${name}.prototype._checkReadOnly = function _checkReadOnly ( newState ) {
${Array.from(generator.readonly).map(
Expand Down
3 changes: 1 addition & 2 deletions src/generators/dom/preprocess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,8 +466,7 @@ export default function preprocess(
const state: State = {
namespace,
parentNode: null,
parentNodes: 'nodes',
isTopLevel: true,
parentNodes: 'nodes'
};

generator.blocks.push(block);
Expand Down
4 changes: 2 additions & 2 deletions src/generators/dom/visitors/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,10 @@ export default function visitComponent(
);

block.builders.mount.addLine(
`${name}._fragment.mount( ${state.parentNode || '#target'}, ${state.parentNode ? 'null' : 'anchor'} );`
`${name}._mount( ${state.parentNode || '#target'}, ${state.parentNode ? 'null' : 'anchor'} );`
);

if (!state.parentNode) block.builders.unmount.addLine(`${name}._fragment.unmount();`);
if (!state.parentNode) block.builders.unmount.addLine(`${name}._unmount();`);

block.builders.destroy.addLine(`${name}.destroy( false );`);

Expand Down

0 comments on commit 7c29def

Please sign in to comment.