Skip to content

Commit 7c29def

Browse files
authored
Merge pull request #811 from sveltejs/gh-797
compile to custom element
2 parents 3ea3f53 + 81a04ad commit 7c29def

File tree

44 files changed

+1577
-128
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1577
-128
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
],
1212
"scripts": {
1313
"test": "mocha --opts mocha.opts",
14+
"quicktest": "mocha --opts mocha.opts",
1415
"precoverage": "export COVERAGE=true && nyc mocha --opts mocha.coverage.opts",
1516
"coverage": "nyc report --reporter=text-lcov > coverage.lcov",
1617
"codecov": "codecov",

src/css/Stylesheet.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ export default class Stylesheet {
337337
}
338338
}
339339

340-
render(cssOutputFilename: string) {
340+
render(cssOutputFilename: string, shouldTransformSelectors: boolean) {
341341
if (!this.hasStyles) {
342342
return { css: null, cssMap: null };
343343
}
@@ -351,9 +351,11 @@ export default class Stylesheet {
351351
}
352352
});
353353

354-
this.children.forEach((child: (Atrule|Rule)) => {
355-
child.transform(code, this.id, this.keyframes, this.cascade);
356-
});
354+
if (shouldTransformSelectors) {
355+
this.children.forEach((child: (Atrule|Rule)) => {
356+
child.transform(code, this.id, this.keyframes, this.cascade);
357+
});
358+
}
357359

358360
let c = 0;
359361
this.children.forEach(child => {

src/generators/Generator.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import clone from '../utils/clone';
1515
import DomBlock from './dom/Block';
1616
import SsrBlock from './server-side-rendering/Block';
1717
import Stylesheet from '../css/Stylesheet';
18-
import { Node, GenerateOptions, Parsed, CompileOptions } from '../interfaces';
18+
import { Node, GenerateOptions, Parsed, CompileOptions, CustomElementOptions } from '../interfaces';
1919

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

@@ -31,6 +31,10 @@ export default class Generator {
3131
name: string;
3232
options: CompileOptions;
3333

34+
customElement: CustomElementOptions;
35+
tag: string;
36+
props: string[];
37+
3438
defaultExport: Node[];
3539
imports: Node[];
3640
helpers: Set<string>;
@@ -100,6 +104,19 @@ export default class Generator {
100104

101105
this.parseJs();
102106
this.name = this.alias(name);
107+
108+
if (options.customElement === true) {
109+
this.customElement = {
110+
tag: this.tag,
111+
props: this.props // TODO autofill this in
112+
}
113+
} else {
114+
this.customElement = options.customElement;
115+
}
116+
117+
if (this.customElement && !this.customElement.tag) {
118+
throw new Error(`No tag name specified`); // TODO better error
119+
}
103120
}
104121

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

375-
const { css, cssMap } = this.stylesheet.render(options.cssOutputFilename);
392+
const { css, cssMap } = this.customElement ?
393+
{ css: null, cssMap: null } :
394+
this.stylesheet.render(options.cssOutputFilename, true);
376395

377396
return {
378397
ast: this.ast,
@@ -554,6 +573,16 @@ export default class Generator {
554573
templateProperties.ondestroy = templateProperties.onteardown;
555574
}
556575

576+
if (templateProperties.tag) {
577+
this.tag = templateProperties.tag.value.value;
578+
removeObjectKey(this.code, defaultExport.declaration, 'tag');
579+
}
580+
581+
if (templateProperties.props) {
582+
this.props = templateProperties.props.value.elements.map((element: Node) => element.value);
583+
removeObjectKey(this.code, defaultExport.declaration, 'props');
584+
}
585+
557586
// now that we've analysed the default export, we can determine whether or not we need to keep it
558587
let hasDefaultExport = !!defaultExport;
559588
if (defaultExport && defaultExport.declaration.properties.length === 0) {

src/generators/dom/index.ts

Lines changed: 139 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -111,18 +111,17 @@ export default function dom(
111111
`);
112112
}
113113

114-
if (generator.stylesheet.hasStyles && options.css !== false) {
115-
const { css, cssMap } = generator.stylesheet.render(options.filename);
116-
117-
const textContent = stringify(options.dev ?
118-
`${css}\n/*# sourceMappingURL=${cssMap.toUrl()} */` :
119-
css, { onlyEscapeAtSymbol: true });
114+
const { css, cssMap } = generator.stylesheet.render(options.filename, !generator.customElement);
115+
const styles = generator.stylesheet.hasStyles && stringify(options.dev ?
116+
`${css}\n/*# sourceMappingURL=${cssMap.toUrl()} */` :
117+
css, { onlyEscapeAtSymbol: true });
120118

119+
if (styles && generator.options.css !== false && !generator.customElement) {
121120
builder.addBlock(deindent`
122121
function @add_css () {
123122
var style = @createElement( 'style' );
124123
style.id = '${generator.stylesheet.id}-style';
125-
style.textContent = ${textContent};
124+
style.textContent = ${styles};
126125
@appendNode( style, document.head );
127126
}
128127
`);
@@ -143,95 +142,156 @@ export default function dom(
143142
? `@proto `
144143
: deindent`
145144
{
146-
${['destroy', 'get', 'fire', 'observe', 'on', 'set', '_set', 'teardown']
145+
${['destroy', 'get', 'fire', 'observe', 'on', 'set', 'teardown', '_set', '_mount', '_unmount']
147146
.map(n => `${n}: @${n === 'teardown' ? 'destroy' : n}`)
148147
.join(',\n')}
149148
}`;
150149

151-
// TODO deprecate component.teardown()
152-
builder.addBlock(deindent`
153-
function ${name} ( options ) {
154-
${options.dev &&
155-
`if ( !options || (!options.target && !options._root) ) throw new Error( "'target' is a required option" );`}
156-
this.options = options;
157-
${generator.usesRefs && `this.refs = {};`}
158-
this._state = ${templateProperties.data
159-
? `@assign( @template.data(), options.data )`
160-
: `options.data || {}`};
161-
${generator.metaBindings}
162-
${computations.length && `this._recompute( {}, this._state, {}, true );`}
163-
${options.dev &&
164-
Array.from(generator.expectedProperties).map(
165-
prop =>
166-
`if ( !( '${prop}' in this._state ) ) console.warn( "Component was created without expected data property '${prop}'" );`
167-
)}
168-
${generator.bindingGroups.length &&
169-
`this._bindingGroups = [ ${Array(generator.bindingGroups.length)
170-
.fill('[]')
171-
.join(', ')} ];`}
172-
173-
this._observers = {
174-
pre: Object.create( null ),
175-
post: Object.create( null )
176-
};
150+
const constructorBody = deindent`
151+
${options.dev && !generator.customElement &&
152+
`if ( !options || (!options.target && !options._root) ) throw new Error( "'target' is a required option" );`}
153+
this.options = options;
154+
${generator.usesRefs && `this.refs = {};`}
155+
this._state = ${templateProperties.data
156+
? `@assign( @template.data(), options.data )`
157+
: `options.data || {}`};
158+
${generator.metaBindings}
159+
${computations.length && `this._recompute( {}, this._state, {}, true );`}
160+
${options.dev &&
161+
Array.from(generator.expectedProperties).map(
162+
prop =>
163+
`if ( !( '${prop}' in this._state ) ) console.warn( "Component was created without expected data property '${prop}'" );`
164+
)}
165+
${generator.bindingGroups.length &&
166+
`this._bindingGroups = [ ${Array(generator.bindingGroups.length)
167+
.fill('[]')
168+
.join(', ')} ];`}
169+
170+
this._observers = {
171+
pre: Object.create( null ),
172+
post: Object.create( null )
173+
};
174+
175+
this._handlers = Object.create( null );
176+
${templateProperties.ondestroy && `this._handlers.destroy = [@template.ondestroy]`}
177+
178+
this._root = options._root || this;
179+
this._yield = options._yield;
180+
this._bind = options._bind;
181+
${generator.slots.size && `this._slotted = options.slots || {};`}
182+
183+
${generator.customElement ?
184+
deindent`
185+
this.attachShadow({ mode: 'open' });
186+
${css && `this.shadowRoot.innerHTML = \`<style>${options.dev ? `${css}\n/*# sourceMappingURL=${cssMap.toUrl()} */` : css}</style>\`;`}
187+
` :
188+
(generator.stylesheet.hasStyles && options.css !== false &&
189+
`if ( !document.getElementById( '${generator.stylesheet.id}-style' ) ) @add_css();`)
190+
}
177191
178-
this._handlers = Object.create( null );
179-
${templateProperties.ondestroy && `this._handlers.destroy = [@template.ondestroy]`}
192+
${templateProperties.oncreate && `var oncreate = @template.oncreate.bind( this );`}
180193
181-
this._root = options._root || this;
182-
this._yield = options._yield;
183-
this._bind = options._bind;
184-
${generator.slots.size && `this._slotted = options.slots || {};`}
194+
${(templateProperties.oncreate || generator.hasComponents || generator.hasComplexBindings || generator.hasIntroTransitions) && deindent`
195+
if ( !options._root ) {
196+
this._oncreate = [${templateProperties.oncreate && `oncreate`}];
197+
${(generator.hasComponents || generator.hasComplexBindings) && `this._beforecreate = [];`}
198+
${(generator.hasComponents || generator.hasIntroTransitions) && `this._aftercreate = [];`}
199+
} ${templateProperties.oncreate && deindent`
200+
else {
201+
this._root._oncreate.push(oncreate);
202+
}
203+
`}
204+
`}
185205
186-
${generator.stylesheet.hasStyles &&
187-
options.css !== false &&
188-
`if ( !document.getElementById( '${generator.stylesheet.id}-style' ) ) @add_css();`}
206+
${generator.slots.size && `this.slots = {};`}
189207
190-
${templateProperties.oncreate && `var oncreate = @template.oncreate.bind( this );`}
208+
this._fragment = @create_main_fragment( this._state, this );
191209
192-
${(templateProperties.oncreate || generator.hasComponents || generator.hasComplexBindings || generator.hasIntroTransitions) && deindent`
193-
if ( !options._root ) {
194-
this._oncreate = [${templateProperties.oncreate && `oncreate`}];
195-
${(generator.hasComponents || generator.hasComplexBindings) && `this._beforecreate = [];`}
196-
${(generator.hasComponents || generator.hasIntroTransitions) && `this._aftercreate = [];`}
197-
} ${templateProperties.oncreate && deindent`
198-
else {
199-
this._root._oncreate.push(oncreate);
200-
}
210+
if ( options.target ) {
211+
${generator.hydratable
212+
? deindent`
213+
var nodes = @children( options.target );
214+
options.hydrate ? this._fragment.claim( nodes ) : this._fragment.create();
215+
nodes.forEach( @detachNode );
216+
` :
217+
deindent`
218+
${options.dev && `if ( options.hydrate ) throw new Error( 'options.hydrate only works if the component was compiled with the \`hydratable: true\` option' );`}
219+
this._fragment.create();
201220
`}
221+
${generator.customElement ?
222+
`this._mount( options.target, options.anchor || null );` :
223+
`this._fragment.${block.hasIntroMethod ? 'intro' : 'mount'}( options.target, options.anchor || null );`}
224+
225+
${(generator.hasComponents || generator.hasComplexBindings || templateProperties.oncreate || generator.hasIntroTransitions) && deindent`
226+
${generator.hasComponents && `this._lock = true;`}
227+
${(generator.hasComponents || generator.hasComplexBindings) && `@callAll(this._beforecreate);`}
228+
${(generator.hasComponents || templateProperties.oncreate) && `@callAll(this._oncreate);`}
229+
${(generator.hasComponents || generator.hasIntroTransitions) && `@callAll(this._aftercreate);`}
230+
${generator.hasComponents && `this._lock = false;`}
202231
`}
232+
}
233+
`;
234+
235+
if (generator.customElement) {
236+
const props = generator.props || Array.from(generator.expectedProperties);
237+
238+
builder.addBlock(deindent`
239+
class ${name} extends HTMLElement {
240+
constructor(options = {}) {
241+
super();
242+
${constructorBody}
243+
}
244+
245+
static get observedAttributes() {
246+
return ${JSON.stringify(props)};
247+
}
203248
204-
${generator.slots.size && `this.slots = {};`}
205-
206-
this._fragment = @create_main_fragment( this._state, this );
207-
208-
if ( options.target ) {
209-
${generator.hydratable
210-
? deindent`
211-
var nodes = @children( options.target );
212-
options.hydrate ? this._fragment.claim( nodes ) : this._fragment.create();
213-
nodes.forEach( @detachNode );
214-
` :
215-
deindent`
216-
${options.dev && `if ( options.hydrate ) throw new Error( 'options.hydrate only works if the component was compiled with the \`hydratable: true\` option' );`}
217-
this._fragment.create();
218-
`}
219-
this._fragment.${block.hasIntroMethod ? 'intro' : 'mount'}( options.target, options.anchor || null );
249+
${props.map(prop => deindent`
250+
get ${prop}() {
251+
return this.get('${prop}');
252+
}
253+
254+
set ${prop}(value) {
255+
this.set({ ${prop}: value });
256+
}
257+
`).join('\n\n')}
258+
259+
${generator.slots.size && deindent`
260+
connectedCallback() {
261+
Object.keys(this._slotted).forEach(key => {
262+
this.appendChild(this._slotted[key]);
263+
});
264+
}`}
265+
266+
attributeChangedCallback ( attr, oldValue, newValue ) {
267+
this.set({ [attr]: newValue });
268+
}
220269
}
221270
222-
${(generator.hasComponents || generator.hasComplexBindings || templateProperties.oncreate || generator.hasIntroTransitions) && deindent`
223-
if ( !options._root ) {
224-
${generator.hasComponents && `this._lock = true;`}
225-
${(generator.hasComponents || generator.hasComplexBindings) && `@callAll(this._beforecreate);`}
226-
${(generator.hasComponents || templateProperties.oncreate) && `@callAll(this._oncreate);`}
227-
${(generator.hasComponents || generator.hasIntroTransitions) && `@callAll(this._aftercreate);`}
228-
${generator.hasComponents && `this._lock = false;`}
271+
customElements.define('${generator.tag}', ${name});
272+
@assign( ${prototypeBase}, ${proto}, {
273+
_mount(target, anchor) {
274+
this._fragment.${block.hasIntroMethod ? 'intro' : 'mount'}(this.shadowRoot, null);
275+
target.insertBefore(this, anchor);
276+
},
277+
278+
_unmount() {
279+
this.parentNode.removeChild(this);
229280
}
230-
`}
231-
}
281+
});
282+
`);
283+
} else {
284+
builder.addBlock(deindent`
285+
function ${name} ( options ) {
286+
${constructorBody}
287+
}
232288
233-
@assign( ${prototypeBase}, ${proto});
289+
@assign( ${prototypeBase}, ${proto});
290+
`);
291+
}
234292

293+
// TODO deprecate component.teardown()
294+
builder.addBlock(deindent`
235295
${options.dev && deindent`
236296
${name}.prototype._checkReadOnly = function _checkReadOnly ( newState ) {
237297
${Array.from(generator.readonly).map(

src/generators/dom/preprocess.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -466,8 +466,7 @@ export default function preprocess(
466466
const state: State = {
467467
namespace,
468468
parentNode: null,
469-
parentNodes: 'nodes',
470-
isTopLevel: true,
469+
parentNodes: 'nodes'
471470
};
472471

473472
generator.blocks.push(block);

src/generators/dom/visitors/Component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,10 +234,10 @@ export default function visitComponent(
234234
);
235235

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

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

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

0 commit comments

Comments
 (0)