basic custom element generation (#797)

pull/811/head
Rich Harris 7 years ago
parent 89c0b71e25
commit afe3e2e669

@ -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",

@ -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;
@ -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>;
@ -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) {
@ -554,6 +571,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) {
// TODO
this.props = templateProperties.props.value;
}
// 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) {

@ -148,88 +148,110 @@ export default function dom(
.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 )
};
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 || {};`}
const target = generator.customElement ? `this.attachShadow({ mode: 'open' })` : `options.target`;
const anchor = generator.customElement ? `null` : `options.anchor || null`;
const constructorBody = deindent`
${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 )
};
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.stylesheet.hasStyles &&
options.css !== false &&
`if ( !document.getElementById( '${generator.stylesheet.id}-style' ) ) @add_css();`}
${templateProperties.oncreate && `var oncreate = @template.oncreate.bind( 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);
}
`}
`}
${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._root ) {
${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'}( ${target}, ${anchor} );
}
${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 );
${(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;`}
}
`}
`;
${(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;`}
if (generator.customElement) {
builder.addBlock(deindent`
class ${name} extends HTMLElement {
constructor(options = {}) {
super();
${constructorBody}
}
`}
}
}
customElements.define('${generator.tag}', ${name});
`);
} else {
builder.addBlock(deindent`
function ${name} ( options ) {
${constructorBody}
}
`);
}
// TODO deprecate component.teardown()
builder.addBlock(deindent`
@assign( ${prototypeBase}, ${proto});
${options.dev && deindent`

@ -54,6 +54,7 @@ export interface CompileOptions {
cascade?: boolean;
hydratable?: boolean;
legacy?: boolean;
customElement: CustomElementOptions | true;
onerror?: (error: Error) => void;
onwarn?: (warning: Warning) => void;
@ -67,4 +68,9 @@ export interface GenerateOptions {
export interface Visitor {
enter: (node: Node) => void;
leave?: (node: Node) => void;
}
export interface CustomElementOptions {
tag?: string;
props?: string[];
}

@ -9,6 +9,7 @@ import methods from './methods';
import components from './components';
import events from './events';
import namespace from './namespace';
import tag from './tag';
import transitions from './transitions';
import setup from './setup';
@ -24,6 +25,7 @@ export default {
components,
events,
namespace,
tag,
transitions,
setup,
};

@ -0,0 +1,20 @@
import usesThisOrArguments from '../utils/usesThisOrArguments';
import { Validator } from '../../';
import { Node } from '../../../interfaces';
export default function tag(validator: Validator, prop: Node) {
if (prop.value.type !== 'Literal' || typeof prop.value.value !== 'string') {
validator.error(
`'tag' must be a string literal`,
prop.value.start
);
}
const tag = prop.value.value;
if (!/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) {
validator.error(
`tag name must be two or more words joined by the '-' character`,
prop.value.start
);
}
}

@ -0,0 +1,5 @@
export default {
options: {
customElement: true
}
};

@ -0,0 +1,218 @@
function noop() {}
function assign(target) {
var k,
source,
i = 1,
len = arguments.length;
for (; i < len; i++) {
source = arguments[i];
for (k in source) target[k] = source[k];
}
return target;
}
function appendNode(node, target) {
target.appendChild(node);
}
function insertNode(node, target, anchor) {
target.insertBefore(node, anchor);
}
function detachNode(node) {
node.parentNode.removeChild(node);
}
function createElement(name) {
return document.createElement(name);
}
function createText(data) {
return document.createTextNode(data);
}
function destroy(detach) {
this.destroy = noop;
this.fire('destroy');
this.set = this.get = noop;
if (detach !== false) this._fragment.unmount();
this._fragment.destroy();
this._fragment = this._state = null;
}
function differs(a, b) {
return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}
function dispatchObservers(component, group, changed, newState, oldState) {
for (var key in group) {
if (!changed[key]) continue;
var newValue = newState[key];
var oldValue = oldState[key];
var callbacks = group[key];
if (!callbacks) continue;
for (var i = 0; i < callbacks.length; i += 1) {
var callback = callbacks[i];
if (callback.__calling) continue;
callback.__calling = true;
callback.call(component, newValue, oldValue);
callback.__calling = false;
}
}
}
function get(key) {
return key ? this._state[key] : this._state;
}
function fire(eventName, data) {
var handlers =
eventName in this._handlers && this._handlers[eventName].slice();
if (!handlers) return;
for (var i = 0; i < handlers.length; i += 1) {
handlers[i].call(this, data);
}
}
function observe(key, callback, options) {
var group = options && options.defer
? this._observers.post
: this._observers.pre;
(group[key] || (group[key] = [])).push(callback);
if (!options || options.init !== false) {
callback.__calling = true;
callback.call(this, this._state[key]);
callback.__calling = false;
}
return {
cancel: function() {
var index = group[key].indexOf(callback);
if (~index) group[key].splice(index, 1);
}
};
}
function on(eventName, handler) {
if (eventName === 'teardown') return this.on('destroy', handler);
var handlers = this._handlers[eventName] || (this._handlers[eventName] = []);
handlers.push(handler);
return {
cancel: function() {
var index = handlers.indexOf(handler);
if (~index) handlers.splice(index, 1);
}
};
}
function set(newState) {
this._set(assign({}, newState));
if (this._root._lock) return;
this._root._lock = true;
callAll(this._root._beforecreate);
callAll(this._root._oncreate);
callAll(this._root._aftercreate);
this._root._lock = false;
}
function _set(newState) {
var oldState = this._state,
changed = {},
dirty = false;
for (var key in newState) {
if (differs(newState[key], oldState[key])) changed[key] = dirty = true;
}
if (!dirty) return;
this._state = assign({}, oldState, newState);
this._recompute(changed, this._state, oldState, false);
if (this._bind) this._bind(changed, this._state);
dispatchObservers(this, this._observers.pre, changed, this._state, oldState);
this._fragment.update(changed, this._state);
dispatchObservers(this, this._observers.post, changed, this._state, oldState);
}
function callAll(fns) {
while (fns && fns.length) fns.pop()();
}
var proto = {
destroy: destroy,
get: get,
fire: fire,
observe: observe,
on: on,
set: set,
teardown: destroy,
_recompute: noop,
_set: _set
};
function create_main_fragment ( state, component ) {
var div, text;
return {
create: function () {
div = createElement( 'div' );
text = createText( "I am shadow DOM" );
},
mount: function ( target, anchor ) {
insertNode( div, target, anchor );
appendNode( text, div );
},
update: noop,
unmount: function () {
detachNode( div );
},
destroy: noop
};
}
class SvelteComponent extends HTMLElement {
constructor(options = {}) {
super();
this.options = options;
this._state = options.data || {};
this._observers = {
pre: Object.create( null ),
post: Object.create( null )
};
this._handlers = Object.create( null );
this._root = options._root || this;
this._yield = options._yield;
this._bind = options._bind;
this._fragment = create_main_fragment( this._state, this );
if ( !options._root ) {
this._fragment.create();
this._fragment.mount( this.attachShadow({ mode: 'open' }), null );
}
}
}
customElements.define('custom-element', SvelteComponent);
assign( SvelteComponent.prototype, proto );
export default SvelteComponent;

@ -0,0 +1,57 @@
import { appendNode, assign, createElement, createText, detachNode, insertNode, noop, proto } from "svelte/shared.js";
function create_main_fragment ( state, component ) {
var div, text;
return {
create: function () {
div = createElement( 'div' );
text = createText( "I am shadow DOM" );
},
mount: function ( target, anchor ) {
insertNode( div, target, anchor );
appendNode( text, div );
},
update: noop,
unmount: function () {
detachNode( div );
},
destroy: noop
};
}
class SvelteComponent extends HTMLElement {
constructor(options = {}) {
super();
this.options = options;
this._state = options.data || {};
this._observers = {
pre: Object.create( null ),
post: Object.create( null )
};
this._handlers = Object.create( null );
this._root = options._root || this;
this._yield = options._yield;
this._bind = options._bind;
this._fragment = create_main_fragment( this._state, this );
if ( !options._root ) {
this._fragment.create();
this._fragment.mount( this.attachShadow({ mode: 'open' }), null );
}
}
}
customElements.define('custom-element', SvelteComponent);
assign( SvelteComponent.prototype, proto );
export default SvelteComponent;

@ -0,0 +1,7 @@
<div>I am shadow DOM</div>
<script>
export default {
tag: 'custom-element'
};
</script>

@ -22,8 +22,6 @@ function getName(filename) {
return base[0].toUpperCase() + base.slice(1);
}
const Object_assign = Object.assign;
describe("runtime", () => {
before(() => {
svelte = loadSvelte(true);

Loading…
Cancel
Save