diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts index 0bc180e8d1..306fea312a 100644 --- a/src/generators/nodes/Element.ts +++ b/src/generators/nodes/Element.ts @@ -147,6 +147,8 @@ export default class Element extends Node { block.addDependencies(this.spread.metadata.dependencies); } + this.isComponentTarget = this.name === 'svelte:target'; + this.var = block.getUniqueName( this.name.replace(/[^a-zA-Z0-9_$]/g, '_') ); @@ -185,7 +187,9 @@ export default class Element extends Node { parentNode; block.addVariable(name); - const renderStatement = getRenderStatement(this.generator, this.namespace, this.name); + const renderStatement = this.isComponentTarget ? + `#component.options.target` : + getRenderStatement(this.generator, this.namespace, this.name); block.builders.create.addLine( `${name} = ${renderStatement};` ); @@ -203,20 +207,22 @@ export default class Element extends Node { } } - if (initialMountNode) { - block.builders.mount.addLine( - `@appendNode(${name}, ${initialMountNode});` - ); + if (!this.isComponentTarget) { + if (initialMountNode) { + block.builders.mount.addLine( + `@appendNode(${name}, ${initialMountNode});` + ); - if (initialMountNode === 'document.head') { + if (initialMountNode === 'document.head') { + block.builders.unmount.addLine(`@detachNode(${name});`); + } + } else { + block.builders.mount.addLine(`@insertNode(${name}, #target, anchor);`); + + // TODO we eventually need to consider what happens to elements + // that belong to the same outgroup as an outroing element... block.builders.unmount.addLine(`@detachNode(${name});`); } - } else { - block.builders.mount.addLine(`@insertNode(${name}, #target, anchor);`); - - // TODO we eventually need to consider what happens to elements - // that belong to the same outgroup as an outroing element... - block.builders.unmount.addLine(`@detachNode(${name});`); } // TODO move this into a class as well? @@ -785,6 +791,10 @@ export default class Element extends Node { } remount(name: string) { + if (this.isComponentTarget) { + return ''; + } + const slot = this.attributes.find(attribute => attribute.name === 'slot'); if (slot) { return `@appendNode(${this.var}, ${name}._slotted.${this.getStaticAttributeValue('slot')});`; diff --git a/src/validate/html/validateElement.ts b/src/validate/html/validateElement.ts index 2a32769797..2d0e58689a 100644 --- a/src/validate/html/validateElement.ts +++ b/src/validate/html/validateElement.ts @@ -85,6 +85,15 @@ export default function validateElement( }); } + if (node.name === 'svelte:target') { + if (node.children.length > 0) { + validator.error(node.children[0], { + code: `illegal-structure`, + message: ` cannot have children` + }); + } + } + let hasIntro: boolean; let hasOutro: boolean; let hasTransition: boolean; diff --git a/test/js/samples/target-attributes/expected-bundle.js b/test/js/samples/target-attributes/expected-bundle.js new file mode 100644 index 0000000000..d5fcc8d2bb --- /dev/null +++ b/test/js/samples/target-attributes/expected-bundle.js @@ -0,0 +1,171 @@ +function noop() {} + +function assign(tar, src) { + for (var k in src) tar[k] = src[k]; + return tar; +} + +function setAttribute(node, attribute, value) { + node.setAttribute(attribute, value); +} + +function blankObject() { + return Object.create(null); +} + +function destroy(detach) { + this.destroy = noop; + this.fire('destroy'); + this.set = this.get = noop; + + if (detach !== false) this._fragment.u(); + this._fragment.d(); + this._fragment = this._state = null; +} + +function _differs(a, b) { + return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function'); +} + +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) { + var handler = handlers[i]; + + if (!handler.__calling) { + handler.__calling = true; + handler.call(this, data); + handler.__calling = false; + } + } +} + +function get() { + return this._state; +} + +function init(component, options) { + component._handlers = blankObject(); + component._bind = options._bind; + + component.options = options; + component.root = options.root || component; + component.store = component.root.store || options.store; +} + +function on(eventName, 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 (this._differs(newState[key], oldState[key])) changed[key] = dirty = true; + } + if (!dirty) return; + + this._state = assign(assign({}, oldState), newState); + this._recompute(changed, this._state); + if (this._bind) this._bind(changed, this._state); + + if (this._fragment) { + this.fire("state", { changed: changed, current: this._state, previous: oldState }); + this._fragment.p(changed, this._state); + this.fire("update", { changed: changed, current: this._state, previous: oldState }); + } +} + +function callAll(fns) { + while (fns && fns.length) fns.shift()(); +} + +function _mount(target, anchor) { + this._fragment[this._fragment.i ? 'i' : 'm'](target, anchor || null); +} + +function _unmount() { + if (this._fragment) this._fragment.u(); +} + +var proto = { + destroy, + get, + fire, + on, + set, + _recompute: noop, + _set, + _mount, + _unmount, + _differs +}; + +/* generated by Svelte vX.Y.Z */ + +function create_main_fragment(component, state) { + var svelte_target, svelte_target_href_value; + + return { + c: function create() { + svelte_target = component.options.target; + this.h(); + }, + + h: function hydrate() { + svelte_target.className = "addedClass"; + setAttribute(svelte_target, "href", svelte_target_href_value = "/element/" + state.element + "/" + state.id); + }, + + m: noop, + + p: function update(changed, state) { + if ((changed.element || changed.id) && svelte_target_href_value !== (svelte_target_href_value = "/element/" + state.element + "/" + state.id)) { + setAttribute(svelte_target, "href", svelte_target_href_value); + } + }, + + u: noop, + + d: noop + }; +} + +function SvelteComponent(options) { + init(this, options); + this._state = assign({}, options.data); + + this._fragment = create_main_fragment(this, this._state); + + if (options.target) { + this._fragment.c(); + this._mount(options.target, options.anchor); + } +} + +assign(SvelteComponent.prototype, proto); + +export default SvelteComponent; diff --git a/test/js/samples/target-attributes/expected.js b/test/js/samples/target-attributes/expected.js new file mode 100644 index 0000000000..5b9246ba98 --- /dev/null +++ b/test/js/samples/target-attributes/expected.js @@ -0,0 +1,45 @@ +/* generated by Svelte vX.Y.Z */ +import { assign, init, noop, proto, setAttribute } from "svelte/shared.js"; + +function create_main_fragment(component, state) { + var svelte_target, svelte_target_href_value; + + return { + c: function create() { + svelte_target = component.options.target; + this.h(); + }, + + h: function hydrate() { + svelte_target.className = "addedClass"; + setAttribute(svelte_target, "href", svelte_target_href_value = "/element/" + state.element + "/" + state.id); + }, + + m: noop, + + p: function update(changed, state) { + if ((changed.element || changed.id) && svelte_target_href_value !== (svelte_target_href_value = "/element/" + state.element + "/" + state.id)) { + setAttribute(svelte_target, "href", svelte_target_href_value); + } + }, + + u: noop, + + d: noop + }; +} + +function SvelteComponent(options) { + init(this, options); + this._state = assign({}, options.data); + + this._fragment = create_main_fragment(this, this._state); + + if (options.target) { + this._fragment.c(); + this._mount(options.target, options.anchor); + } +} + +assign(SvelteComponent.prototype, proto); +export default SvelteComponent; \ No newline at end of file diff --git a/test/js/samples/target-attributes/input.html b/test/js/samples/target-attributes/input.html new file mode 100644 index 0000000000..aa1387948e --- /dev/null +++ b/test/js/samples/target-attributes/input.html @@ -0,0 +1 @@ + diff --git a/test/js/samples/target-event-handler/expected-bundle.js b/test/js/samples/target-event-handler/expected-bundle.js new file mode 100644 index 0000000000..c5a1984990 --- /dev/null +++ b/test/js/samples/target-event-handler/expected-bundle.js @@ -0,0 +1,183 @@ +function noop() {} + +function assign(tar, src) { + for (var k in src) tar[k] = src[k]; + return tar; +} + +function addListener(node, event, handler) { + node.addEventListener(event, handler, false); +} + +function removeListener(node, event, handler) { + node.removeEventListener(event, handler, false); +} + +function blankObject() { + return Object.create(null); +} + +function destroy(detach) { + this.destroy = noop; + this.fire('destroy'); + this.set = this.get = noop; + + if (detach !== false) this._fragment.u(); + this._fragment.d(); + this._fragment = this._state = null; +} + +function _differs(a, b) { + return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function'); +} + +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) { + var handler = handlers[i]; + + if (!handler.__calling) { + handler.__calling = true; + handler.call(this, data); + handler.__calling = false; + } + } +} + +function get() { + return this._state; +} + +function init(component, options) { + component._handlers = blankObject(); + component._bind = options._bind; + + component.options = options; + component.root = options.root || component; + component.store = component.root.store || options.store; +} + +function on(eventName, 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 (this._differs(newState[key], oldState[key])) changed[key] = dirty = true; + } + if (!dirty) return; + + this._state = assign(assign({}, oldState), newState); + this._recompute(changed, this._state); + if (this._bind) this._bind(changed, this._state); + + if (this._fragment) { + this.fire("state", { changed: changed, current: this._state, previous: oldState }); + this._fragment.p(changed, this._state); + this.fire("update", { changed: changed, current: this._state, previous: oldState }); + } +} + +function callAll(fns) { + while (fns && fns.length) fns.shift()(); +} + +function _mount(target, anchor) { + this._fragment[this._fragment.i ? 'i' : 'm'](target, anchor || null); +} + +function _unmount() { + if (this._fragment) this._fragment.u(); +} + +var proto = { + destroy, + get, + fire, + on, + set, + _recompute: noop, + _set, + _mount, + _unmount, + _differs +}; + +/* generated by Svelte vX.Y.Z */ + +var methods = { + handleClick() { + alert('Clicked.'); + } +}; + +function create_main_fragment(component, state) { + var svelte_target; + + function click_handler(event) { + component.handleClick(); + } + + return { + c: function create() { + svelte_target = component.options.target; + this.h(); + }, + + h: function hydrate() { + addListener(svelte_target, "click", click_handler); + }, + + m: noop, + + p: noop, + + u: noop, + + d: function destroy$$1() { + removeListener(svelte_target, "click", click_handler); + } + }; +} + +function SvelteComponent(options) { + init(this, options); + this._state = assign({}, options.data); + + this._fragment = create_main_fragment(this, this._state); + + if (options.target) { + this._fragment.c(); + this._mount(options.target, options.anchor); + } +} + +assign(SvelteComponent.prototype, proto); +assign(SvelteComponent.prototype, methods); + +export default SvelteComponent; diff --git a/test/js/samples/target-event-handler/expected.js b/test/js/samples/target-event-handler/expected.js new file mode 100644 index 0000000000..60b2817dc3 --- /dev/null +++ b/test/js/samples/target-event-handler/expected.js @@ -0,0 +1,53 @@ +/* generated by Svelte vX.Y.Z */ +import { addListener, assign, init, noop, proto, removeListener } from "svelte/shared.js"; + +var methods = { + handleClick() { + alert('Clicked.'); + } +}; + +function create_main_fragment(component, state) { + var svelte_target; + + function click_handler(event) { + component.handleClick(); + } + + return { + c: function create() { + svelte_target = component.options.target; + this.h(); + }, + + h: function hydrate() { + addListener(svelte_target, "click", click_handler); + }, + + m: noop, + + p: noop, + + u: noop, + + d: function destroy() { + removeListener(svelte_target, "click", click_handler); + } + }; +} + +function SvelteComponent(options) { + init(this, options); + this._state = assign({}, options.data); + + this._fragment = create_main_fragment(this, this._state); + + if (options.target) { + this._fragment.c(); + this._mount(options.target, options.anchor); + } +} + +assign(SvelteComponent.prototype, proto); +assign(SvelteComponent.prototype, methods); +export default SvelteComponent; \ No newline at end of file diff --git a/test/js/samples/target-event-handler/input.html b/test/js/samples/target-event-handler/input.html new file mode 100644 index 0000000000..90993a3f26 --- /dev/null +++ b/test/js/samples/target-event-handler/input.html @@ -0,0 +1,11 @@ + + + diff --git a/test/validator/samples/target-children/errors.json b/test/validator/samples/target-children/errors.json new file mode 100644 index 0000000000..fec9a9b987 --- /dev/null +++ b/test/validator/samples/target-children/errors.json @@ -0,0 +1,15 @@ +[{ + "code": "illegal-structure", + "message": " cannot have children", + "start": { + "line": 1, + "column": 15, + "character": 15 + }, + "end": { + "line": 3, + "column": 0, + "character": 22 + }, + "pos": 15 +}] diff --git a/test/validator/samples/target-children/input.html b/test/validator/samples/target-children/input.html new file mode 100644 index 0000000000..432f10722b --- /dev/null +++ b/test/validator/samples/target-children/input.html @@ -0,0 +1,3 @@ + + Foo +