diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c15d0aaa2..002d5c7039 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Svelte changelog +## 2.4.1 + +* Fix DOM event context ([#1390](https://github.com/sveltejs/svelte/issues/1390)) + ## 2.4.0 * Integrate CLI ([#1360](https://github.com/sveltejs/svelte/issues/1360)) diff --git a/package.json b/package.json index 111f091eba..c2ede3bbad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "2.4.0", + "version": "2.4.1", "description": "The magical disappearing UI framework", "main": "compiler/svelte.js", "bin": { diff --git a/src/compile/nodes/Attribute.ts b/src/compile/nodes/Attribute.ts index 37de5068a9..b933ca2e7c 100644 --- a/src/compile/nodes/Attribute.ts +++ b/src/compile/nodes/Attribute.ts @@ -66,11 +66,7 @@ export default class Attribute extends Node { return expression; }); - // TODO this would be better, but it breaks some stuff - // this.isDynamic = this.dependencies.size > 0; - this.isDynamic = this.chunks.length === 1 - ? this.chunks[0].type !== 'Text' - : this.chunks.length > 1; + this.isDynamic = this.dependencies.size > 0; this.shouldCache = this.isDynamic ? this.chunks.length === 1 @@ -82,7 +78,7 @@ export default class Attribute extends Node { getValue() { if (this.isTrue) return true; - if (this.chunks.length === 0) return `''`; + if (this.chunks.length === 0) return `""`; if (this.chunks.length === 1) { return this.chunks[0].type === 'Text' @@ -252,9 +248,7 @@ export default class Attribute extends Node { ); } } else { - const value = this.isTrue - ? 'true' - : this.chunks.length === 0 ? `""` : stringify(this.chunks[0].data); + const value = this.getValue(); const statement = ( isLegacyInputType diff --git a/src/compile/nodes/Component.ts b/src/compile/nodes/Component.ts index 1a872fe6ce..9996c19a92 100644 --- a/src/compile/nodes/Component.ts +++ b/src/compile/nodes/Component.ts @@ -209,12 +209,6 @@ export default class Component extends Node { .join(' || ')}) ${name_changes}.${attribute.name} = ${attribute.getValue()}; `); } - - else { - // TODO this is an odd situation to encounter – I *think* it should only happen with - // each block indices, in which case it may be possible to optimise this - updates.push(`${name_changes}.${attribute.name} = ${attribute.getValue()};`); - } }); } } diff --git a/src/compile/nodes/EachBlock.ts b/src/compile/nodes/EachBlock.ts index 6d771dbf87..d9c93fc036 100644 --- a/src/compile/nodes/EachBlock.ts +++ b/src/compile/nodes/EachBlock.ts @@ -31,10 +31,6 @@ export default class EachBlock extends Node { this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring this.index = info.index; - this.key = info.key - ? new Expression(compiler, this, scope, info.key) - : null; - this.scope = scope.child(); this.contexts = []; @@ -44,6 +40,10 @@ export default class EachBlock extends Node { this.scope.add(context.key.name, this.expression.dependencies); }); + this.key = info.key + ? new Expression(compiler, this, this.scope, info.key) + : null; + if (this.index) { // index can only change if this is a keyed each block const dependencies = this.key ? this.expression.dependencies : []; diff --git a/src/compile/nodes/EventHandler.ts b/src/compile/nodes/EventHandler.ts index 06d63f7f97..54dbd66157 100644 --- a/src/compile/nodes/EventHandler.ts +++ b/src/compile/nodes/EventHandler.ts @@ -77,16 +77,18 @@ export default class EventHandler extends Node { } } - this.args.forEach(arg => { - arg.overwriteThis(this.parent.var); - }); - - if (this.isCustomEvent && this.callee && this.callee.name === 'this') { - const node = this.callee.nodes[0]; - compiler.code.overwrite(node.start, node.end, this.parent.var, { - storeName: true, - contentOnly: true + if (this.isCustomEvent) { + this.args.forEach(arg => { + arg.overwriteThis(this.parent.var); }); + + if (this.callee && this.callee.name === 'this') { + const node = this.callee.nodes[0]; + compiler.code.overwrite(node.start, node.end, this.parent.var, { + storeName: true, + contentOnly: true + }); + } } } } \ No newline at end of file diff --git a/src/shared/dom.js b/src/shared/dom.js index 54778d1aaf..2490fe0be8 100644 --- a/src/shared/dom.js +++ b/src/shared/dom.js @@ -203,8 +203,11 @@ export function addResizeListener(element, fn) { object.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;'); object.type = 'text/html'; + let win; + object.onload = () => { - object.contentDocument.defaultView.addEventListener('resize', fn); + win = object.contentDocument.defaultView; + win.addEventListener('resize', fn); }; if (/Trident/.test(navigator.userAgent)) { @@ -217,7 +220,7 @@ export function addResizeListener(element, fn) { return { cancel: () => { - object.contentDocument.defaultView.removeEventListener('resize', fn); + win.removeEventListener('resize', fn); element.removeChild(object); } }; diff --git a/store.js b/store.js index 42f17c9bb8..c5b569c6a8 100644 --- a/store.js +++ b/store.js @@ -49,29 +49,30 @@ assign(Store.prototype, { _sortComputedProperties: function() { var computed = this._computed; var sorted = this._sortedComputedProperties = []; - var cycles; var visited = blankObject(); + var currentKey; function visit(key) { - if (cycles[key]) { - throw new Error('Cyclical dependency detected'); - } - - if (visited[key]) return; - visited[key] = true; - var c = computed[key]; if (c) { - cycles[key] = true; - c.deps.forEach(visit); - sorted.push(c); + c.deps.forEach(dep => { + if (dep === currentKey) { + throw new Error(`Cyclical dependency detected between ${dep} <-> ${key}`); + } + + visit(dep); + }); + + if (!visited[key]) { + visited[key] = true; + sorted.push(c); + } } } for (var key in this._computed) { - cycles = blankObject(); - visit(key); + visit(currentKey = key); } }, diff --git a/test/js/samples/bind-width-height/expected-bundle.js b/test/js/samples/bind-width-height/expected-bundle.js index 4d003e7e85..0787252e8d 100644 --- a/test/js/samples/bind-width-height/expected-bundle.js +++ b/test/js/samples/bind-width-height/expected-bundle.js @@ -26,8 +26,11 @@ function addResizeListener(element, fn) { object.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;'); object.type = 'text/html'; + let win; + object.onload = () => { - object.contentDocument.defaultView.addEventListener('resize', fn); + win = object.contentDocument.defaultView; + win.addEventListener('resize', fn); }; if (/Trident/.test(navigator.userAgent)) { @@ -40,7 +43,7 @@ function addResizeListener(element, fn) { return { cancel: () => { - object.contentDocument.defaultView.removeEventListener('resize', fn); + win.removeEventListener('resize', fn); element.removeChild(object); } }; diff --git a/test/js/samples/component-static-array/expected-bundle.js b/test/js/samples/component-static-array/expected-bundle.js new file mode 100644 index 0000000000..a210f5ce42 --- /dev/null +++ b/test/js/samples/component-static-array/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 blankObject() { + return Object.create(null); +} + +function destroy(detach) { + this.destroy = noop; + this.fire('destroy'); + this.set = noop; + + if (detach !== false) this._fragment.u(); + this._fragment.d(); + this._fragment = null; + this._state = {}; +} + +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 Nested = window.Nested; + +function create_main_fragment(component, ctx) { + + var nested_initial_data = { foo: [1, 2, 3] }; + var nested = new Nested({ + root: component.root, + data: nested_initial_data + }); + + return { + c() { + nested._fragment.c(); + }, + + m(target, anchor) { + nested._mount(target, anchor); + }, + + p: noop, + + u() { + nested._unmount(); + }, + + d() { + nested.destroy(false); + } + }; +} + +function SvelteComponent(options) { + init(this, options); + this._state = assign({}, options.data); + + if (!options.root) { + this._oncreate = []; + this._beforecreate = []; + this._aftercreate = []; + } + + this._fragment = create_main_fragment(this, this._state); + + if (options.target) { + this._fragment.c(); + this._mount(options.target, options.anchor); + + this._lock = true; + callAll(this._beforecreate); + callAll(this._oncreate); + callAll(this._aftercreate); + this._lock = false; + } +} + +assign(SvelteComponent.prototype, proto); + +export default SvelteComponent; diff --git a/test/js/samples/component-static-array/expected.js b/test/js/samples/component-static-array/expected.js new file mode 100644 index 0000000000..cba2d4d947 --- /dev/null +++ b/test/js/samples/component-static-array/expected.js @@ -0,0 +1,60 @@ +/* generated by Svelte vX.Y.Z */ +import { assign, callAll, init, noop, proto } from "svelte/shared.js"; + +var Nested = window.Nested; + +function create_main_fragment(component, ctx) { + + var nested_initial_data = { foo: [1, 2, 3] }; + var nested = new Nested({ + root: component.root, + data: nested_initial_data + }); + + return { + c() { + nested._fragment.c(); + }, + + m(target, anchor) { + nested._mount(target, anchor); + }, + + p: noop, + + u() { + nested._unmount(); + }, + + d() { + nested.destroy(false); + } + }; +} + +function SvelteComponent(options) { + init(this, options); + this._state = assign({}, options.data); + + if (!options.root) { + this._oncreate = []; + this._beforecreate = []; + this._aftercreate = []; + } + + this._fragment = create_main_fragment(this, this._state); + + if (options.target) { + this._fragment.c(); + this._mount(options.target, options.anchor); + + this._lock = true; + callAll(this._beforecreate); + callAll(this._oncreate); + callAll(this._aftercreate); + this._lock = false; + } +} + +assign(SvelteComponent.prototype, proto); +export default SvelteComponent; \ No newline at end of file diff --git a/test/js/samples/component-static-array/input.html b/test/js/samples/component-static-array/input.html new file mode 100644 index 0000000000..f87ea0a7e5 --- /dev/null +++ b/test/js/samples/component-static-array/input.html @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/test/runtime/samples/dev-warning-missing-data-each/_config.js b/test/runtime/samples/dev-warning-missing-data-each/_config.js new file mode 100644 index 0000000000..a8eb59216a --- /dev/null +++ b/test/runtime/samples/dev-warning-missing-data-each/_config.js @@ -0,0 +1,22 @@ +export default { + dev: true, + + data: { + letters: [ + { + id: 1, + char: 'a', + }, + { + id: 2, + char: 'b', + }, + { + id: 3, + char: 'c', + }, + ], + }, + + warnings: [], +}; diff --git a/test/runtime/samples/dev-warning-missing-data-each/main.html b/test/runtime/samples/dev-warning-missing-data-each/main.html new file mode 100644 index 0000000000..6b6a72204e --- /dev/null +++ b/test/runtime/samples/dev-warning-missing-data-each/main.html @@ -0,0 +1,3 @@ +{#each letters as letter (letter.id)} +
{letter.char}
+{/each} \ No newline at end of file diff --git a/test/runtime/samples/event-handler-each-this/_config.js b/test/runtime/samples/event-handler-each-this/_config.js new file mode 100644 index 0000000000..e6d1056e7e --- /dev/null +++ b/test/runtime/samples/event-handler-each-this/_config.js @@ -0,0 +1,28 @@ +export default { + data: { + items: ['foo', 'bar', 'baz'], + }, + + html: ` + + + + `, + + test(assert, component, target, window) { + const buttons = target.querySelectorAll('button'); + const event = new window.MouseEvent('click'); + + const clicked = []; + + component.on('clicked', event => { + clicked.push(event.node); + }); + + buttons[1].dispatchEvent(event); + + assert.equal(clicked.length, 1); + assert.equal(clicked[0].nodeName, 'BUTTON'); + assert.equal(clicked[0].textContent, 'bar'); + } +}; diff --git a/test/runtime/samples/event-handler-each-this/main.html b/test/runtime/samples/event-handler-each-this/main.html new file mode 100644 index 0000000000..9e5ea88a50 --- /dev/null +++ b/test/runtime/samples/event-handler-each-this/main.html @@ -0,0 +1,3 @@ +{#each items as item} + +{/each} \ No newline at end of file diff --git a/test/store/index.js b/test/store/index.js index 8bddb046f1..8d5dd5749c 100644 --- a/test/store/index.js +++ b/test/store/index.js @@ -141,8 +141,27 @@ describe('store', () => { assert.throws(() => { store.compute('a', ['b'], b => b + 1); - store.compute('b', ['a'], a => a + 1); - }, /Cyclical dependency detected/); + store.compute('b', ['c'], c => c + 1); + store.compute('c', ['a'], a => a + 1); + }, /Cyclical dependency detected between a <-> c/); + }); + + it('does not falsely report cycles', () => { + const store = new Store(); + + store.compute('dep4', ['dep1', 'dep2', 'dep3'], (...args) => ['dep4'].concat(...args)); + store.compute('dep1', ['source'], (...args) => ['dep1'].concat(...args)); + store.compute('dep2', ['dep1'], (...args) => ['dep2'].concat(...args)); + store.compute('dep3', ['dep1', 'dep2'], (...args) => ['dep3'].concat(...args)); + store.set({source: 'source'}); + + assert.deepEqual(store.get().dep4, [ + 'dep4', + 'dep1', 'source', + 'dep2', 'dep1', 'source', + 'dep3', 'dep1', 'source', + 'dep2', 'dep1', 'source' + ]); }); });