From d0e5098ea639c7e0c7026591e436c4af73f52b02 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Nov 2017 12:50:12 -0500 Subject: [PATCH 01/19] implement Store --- .eslintignore | 1 + store.js | 87 +++++++++++++++++++++++++++++++++++++++++ test/store/index.js | 95 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 store.js create mode 100644 test/store/index.js diff --git a/.eslintignore b/.eslintignore index bc31435419cc..4a113378ce2c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ src/shared shared.js +store.js test/test.js test/setup.js **/_actual.js diff --git a/store.js b/store.js new file mode 100644 index 000000000000..4fee967aafbb --- /dev/null +++ b/store.js @@ -0,0 +1,87 @@ +import { + assign, + blankObject, + differs, + dispatchObservers, + get, + observe +} from './shared.js'; + +function Store(state) { + this._state = state ? assign({}, state) : {}; + this._observers = { pre: blankObject(), post: blankObject() }; + this._changeHandlers = []; + this._dependents = []; +} + +assign(Store.prototype, { + get, + observe +}, { + _add: function(component, props) { + this._dependents.push({ + component: component, + props: props + }); + }, + + _remove: function(component) { + let i = this._dependents.length; + while (i--) { + if (this._dependents[i].component === component) { + this._dependents.splice(i, 1); + return; + } + } + }, + + onchange: function(callback) { + this._changeHandlers.push(callback); + return { + cancel: function() { + var index = this._changeHandlers.indexOf(callback); + if (~index) this._changeHandlers.splice(index, 1); + } + }; + }, + + set: function(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); + + for (var i = 0; i < this._changeHandlers.length; i += 1) { + this._changeHandlers[i](this._state, changed); + } + + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + + var dependents = this._dependents.slice(); // guard against mutations + for (var i = 0; i < dependents.length; i += 1) { + var dependent = dependents[i]; + var componentState = {}; + dirty = false; + + for (var j = 0; j < dependent.props.length; j += 1) { + var prop = dependent.props[j]; + if (prop in changed) { + componentState['$' + prop] = this._state[prop]; + dirty = true; + } + } + + if (dirty) dependent.component.set(componentState); + } + + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } +}); + +export default Store; \ No newline at end of file diff --git a/test/store/index.js b/test/store/index.js new file mode 100644 index 000000000000..414e49bae4fd --- /dev/null +++ b/test/store/index.js @@ -0,0 +1,95 @@ +import assert from 'assert'; +import Store from '../../store.js'; + +describe('store', () => { + describe('get', () => { + it('gets a specific key', () => { + const store = new Store({ + foo: 'bar' + }); + + assert.equal(store.get('foo'), 'bar'); + }); + + it('gets the entire state object', () => { + const store = new Store({ + foo: 'bar' + }); + + assert.deepEqual(store.get(), { foo: 'bar' }); + }); + }); + + describe('set', () => { + it('sets state', () => { + const store = new Store(); + + store.set({ + foo: 'bar' + }); + + assert.equal(store.get('foo'), 'bar'); + }); + }); + + describe('observe', () => { + it('observes state', () => { + let newFoo; + let oldFoo; + + const store = new Store({ + foo: 'bar' + }); + + store.observe('foo', (n, o) => { + newFoo = n; + oldFoo = o; + }); + + assert.equal(newFoo, 'bar'); + assert.equal(oldFoo, undefined); + + store.set({ + foo: 'baz' + }); + + assert.equal(newFoo, 'baz'); + assert.equal(oldFoo, 'bar'); + }); + }); + + describe('onchange', () => { + it('fires a callback when state changes', () => { + const store = new Store(); + + let count = 0; + let args; + + store.onchange((state, changed) => { + count += 1; + args = { state, changed }; + }); + + store.set({ foo: 'bar' }); + + assert.equal(count, 1); + assert.deepEqual(args, { + state: { foo: 'bar' }, + changed: { foo: true } + }); + + // this should be a noop + store.set({ foo: 'bar' }); + assert.equal(count, 1); + + // this shouldn't + store.set({ foo: 'baz' }); + + assert.equal(count, 2); + assert.deepEqual(args, { + state: { foo: 'baz' }, + changed: { foo: true } + }); + }); + }); +}); From 2705532cb013adbba38bb155ecce5519a37a5200 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Nov 2017 13:56:32 -0500 Subject: [PATCH 02/19] add _init method --- store.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/store.js b/store.js index 4fee967aafbb..3a4a516e466d 100644 --- a/store.js +++ b/store.js @@ -25,6 +25,15 @@ assign(Store.prototype, { }); }, + _init: function(props) { + var state = {}; + for (let i = 0; i < props.length; i += 1) { + var prop = props[i]; + state['$' + prop] = this._state[prop]; + } + return state; + }, + _remove: function(component) { let i = this._dependents.length; while (i--) { From f80ace5fd6c22a533ee0e5be82ecf4783d40cbcd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Nov 2017 13:57:07 -0500 Subject: [PATCH 03/19] client-side store subscriptions --- src/generators/dom/index.ts | 19 ++++++++++++++++--- src/interfaces.ts | 1 + src/shared/index.js | 9 +++++++-- test/runtime/index.js | 12 +++++++----- test/runtime/samples/store/_config.js | 19 +++++++++++++++++++ test/runtime/samples/store/main.html | 1 + 6 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 test/runtime/samples/store/_config.js create mode 100644 test/runtime/samples/store/main.html diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index f6c007c79dd1..2bf0c21c4be5 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -184,15 +184,23 @@ export default function dom( const debugName = `<${generator.customElement ? generator.tag : name}>`; // generate initial state object - const globals = Array.from(generator.expectedProperties).filter(prop => globalWhitelist.has(prop)); + const expectedProperties = Array.from(generator.expectedProperties); + const globals = expectedProperties.filter(prop => globalWhitelist.has(prop)); + const storeProps = options.store ? expectedProperties.filter(prop => prop[0] === '$') : []; + const initialState = []; + if (globals.length > 0) { initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`); } + if (storeProps.length > 0) { + initialState.push(`this.store._init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`); + } + if (templateProperties.data) { initialState.push(`%data()`); - } else if (globals.length === 0) { + } else if (globals.length === 0 && storeProps.length === 0) { initialState.push('{}'); } @@ -205,6 +213,7 @@ export default function dom( @init(this, options); ${generator.usesRefs && `this.refs = {};`} this._state = @assign(${initialState.join(', ')}); + ${storeProps.length > 0 && `this.store._add(this, [${storeProps.map(prop => `"${prop.slice(1)}"`)}]);`} ${generator.metaBindings} ${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`} ${options.dev && @@ -215,7 +224,11 @@ export default function dom( ${generator.bindingGroups.length && `this._bindingGroups = [${Array(generator.bindingGroups.length).fill('[]').join(', ')}];`} - ${templateProperties.ondestroy && `this._handlers.destroy = [%ondestroy]`} + ${(templateProperties.ondestroy || storeProps.length) && ( + `this._handlers.destroy = [${ + [templateProperties.ondestroy && `%ondestroy`, storeProps.length && `@removeFromStore`].filter(Boolean).join(', ') + }]` + )} ${generator.slots.size && `this._slotted = options.slots || {};`} diff --git a/src/interfaces.ts b/src/interfaces.ts index fff52e8da4c6..3a9280df3cf1 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -56,6 +56,7 @@ export interface CompileOptions { legacy?: boolean; customElement?: CustomElementOptions | true; css?: boolean; + store?: boolean; onerror?: (error: Error) => void; onwarn?: (warning: Warning) => void; diff --git a/src/shared/index.js b/src/shared/index.js index f9d0f919989f..9825972b8289 100644 --- a/src/shared/index.js +++ b/src/shared/index.js @@ -65,12 +65,13 @@ export function get(key) { } export function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } export function observe(key, callback, options) { @@ -187,6 +188,10 @@ export function _unmount() { this._fragment.u(); } +export function removeFromStore() { + this.store._remove(this); +} + export var proto = { destroy: destroy, get: get, diff --git a/test/runtime/index.js b/test/runtime/index.js index 122fe645b63d..a82f5b69750b 100644 --- a/test/runtime/index.js +++ b/test/runtime/index.js @@ -63,6 +63,7 @@ describe("runtime", () => { compileOptions.shared = shared; compileOptions.hydratable = hydrate; compileOptions.dev = config.dev; + compileOptions.store = !!config.store; // check that no ES2015+ syntax slipped in if (!config.allowES2015) { @@ -88,7 +89,7 @@ describe("runtime", () => { } } catch (err) { failed.add(dir); - showOutput(cwd, { shared }, svelte); // eslint-disable-line no-console + showOutput(cwd, { shared, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console throw err; } } @@ -134,7 +135,7 @@ describe("runtime", () => { try { SvelteComponent = require(`./samples/${dir}/main.html`); } catch (err) { - showOutput(cwd, { shared, hydratable: hydrate }, svelte); // eslint-disable-line no-console + showOutput(cwd, { shared, hydratable: hydrate, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console throw err; } @@ -154,7 +155,8 @@ describe("runtime", () => { const options = Object.assign({}, { target, hydrate, - data: config.data + data: config.data, + store: config.store }, config.options || {}); const component = new SvelteComponent(options); @@ -188,12 +190,12 @@ describe("runtime", () => { config.error(assert, err); } else { failed.add(dir); - showOutput(cwd, { shared, hydratable: hydrate }, svelte); // eslint-disable-line no-console + showOutput(cwd, { shared, hydratable: hydrate, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console throw err; } } - if (config.show) showOutput(cwd, { shared, hydratable: hydrate }, svelte); + if (config.show) showOutput(cwd, { shared, hydratable: hydrate, store: !!compileOptions.store }, svelte); }); } diff --git a/test/runtime/samples/store/_config.js b/test/runtime/samples/store/_config.js new file mode 100644 index 000000000000..55d01fc125a8 --- /dev/null +++ b/test/runtime/samples/store/_config.js @@ -0,0 +1,19 @@ +import Store from '../../../../store.js'; + +const store = new Store({ + name: 'world' +}); + +export default { + solo: true, + + store, + + html: `

Hello world!

`, + + test(assert, component, target) { + store.set({ name: 'everybody' }); + + assert.htmlEqual(target.innerHTML, `

Hello everybody!

`); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/store/main.html b/test/runtime/samples/store/main.html new file mode 100644 index 000000000000..28154934b8b0 --- /dev/null +++ b/test/runtime/samples/store/main.html @@ -0,0 +1 @@ +

Hello {{$name}}!

\ No newline at end of file From 75e911b05a6e25355a0ab8f80808e92465aeb6bb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Nov 2017 14:23:45 -0500 Subject: [PATCH 04/19] update snapshot tests --- .../collapses-text-around-comments/expected-bundle.js | 5 +++-- test/js/samples/component-static/expected-bundle.js | 5 +++-- test/js/samples/computed-collapsed-if/expected-bundle.js | 5 +++-- test/js/samples/css-media-query/expected-bundle.js | 5 +++-- test/js/samples/css-shadow-dom-keyframes/expected-bundle.js | 5 +++-- test/js/samples/do-use-dataset/expected-bundle.js | 5 +++-- .../js/samples/dont-use-dataset-in-legacy/expected-bundle.js | 5 +++-- test/js/samples/each-block-changed-check/expected-bundle.js | 5 +++-- test/js/samples/event-handlers-custom/expected-bundle.js | 5 +++-- test/js/samples/if-block-no-update/expected-bundle.js | 5 +++-- test/js/samples/if-block-simple/expected-bundle.js | 5 +++-- .../inline-style-optimized-multiple/expected-bundle.js | 5 +++-- .../js/samples/inline-style-optimized-url/expected-bundle.js | 5 +++-- test/js/samples/inline-style-optimized/expected-bundle.js | 5 +++-- test/js/samples/inline-style-unoptimized/expected-bundle.js | 5 +++-- .../samples/input-without-blowback-guard/expected-bundle.js | 5 +++-- test/js/samples/legacy-input-type/expected-bundle.js | 5 +++-- test/js/samples/legacy-quote-class/expected-bundle.js | 5 +++-- test/js/samples/media-bindings/expected-bundle.js | 5 +++-- test/js/samples/non-imported-component/expected-bundle.js | 5 +++-- .../samples/onrender-onteardown-rewritten/expected-bundle.js | 5 +++-- test/js/samples/setup-method/expected-bundle.js | 5 +++-- test/js/samples/use-elements-as-anchors/expected-bundle.js | 5 +++-- test/js/samples/window-binding-scroll/expected-bundle.js | 5 +++-- 24 files changed, 72 insertions(+), 48 deletions(-) diff --git a/test/js/samples/collapses-text-around-comments/expected-bundle.js b/test/js/samples/collapses-text-around-comments/expected-bundle.js index a5776bc19519..c54a4508e72e 100644 --- a/test/js/samples/collapses-text-around-comments/expected-bundle.js +++ b/test/js/samples/collapses-text-around-comments/expected-bundle.js @@ -91,12 +91,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/component-static/expected-bundle.js b/test/js/samples/component-static/expected-bundle.js index 94f69ca2cac8..27d4a5d5df62 100644 --- a/test/js/samples/component-static/expected-bundle.js +++ b/test/js/samples/component-static/expected-bundle.js @@ -67,12 +67,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/computed-collapsed-if/expected-bundle.js b/test/js/samples/computed-collapsed-if/expected-bundle.js index c378d78026d5..9129d64ebfda 100644 --- a/test/js/samples/computed-collapsed-if/expected-bundle.js +++ b/test/js/samples/computed-collapsed-if/expected-bundle.js @@ -67,12 +67,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/css-media-query/expected-bundle.js b/test/js/samples/css-media-query/expected-bundle.js index 11ffa87f2170..3eb06a7baaae 100644 --- a/test/js/samples/css-media-query/expected-bundle.js +++ b/test/js/samples/css-media-query/expected-bundle.js @@ -87,12 +87,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js b/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js index 86edac7465f3..a8a14c254a69 100644 --- a/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js +++ b/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js @@ -79,12 +79,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/do-use-dataset/expected-bundle.js b/test/js/samples/do-use-dataset/expected-bundle.js index 42de89fd93ef..1baf806a5223 100644 --- a/test/js/samples/do-use-dataset/expected-bundle.js +++ b/test/js/samples/do-use-dataset/expected-bundle.js @@ -83,12 +83,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/dont-use-dataset-in-legacy/expected-bundle.js b/test/js/samples/dont-use-dataset-in-legacy/expected-bundle.js index 2d25e23a1029..3ebad459d3df 100644 --- a/test/js/samples/dont-use-dataset-in-legacy/expected-bundle.js +++ b/test/js/samples/dont-use-dataset-in-legacy/expected-bundle.js @@ -87,12 +87,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/each-block-changed-check/expected-bundle.js b/test/js/samples/each-block-changed-check/expected-bundle.js index c9723db6160b..dae7830ecbb7 100644 --- a/test/js/samples/each-block-changed-check/expected-bundle.js +++ b/test/js/samples/each-block-changed-check/expected-bundle.js @@ -99,12 +99,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/event-handlers-custom/expected-bundle.js b/test/js/samples/event-handlers-custom/expected-bundle.js index 46c109100aa4..b1974ae869ea 100644 --- a/test/js/samples/event-handlers-custom/expected-bundle.js +++ b/test/js/samples/event-handlers-custom/expected-bundle.js @@ -79,12 +79,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/if-block-no-update/expected-bundle.js b/test/js/samples/if-block-no-update/expected-bundle.js index 0277095a8360..12822a4ed3bd 100644 --- a/test/js/samples/if-block-no-update/expected-bundle.js +++ b/test/js/samples/if-block-no-update/expected-bundle.js @@ -83,12 +83,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/if-block-simple/expected-bundle.js b/test/js/samples/if-block-simple/expected-bundle.js index 40acbe39598c..098526057995 100644 --- a/test/js/samples/if-block-simple/expected-bundle.js +++ b/test/js/samples/if-block-simple/expected-bundle.js @@ -83,12 +83,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/inline-style-optimized-multiple/expected-bundle.js b/test/js/samples/inline-style-optimized-multiple/expected-bundle.js index eff6f53cfa7e..6260246cc081 100644 --- a/test/js/samples/inline-style-optimized-multiple/expected-bundle.js +++ b/test/js/samples/inline-style-optimized-multiple/expected-bundle.js @@ -83,12 +83,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/inline-style-optimized-url/expected-bundle.js b/test/js/samples/inline-style-optimized-url/expected-bundle.js index 3a126946ad4c..921d377bf92c 100644 --- a/test/js/samples/inline-style-optimized-url/expected-bundle.js +++ b/test/js/samples/inline-style-optimized-url/expected-bundle.js @@ -83,12 +83,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/inline-style-optimized/expected-bundle.js b/test/js/samples/inline-style-optimized/expected-bundle.js index 0c127a639145..8ddc55d2155a 100644 --- a/test/js/samples/inline-style-optimized/expected-bundle.js +++ b/test/js/samples/inline-style-optimized/expected-bundle.js @@ -83,12 +83,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/inline-style-unoptimized/expected-bundle.js b/test/js/samples/inline-style-unoptimized/expected-bundle.js index d6f545111a41..c3ca9e695714 100644 --- a/test/js/samples/inline-style-unoptimized/expected-bundle.js +++ b/test/js/samples/inline-style-unoptimized/expected-bundle.js @@ -83,12 +83,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/input-without-blowback-guard/expected-bundle.js b/test/js/samples/input-without-blowback-guard/expected-bundle.js index 1918f9f6d109..a7a7d25e0c7c 100644 --- a/test/js/samples/input-without-blowback-guard/expected-bundle.js +++ b/test/js/samples/input-without-blowback-guard/expected-bundle.js @@ -87,12 +87,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/legacy-input-type/expected-bundle.js b/test/js/samples/legacy-input-type/expected-bundle.js index 7211738f4526..54a4c3c02d37 100644 --- a/test/js/samples/legacy-input-type/expected-bundle.js +++ b/test/js/samples/legacy-input-type/expected-bundle.js @@ -85,12 +85,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/legacy-quote-class/expected-bundle.js b/test/js/samples/legacy-quote-class/expected-bundle.js index 1575bf40643b..c6f7bb8405c1 100644 --- a/test/js/samples/legacy-quote-class/expected-bundle.js +++ b/test/js/samples/legacy-quote-class/expected-bundle.js @@ -102,12 +102,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/media-bindings/expected-bundle.js b/test/js/samples/media-bindings/expected-bundle.js index 80545b940234..bdac2a8f2a43 100644 --- a/test/js/samples/media-bindings/expected-bundle.js +++ b/test/js/samples/media-bindings/expected-bundle.js @@ -95,12 +95,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/non-imported-component/expected-bundle.js b/test/js/samples/non-imported-component/expected-bundle.js index 575be613fabd..fd201d65b3fb 100644 --- a/test/js/samples/non-imported-component/expected-bundle.js +++ b/test/js/samples/non-imported-component/expected-bundle.js @@ -81,12 +81,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js b/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js index f1e701bd04cf..364ee2901d19 100644 --- a/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js +++ b/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js @@ -67,12 +67,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/setup-method/expected-bundle.js b/test/js/samples/setup-method/expected-bundle.js index 0c545ea70c80..df267db975a7 100644 --- a/test/js/samples/setup-method/expected-bundle.js +++ b/test/js/samples/setup-method/expected-bundle.js @@ -67,12 +67,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/use-elements-as-anchors/expected-bundle.js b/test/js/samples/use-elements-as-anchors/expected-bundle.js index 8805cc3847dc..272e03977275 100644 --- a/test/js/samples/use-elements-as-anchors/expected-bundle.js +++ b/test/js/samples/use-elements-as-anchors/expected-bundle.js @@ -91,12 +91,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/window-binding-scroll/expected-bundle.js b/test/js/samples/window-binding-scroll/expected-bundle.js index 6c357ef7321d..3a11deb1b695 100644 --- a/test/js/samples/window-binding-scroll/expected-bundle.js +++ b/test/js/samples/window-binding-scroll/expected-bundle.js @@ -87,12 +87,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { From f64e473d2e14ed9004cf80536d5240117445b301 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Nov 2017 14:26:46 -0500 Subject: [PATCH 05/19] reenable all tests --- test/runtime/samples/store/_config.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/runtime/samples/store/_config.js b/test/runtime/samples/store/_config.js index 55d01fc125a8..0e1663315b70 100644 --- a/test/runtime/samples/store/_config.js +++ b/test/runtime/samples/store/_config.js @@ -5,8 +5,6 @@ const store = new Store({ }); export default { - solo: true, - store, html: `

Hello world!

`, From be68cd9de2509152ed847861262668d7bc87abce Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Nov 2017 14:49:16 -0500 Subject: [PATCH 06/19] use store in SSR mode --- src/generators/server-side-rendering/index.ts | 14 ++++++++++---- .../server-side-rendering/visitors/Component.ts | 11 ++++++++++- src/server-side-rendering/register.js | 12 ++++++++++-- .../samples/ssr-no-oncreate-etc/expected-bundle.js | 2 +- test/js/samples/ssr-no-oncreate-etc/expected.js | 2 +- test/server-side-rendering/index.js | 11 +++++++++-- 6 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts index 2accc2bc7e8f..c6741cb958b1 100644 --- a/src/generators/server-side-rendering/index.ts +++ b/src/generators/server-side-rendering/index.ts @@ -73,16 +73,22 @@ export default function ssr( generator.stylesheet.render(options.filename, true); // generate initial state object - // TODO this doesn't work, because expectedProperties isn't populated - const globals = Array.from(generator.expectedProperties).filter(prop => globalWhitelist.has(prop)); + const expectedProperties = Array.from(generator.expectedProperties); + const globals = expectedProperties.filter(prop => globalWhitelist.has(prop)); + const storeProps = options.store ? expectedProperties.filter(prop => prop[0] === '$') : []; + const initialState = []; if (globals.length > 0) { initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`); } + if (storeProps.length > 0) { + initialState.push(`options.store._init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`); + } + if (templateProperties.data) { initialState.push(`%data()`); - } else if (globals.length === 0) { + } else if (globals.length === 0 && storeProps.length === 0) { initialState.push('{}'); } @@ -99,7 +105,7 @@ export default function ssr( return ${templateProperties.data ? `%data()` : `{}`}; }; - ${name}.render = function(state, options) { + ${name}.render = function(state, options = {}) { state = Object.assign(${initialState.join(', ')}); ${computations.map( diff --git a/src/generators/server-side-rendering/visitors/Component.ts b/src/generators/server-side-rendering/visitors/Component.ts index b582dc13d949..74c7e239e458 100644 --- a/src/generators/server-side-rendering/visitors/Component.ts +++ b/src/generators/server-side-rendering/visitors/Component.ts @@ -79,6 +79,11 @@ export default function visitComponent( let open = `\${${expression}.render({${props}}`; + const options = []; + if (generator.options.store) { + options.push(`store: options.store`); + } + if (node.children.length) { const appendTarget: AppendTarget = { slots: { default: '' }, @@ -95,11 +100,15 @@ export default function visitComponent( .map(name => `${name}: () => \`${appendTarget.slots[name]}\``) .join(', '); - open += `, { slotted: { ${slotted} } }`; + options.push(`slotted: { ${slotted} }`); generator.appendTargets.pop(); } + if (options.length) { + open += `, { ${options.join(', ')} }`; + } + generator.append(open); generator.append(')}'); } diff --git a/src/server-side-rendering/register.js b/src/server-side-rendering/register.js index 254c1e441950..bb4ea61e7bb0 100644 --- a/src/server-side-rendering/register.js +++ b/src/server-side-rendering/register.js @@ -2,16 +2,22 @@ import * as fs from 'fs'; import * as path from 'path'; import { compile } from '../index.ts'; +const compileOptions = {}; + function capitalise(name) { return name[0].toUpperCase() + name.slice(1); } export default function register(options) { const { extensions } = options; + if (extensions) { _deregister('.html'); extensions.forEach(_register); } + + // TODO make this the default and remove in v2 + if ('store' in options) compileOptions.store = options.store; } function _deregister(extension) { @@ -20,13 +26,15 @@ function _deregister(extension) { function _register(extension) { require.extensions[extension] = function(module, filename) { - const {code} = compile(fs.readFileSync(filename, 'utf-8'), { + const options = Object.assign({}, compileOptions, { filename, name: capitalise(path.basename(filename) .replace(new RegExp(`${extension.replace('.', '\\.')}$`), '')), - generate: 'ssr', + generate: 'ssr' }); + const {code} = compile(fs.readFileSync(filename, 'utf-8'), options); + return module._compile(code, filename); }; } diff --git a/test/js/samples/ssr-no-oncreate-etc/expected-bundle.js b/test/js/samples/ssr-no-oncreate-etc/expected-bundle.js index 9a466e06ae3e..1dfa59b423a4 100644 --- a/test/js/samples/ssr-no-oncreate-etc/expected-bundle.js +++ b/test/js/samples/ssr-no-oncreate-etc/expected-bundle.js @@ -4,7 +4,7 @@ SvelteComponent.data = function() { return {}; }; -SvelteComponent.render = function(state, options) { +SvelteComponent.render = function(state, options = {}) { state = Object.assign({}, state); return ``.trim(); diff --git a/test/js/samples/ssr-no-oncreate-etc/expected.js b/test/js/samples/ssr-no-oncreate-etc/expected.js index 51c10c765602..c64b3877ab1d 100644 --- a/test/js/samples/ssr-no-oncreate-etc/expected.js +++ b/test/js/samples/ssr-no-oncreate-etc/expected.js @@ -6,7 +6,7 @@ SvelteComponent.data = function() { return {}; }; -SvelteComponent.render = function(state, options) { +SvelteComponent.render = function(state, options = {}) { state = Object.assign({}, state); return ``.trim(); diff --git a/test/server-side-rendering/index.js b/test/server-side-rendering/index.js index a9bf847b2997..b6bd109c0e01 100644 --- a/test/server-side-rendering/index.js +++ b/test/server-side-rendering/index.js @@ -22,7 +22,8 @@ function tryToReadFile(file) { describe("ssr", () => { before(() => { require("../../ssr/register")({ - extensions: ['.svelte', '.html'] + extensions: ['.svelte', '.html'], + store: true }); return setupHtmlEqual(); @@ -98,9 +99,15 @@ describe("ssr", () => { delete require.cache[resolved]; }); + require("../../ssr/register")({ + store: !!config.store + }); + try { const component = require(`../runtime/samples/${dir}/main.html`); - const html = component.render(config.data); + const html = component.render(config.data, { + store: config.store + }); if (config.html) { assert.htmlEqual(html, config.html); From 945d8ce526d50a093df73453a6762c1ed46e55dc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Nov 2017 16:07:38 -0500 Subject: [PATCH 07/19] store bindings --- src/generators/dom/index.ts | 2 +- .../dom/visitors/Element/addBindings.ts | 42 ++++++++++++++++--- .../onrender-onteardown-rewritten/expected.js | 2 +- .../samples/store-binding/NameInput.html | 1 + test/runtime/samples/store-binding/_config.js | 28 +++++++++++++ test/runtime/samples/store-binding/main.html | 10 +++++ 6 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 test/runtime/samples/store-binding/NameInput.html create mode 100644 test/runtime/samples/store-binding/_config.js create mode 100644 test/runtime/samples/store-binding/main.html diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index 2bf0c21c4be5..c6d9e94df211 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -227,7 +227,7 @@ export default function dom( ${(templateProperties.ondestroy || storeProps.length) && ( `this._handlers.destroy = [${ [templateProperties.ondestroy && `%ondestroy`, storeProps.length && `@removeFromStore`].filter(Boolean).join(', ') - }]` + }];` )} ${generator.slots.size && `this._slotted = options.slots || {};`} diff --git a/src/generators/dom/visitors/Element/addBindings.ts b/src/generators/dom/visitors/Element/addBindings.ts index a2433abf93d0..2e41cd1b190b 100644 --- a/src/generators/dom/visitors/Element/addBindings.ts +++ b/src/generators/dom/visitors/Element/addBindings.ts @@ -195,13 +195,19 @@ export default function addBindings( const usesContext = group.bindings.some(binding => binding.handler.usesContext); const usesState = group.bindings.some(binding => binding.handler.usesState); + const usesStore = group.bindings.some(binding => binding.handler.usesStore); const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n'); const props = new Set(); + const storeProps = new Set(); group.bindings.forEach(binding => { binding.handler.props.forEach(prop => { props.add(prop); }); + + binding.handler.storeProps.forEach(prop => { + storeProps.add(prop); + }); }); // TODO use stringifyProps here, once indenting is fixed // media bindings — awkward special case. The native timeupdate events @@ -222,9 +228,11 @@ export default function addBindings( } ${usesContext && `var context = ${node.var}._svelte;`} ${usesState && `var state = #component.get();`} + ${usesStore && `var $ = #component.store.get();`} ${needsLock && `${lock} = true;`} ${mutations.length > 0 && mutations} - #component.set({ ${Array.from(props).join(', ')} }); + ${props.size > 0 && `#component.set({ ${Array.from(props).join(', ')} });`} + ${storeProps.size > 0 && `#component.store.set({ ${Array.from(storeProps).join(', ')} });`} ${needsLock && `${lock} = false;`} } `); @@ -307,6 +315,13 @@ function getEventHandler( dependencies: string[], value: string, ) { + let storeDependencies = []; + + if (generator.options.store) { + storeDependencies = dependencies.filter(prop => prop[0] === '$').map(prop => prop.slice(1)); + dependencies = dependencies.filter(prop => prop[0] !== '$'); + } + if (block.contexts.has(name)) { const tail = attribute.value.type === 'MemberExpression' ? getTailSnippet(attribute.value) @@ -318,8 +333,10 @@ function getEventHandler( return { usesContext: true, usesState: true, + usesStore: storeDependencies.length > 0, mutation: `${list}[${index}]${tail} = ${value};`, - props: dependencies.map(prop => `${prop}: state.${prop}`) + props: dependencies.map(prop => `${prop}: state.${prop}`), + storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`) }; } @@ -336,16 +353,31 @@ function getEventHandler( return { usesContext: false, usesState: true, + usesStore: storeDependencies.length > 0, mutation: `${snippet} = ${value}`, - props: dependencies.map((prop: string) => `${prop}: state.${prop}`) + props: dependencies.map((prop: string) => `${prop}: state.${prop}`), + storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`) }; } + let props; + let storeProps; + + if (generator.options.store && name[0] === '$') { + props = []; + storeProps = [`${name.slice(1)}: ${value}`]; + } else { + props = [`${name}: ${value}`]; + storeProps = []; + } + return { usesContext: false, usesState: false, + usesStore: false, mutation: null, - props: [`${name}: ${value}`] + props, + storeProps }; } @@ -393,4 +425,4 @@ function isComputed(node: Node) { } return false; -} +} \ No newline at end of file diff --git a/test/js/samples/onrender-onteardown-rewritten/expected.js b/test/js/samples/onrender-onteardown-rewritten/expected.js index 19b85a62e01d..ae6046dddb36 100644 --- a/test/js/samples/onrender-onteardown-rewritten/expected.js +++ b/test/js/samples/onrender-onteardown-rewritten/expected.js @@ -24,7 +24,7 @@ function SvelteComponent(options) { init(this, options); this._state = assign({}, options.data); - this._handlers.destroy = [ondestroy] + this._handlers.destroy = [ondestroy]; var _oncreate = oncreate.bind(this); diff --git a/test/runtime/samples/store-binding/NameInput.html b/test/runtime/samples/store-binding/NameInput.html new file mode 100644 index 000000000000..a5e4f5e48cee --- /dev/null +++ b/test/runtime/samples/store-binding/NameInput.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/runtime/samples/store-binding/_config.js b/test/runtime/samples/store-binding/_config.js new file mode 100644 index 000000000000..c9ae020777e3 --- /dev/null +++ b/test/runtime/samples/store-binding/_config.js @@ -0,0 +1,28 @@ +import Store from '../../../../store.js'; + +const store = new Store({ + name: 'world' +}); + +export default { + store, + + html: ` +

Hello world!

+ + `, + + test(assert, component, target, window) { + const input = target.querySelector('input'); + const event = new window.Event('input'); + + input.value = 'everybody'; + input.dispatchEvent(event); + + assert.equal(store.get('name'), 'everybody'); + assert.htmlEqual(target.innerHTML, ` +

Hello everybody!

+ + `); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/store-binding/main.html b/test/runtime/samples/store-binding/main.html new file mode 100644 index 000000000000..06410ea770f7 --- /dev/null +++ b/test/runtime/samples/store-binding/main.html @@ -0,0 +1,10 @@ +

Hello {{$name}}!

+ + + \ No newline at end of file From a87d30e0e6ad8ee00d1010967f3d9f7807268c5d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Nov 2017 16:23:32 -0500 Subject: [PATCH 08/19] allow event handlers to call store methods --- src/validate/html/validateEventHandler.ts | 10 +++++- src/validate/index.ts | 6 ++-- .../samples/store-event/NameInput.html | 1 + test/runtime/samples/store-event/_config.js | 34 +++++++++++++++++++ test/runtime/samples/store-event/main.html | 10 ++++++ .../samples/store-unexpected/input.html | 1 + .../samples/store-unexpected/warnings.json | 8 +++++ 7 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 test/runtime/samples/store-event/NameInput.html create mode 100644 test/runtime/samples/store-event/_config.js create mode 100644 test/runtime/samples/store-event/main.html create mode 100644 test/validator/samples/store-unexpected/input.html create mode 100644 test/validator/samples/store-unexpected/warnings.json diff --git a/src/validate/html/validateEventHandler.ts b/src/validate/html/validateEventHandler.ts index 2339f801d91f..8e8a6122b384 100644 --- a/src/validate/html/validateEventHandler.ts +++ b/src/validate/html/validateEventHandler.ts @@ -1,6 +1,6 @@ import flattenReference from '../../utils/flattenReference'; import list from '../../utils/list'; -import { Validator } from '../index'; +import validate, { Validator } from '../index'; import validCalleeObjects from '../../utils/validCalleeObjects'; import { Node } from '../../interfaces'; @@ -28,6 +28,13 @@ export default function validateEventHandlerCallee( return; } + if (name === 'store' && attribute.expression.callee.type === 'MemberExpression') { + if (!validator.options.store) { + validator.warn('compile with `store: true` in order to call store methods', attribute.expression.start); + } + return; + } + if ( (callee.type === 'Identifier' && validBuiltins.has(callee.name)) || validator.methods.has(callee.name) @@ -35,6 +42,7 @@ export default function validateEventHandlerCallee( return; const validCallees = ['this.*', 'event.*', 'options.*', 'console.*'].concat( + validator.options.store ? 'store.*' : [], Array.from(validBuiltins), Array.from(validator.methods.keys()) ); diff --git a/src/validate/index.ts b/src/validate/index.ts index 17c3e83be964..5ed4d58bd9f6 100644 --- a/src/validate/index.ts +++ b/src/validate/index.ts @@ -22,6 +22,7 @@ export class Validator { readonly source: string; readonly filename: string; + options: CompileOptions; onwarn: ({}) => void; locator?: (pos: number) => Location; @@ -37,8 +38,8 @@ export class Validator { constructor(parsed: Parsed, source: string, options: CompileOptions) { this.source = source; this.filename = options.filename; - this.onwarn = options.onwarn; + this.options = options; this.namespace = null; this.defaultExport = null; @@ -78,7 +79,7 @@ export default function validate( stylesheet: Stylesheet, options: CompileOptions ) { - const { onwarn, onerror, name, filename } = options; + const { onwarn, onerror, name, filename, store } = options; try { if (name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(name)) { @@ -99,6 +100,7 @@ export default function validate( onwarn, name, filename, + store }); if (parsed.js) { diff --git a/test/runtime/samples/store-event/NameInput.html b/test/runtime/samples/store-event/NameInput.html new file mode 100644 index 000000000000..ecd95d0364dc --- /dev/null +++ b/test/runtime/samples/store-event/NameInput.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/runtime/samples/store-event/_config.js b/test/runtime/samples/store-event/_config.js new file mode 100644 index 000000000000..6f192ff76ff6 --- /dev/null +++ b/test/runtime/samples/store-event/_config.js @@ -0,0 +1,34 @@ +import Store from '../../../../store.js'; + +class MyStore extends Store { + setName(name) { + this.set({ name }); + } +} + +const store = new MyStore({ + name: 'world' +}); + +export default { + store, + + html: ` +

Hello world!

+ + `, + + test(assert, component, target, window) { + const input = target.querySelector('input'); + const event = new window.Event('input'); + + input.value = 'everybody'; + input.dispatchEvent(event); + + assert.equal(store.get('name'), 'everybody'); + assert.htmlEqual(target.innerHTML, ` +

Hello everybody!

+ + `); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/store-event/main.html b/test/runtime/samples/store-event/main.html new file mode 100644 index 000000000000..06410ea770f7 --- /dev/null +++ b/test/runtime/samples/store-event/main.html @@ -0,0 +1,10 @@ +

Hello {{$name}}!

+ + + \ No newline at end of file diff --git a/test/validator/samples/store-unexpected/input.html b/test/validator/samples/store-unexpected/input.html new file mode 100644 index 000000000000..589f28e647e0 --- /dev/null +++ b/test/validator/samples/store-unexpected/input.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/validator/samples/store-unexpected/warnings.json b/test/validator/samples/store-unexpected/warnings.json new file mode 100644 index 000000000000..bac2841dc966 --- /dev/null +++ b/test/validator/samples/store-unexpected/warnings.json @@ -0,0 +1,8 @@ +[{ + "message": "compile with `store: true` in order to call store methods", + "loc": { + "line": 1, + "column": 18 + }, + "pos": 18 +}] \ No newline at end of file From 3206e08286bd7de8912903aa081b309c0c03d8b8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Nov 2017 16:49:37 -0500 Subject: [PATCH 09/19] allow computed properties to depend on store props --- src/generators/Generator.ts | 3 + test/runtime/samples/store-computed/Todo.html | 15 +++++ .../runtime/samples/store-computed/_config.js | 65 +++++++++++++++++++ test/runtime/samples/store-computed/main.html | 11 ++++ 4 files changed, 94 insertions(+) create mode 100644 test/runtime/samples/store-computed/Todo.html create mode 100644 test/runtime/samples/store-computed/_config.js create mode 100644 test/runtime/samples/store-computed/main.html diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index a8775d41b266..99f7597276c1 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -536,6 +536,9 @@ export default class Generator { (param: Node) => param.type === 'AssignmentPattern' ? param.left.name : param.name ); + deps.forEach(dep => { + this.expectedProperties.add(dep); + }); dependencies.set(key, deps); }); diff --git a/test/runtime/samples/store-computed/Todo.html b/test/runtime/samples/store-computed/Todo.html new file mode 100644 index 000000000000..2ad67420cad4 --- /dev/null +++ b/test/runtime/samples/store-computed/Todo.html @@ -0,0 +1,15 @@ +{{#if isVisible}} +
{{todo.description}}
+{{/if}} + + \ No newline at end of file diff --git a/test/runtime/samples/store-computed/_config.js b/test/runtime/samples/store-computed/_config.js new file mode 100644 index 000000000000..2906ff4bf8e7 --- /dev/null +++ b/test/runtime/samples/store-computed/_config.js @@ -0,0 +1,65 @@ +import Store from '../../../../store.js'; + +class MyStore extends Store { + setFilter(filter) { + this.set({ filter }); + } + + toggleTodo(todo) { + todo.done = !todo.done; + this.set({ todos: this.get('todos') }); + } +} + +const todos = [ + { + description: 'Buy some milk', + done: true, + }, + { + description: 'Do the laundry', + done: true, + }, + { + description: "Find life's true purpose", + done: false, + } +]; + +const store = new MyStore({ + filter: 'all', + todos +}); + +export default { + solo: true, + + store, + + html: ` +
Buy some milk
+
Do the laundry
+
Find life's true purpose
+ `, + + test(assert, component, target) { + store.setFilter('pending'); + + assert.htmlEqual(target.innerHTML, ` +
Find life's true purpose
+ `); + + store.toggleTodo(todos[1]); + + assert.htmlEqual(target.innerHTML, ` +
Do the laundry
+
Find life's true purpose
+ `); + + store.setFilter('done'); + + assert.htmlEqual(target.innerHTML, ` +
Buy some milk
+ `); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/store-computed/main.html b/test/runtime/samples/store-computed/main.html new file mode 100644 index 000000000000..5c50839ba38e --- /dev/null +++ b/test/runtime/samples/store-computed/main.html @@ -0,0 +1,11 @@ +{{#each $todos as todo}} + +{{/each}} + + \ No newline at end of file From 8ad22bcf4ac1240e33ff7fa4f31b98035debaf2e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Nov 2017 16:49:57 -0500 Subject: [PATCH 10/19] add store.js to package --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 6bf9fae197e4..7b1937020d1c 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "compiler", "ssr", "shared.js", + "store.js", "README.md" ], "scripts": { From edc61b7bd81432b232f6b50f538affdc56ed1b3b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Nov 2017 17:03:22 -0500 Subject: [PATCH 11/19] fix tests, now that computed prop dependencies are expected --- .../samples/dev-warning-readonly-computed/_config.js | 6 ++++++ test/runtime/samples/set-clones-input/_config.js | 6 +++++- test/runtime/samples/store-computed/_config.js | 2 -- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/test/runtime/samples/dev-warning-readonly-computed/_config.js b/test/runtime/samples/dev-warning-readonly-computed/_config.js index 2706b9610a04..5cf4899db17c 100644 --- a/test/runtime/samples/dev-warning-readonly-computed/_config.js +++ b/test/runtime/samples/dev-warning-readonly-computed/_config.js @@ -1,6 +1,12 @@ export default { + solo: true, + dev: true, + data: { + a: 42 + }, + test ( assert, component ) { try { component.set({ foo: 1 }); diff --git a/test/runtime/samples/set-clones-input/_config.js b/test/runtime/samples/set-clones-input/_config.js index af04f4b73e98..8f365f633078 100644 --- a/test/runtime/samples/set-clones-input/_config.js +++ b/test/runtime/samples/set-clones-input/_config.js @@ -1,10 +1,14 @@ export default { dev: true, + data: { + a: 42 + }, + test ( assert, component ) { const obj = { a: 1 }; component.set( obj ); component.set( obj ); // will fail if the object is not cloned component.destroy(); } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/test/runtime/samples/store-computed/_config.js b/test/runtime/samples/store-computed/_config.js index 2906ff4bf8e7..13fd745f53f8 100644 --- a/test/runtime/samples/store-computed/_config.js +++ b/test/runtime/samples/store-computed/_config.js @@ -32,8 +32,6 @@ const store = new MyStore({ }); export default { - solo: true, - store, html: ` From 1cdfb84fec0438d4eba9c5bbc0ad8aafc0de3794 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Nov 2017 17:05:52 -0500 Subject: [PATCH 12/19] remove solo: true --- test/runtime/samples/dev-warning-readonly-computed/_config.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/runtime/samples/dev-warning-readonly-computed/_config.js b/test/runtime/samples/dev-warning-readonly-computed/_config.js index 5cf4899db17c..95d4ea15c450 100644 --- a/test/runtime/samples/dev-warning-readonly-computed/_config.js +++ b/test/runtime/samples/dev-warning-readonly-computed/_config.js @@ -1,6 +1,4 @@ export default { - solo: true, - dev: true, data: { From f23c886b6a491a777fd73731a75c2f95c9ca99de Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Nov 2017 12:07:28 -0500 Subject: [PATCH 13/19] computed properties --- store.js | 75 +++++++++++++++++++++++++++++++++++++++++++-- test/store/index.js | 51 ++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 3 deletions(-) diff --git a/store.js b/store.js index 3a4a516e466d..91603bb15b77 100644 --- a/store.js +++ b/store.js @@ -8,10 +8,20 @@ import { } from './shared.js'; function Store(state) { - this._state = state ? assign({}, state) : {}; this._observers = { pre: blankObject(), post: blankObject() }; this._changeHandlers = []; this._dependents = []; + + this._proto = blankObject(); + this._changed = blankObject(); + this._dependentProps = blankObject(); + this._dirty = blankObject(); + this._state = Object.create(this._proto); + + for (var key in state) { + this._changed[key] = true; + this._state[key] = state[key]; + } } assign(Store.prototype, { @@ -25,6 +35,17 @@ assign(Store.prototype, { }); }, + _makeDirty: function(prop) { + var dependentProps = this._dependentProps[prop]; + if (dependentProps) { + for (var i = 0; i < dependentProps.length; i += 1) { + var dependentProp = dependentProps[i]; + this._dirty[dependentProp] = this._changed[dependentProp] = true; + this._makeDirty(dependentProp); + } + } + }, + _init: function(props) { var state = {}; for (let i = 0; i < props.length; i += 1) { @@ -44,6 +65,52 @@ assign(Store.prototype, { } }, + compute: function(key, deps, fn) { + var store = this; + var value; + + store._dirty[key] = true; + + for (var i = 0; i < deps.length; i += 1) { + var dep = deps[i]; + if (!this._dependentProps[dep]) this._dependentProps[dep] = []; + this._dependentProps[dep].push(key); + } + + Object.defineProperty(this._proto, key, { + get: function() { + if (store._dirty[key]) { + var values = deps.map(function(dep) { + if (dep in store._changed) changed = true; + return store._state[dep]; + }); + + var newValue = fn.apply(null, values); + + if (differs(newValue, value)) { + value = newValue; + store._changed[key] = true; + + var dependentProps = store._dependentProps[key]; + if (dependentProps) { + for (var i = 0; i < dependentProps.length; i += 1) { + var prop = dependentProps[i]; + store._dirty[prop] = store._changed[prop] = true; + } + } + } + + store._dirty[key] = false; + } + + return value; + }, + set: function() { + throw new Error(`'${key}' is a read-only property`); + } + }); + }, + onchange: function(callback) { this._changeHandlers.push(callback); return { @@ -56,7 +123,7 @@ assign(Store.prototype, { set: function(newState) { var oldState = this._state, - changed = {}, + changed = this._changed = {}, dirty = false; for (var key in newState) { @@ -64,7 +131,9 @@ assign(Store.prototype, { } if (!dirty) return; - this._state = assign({}, oldState, newState); + this._state = assign(Object.create(this._proto), oldState, newState); + + for (var key in changed) this._makeDirty(key); for (var i = 0; i < this._changeHandlers.length; i += 1) { this._changeHandlers[i](this._state, changed); diff --git a/test/store/index.js b/test/store/index.js index 414e49bae4fd..50b6572774b4 100644 --- a/test/store/index.js +++ b/test/store/index.js @@ -92,4 +92,55 @@ describe('store', () => { }); }); }); + + describe('computed', () => { + it('computes a property based on data', () => { + const store = new Store({ + foo: 1 + }); + + store.compute('bar', ['foo'], foo => foo * 2); + assert.equal(store.get('bar'), 2); + + const values = []; + + store.observe('bar', bar => { + values.push(bar); + }); + + store.set({ foo: 2 }); + assert.deepEqual(values, [2, 4]); + }); + + it('computes a property based on another computed property', () => { + const store = new Store({ + foo: 1 + }); + + store.compute('bar', ['foo'], foo => foo * 2); + store.compute('baz', ['bar'], bar => bar * 2); + assert.equal(store.get('baz'), 4); + + const values = []; + + store.observe('baz', baz => { + values.push(baz); + }); + + store.set({ foo: 2 }); + assert.deepEqual(values, [4, 8]); + }); + + it('prevents computed properties from being set', () => { + const store = new Store({ + foo: 1 + }); + + store.compute('bar', ['foo'], foo => foo * 2); + + assert.throws(() => { + store.set({ bar: 'whatever' }); + }, /'bar' is a read-only property/); + }); + }); }); From a669dbfcd4f294d59b9631cf47cfa0e047a74510 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Nov 2017 13:21:55 -0500 Subject: [PATCH 14/19] add combineStores function --- store.js | 23 ++++++++++- test/runtime/samples/store-binding/_config.js | 2 +- .../runtime/samples/store-computed/_config.js | 2 +- test/runtime/samples/store-event/_config.js | 2 +- test/runtime/samples/store/_config.js | 2 +- test/store/index.js | 38 ++++++++++++++++++- 6 files changed, 63 insertions(+), 6 deletions(-) diff --git a/store.js b/store.js index 91603bb15b77..e285439ddfee 100644 --- a/store.js +++ b/store.js @@ -78,6 +78,7 @@ assign(Store.prototype, { } Object.defineProperty(this._proto, key, { + enumerable: true, get: function() { if (store._dirty[key]) { var values = deps.map(function(dep) { @@ -162,4 +163,24 @@ assign(Store.prototype, { } }); -export default Store; \ No newline at end of file +function combineStores(store, children) { + var updates = {}; + + for (const key in children) { + const child = children[key]; + updates[key] = child.get(); + + child.onchange(state => { + var update = {}; + update[key] = state; + store.set(update); + }); + } + + console.log('updates', updates); + + store.set(updates); + return store; +} + +export { Store, combineStores }; \ No newline at end of file diff --git a/test/runtime/samples/store-binding/_config.js b/test/runtime/samples/store-binding/_config.js index c9ae020777e3..aefc4ec652ac 100644 --- a/test/runtime/samples/store-binding/_config.js +++ b/test/runtime/samples/store-binding/_config.js @@ -1,4 +1,4 @@ -import Store from '../../../../store.js'; +import { Store } from '../../../../store.js'; const store = new Store({ name: 'world' diff --git a/test/runtime/samples/store-computed/_config.js b/test/runtime/samples/store-computed/_config.js index 13fd745f53f8..f4e6f49e5454 100644 --- a/test/runtime/samples/store-computed/_config.js +++ b/test/runtime/samples/store-computed/_config.js @@ -1,4 +1,4 @@ -import Store from '../../../../store.js'; +import { Store } from '../../../../store.js'; class MyStore extends Store { setFilter(filter) { diff --git a/test/runtime/samples/store-event/_config.js b/test/runtime/samples/store-event/_config.js index 6f192ff76ff6..2779db5fc270 100644 --- a/test/runtime/samples/store-event/_config.js +++ b/test/runtime/samples/store-event/_config.js @@ -1,4 +1,4 @@ -import Store from '../../../../store.js'; +import { Store } from '../../../../store.js'; class MyStore extends Store { setName(name) { diff --git a/test/runtime/samples/store/_config.js b/test/runtime/samples/store/_config.js index 0e1663315b70..4e266ff0957a 100644 --- a/test/runtime/samples/store/_config.js +++ b/test/runtime/samples/store/_config.js @@ -1,4 +1,4 @@ -import Store from '../../../../store.js'; +import { Store } from '../../../../store.js'; const store = new Store({ name: 'world' diff --git a/test/store/index.js b/test/store/index.js index 50b6572774b4..258bc01c7c16 100644 --- a/test/store/index.js +++ b/test/store/index.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import Store from '../../store.js'; +import { Store, combineStores } from '../../store.js'; describe('store', () => { describe('get', () => { @@ -143,4 +143,40 @@ describe('store', () => { }, /'bar' is a read-only property/); }); }); + + describe('combineStores', () => { + it('merges stores', () => { + const a = new Store({ + x: 1, + y: 2 + }); + + a.compute('z', ['x', 'y'], (x, y) => x + y); + + const b = new Store({ + x: 3, + y: 4 + }); + + b.compute('z', ['x', 'y'], (x, y) => x + y); + + const c = combineStores(new Store(), { a, b }); + + c.compute('total', ['a', 'b'], (a, b) => a.z + b.z); + + assert.deepEqual(c.get(), { + a: { + x: 1, + y: 2, + z: 3 + }, + b: { + x: 3, + y: 4, + z: 7 + }, + total: 10 + }); + }); + }); }); From 47547ed0abfd9c95c7a56c53ca207d22e5f966f0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Nov 2017 13:55:20 -0500 Subject: [PATCH 15/19] better implementation of computed properties --- store.js | 102 +++++++++++++++++++------------------------- test/store/index.js | 13 +++++- 2 files changed, 55 insertions(+), 60 deletions(-) diff --git a/store.js b/store.js index e285439ddfee..2c573b247fd1 100644 --- a/store.js +++ b/store.js @@ -12,16 +12,10 @@ function Store(state) { this._changeHandlers = []; this._dependents = []; - this._proto = blankObject(); - this._changed = blankObject(); - this._dependentProps = blankObject(); - this._dirty = blankObject(); - this._state = Object.create(this._proto); - - for (var key in state) { - this._changed[key] = true; - this._state[key] = state[key]; - } + this._computed = blankObject(); + this._sortedComputedProperties = []; + + this._state = assign({}, state); } assign(Store.prototype, { @@ -35,17 +29,6 @@ assign(Store.prototype, { }); }, - _makeDirty: function(prop) { - var dependentProps = this._dependentProps[prop]; - if (dependentProps) { - for (var i = 0; i < dependentProps.length; i += 1) { - var dependentProp = dependentProps[i]; - this._dirty[dependentProp] = this._changed[dependentProp] = true; - this._makeDirty(dependentProp); - } - } - }, - _init: function(props) { var state = {}; for (let i = 0; i < props.length; i += 1) { @@ -65,51 +48,51 @@ assign(Store.prototype, { } }, - compute: function(key, deps, fn) { - var store = this; - var value; + _sortComputedProperties() { + var computed = this._computed; + var sorted = this._sortedComputedProperties = []; + var visited = blankObject(); - store._dirty[key] = true; + function visit(key) { + if (visited[key]) return; + var c = computed[key]; - for (var i = 0; i < deps.length; i += 1) { - var dep = deps[i]; - if (!this._dependentProps[dep]) this._dependentProps[dep] = []; - this._dependentProps[dep].push(key); + if (c) { + c.deps.forEach(visit); + sorted.push(c); + } } - Object.defineProperty(this._proto, key, { - enumerable: true, - get: function() { - if (store._dirty[key]) { - var values = deps.map(function(dep) { - if (dep in store._changed) changed = true; - return store._state[dep]; - }); + for (var key in this._computed) visit(key); + }, - var newValue = fn.apply(null, values); + compute: function(key, deps, fn) { + var store = this; + var value; + + var c = { + deps: deps, + update: function(state, changed, dirty) { + var values = deps.map(function(dep) { + if (dep in changed) dirty = true; + return state[dep]; + }); + if (dirty) { + var newValue = fn.apply(null, values); if (differs(newValue, value)) { value = newValue; - store._changed[key] = true; - - var dependentProps = store._dependentProps[key]; - if (dependentProps) { - for (var i = 0; i < dependentProps.length; i += 1) { - var prop = dependentProps[i]; - store._dirty[prop] = store._changed[prop] = true; - } - } + changed[key] = true; + state[key] = value; } - - store._dirty[key] = false; } - - return value; - }, - set: function() { - throw new Error(`'${key}' is a read-only property`); } - }); + }; + + c.update(this._state, {}, true); + + this._computed[key] = c; + this._sortComputedProperties(); }, onchange: function(callback) { @@ -128,13 +111,16 @@ assign(Store.prototype, { dirty = false; for (var key in newState) { + if (this._computed[key]) throw new Error("'" + key + "' is a read-only property"); if (differs(newState[key], oldState[key])) changed[key] = dirty = true; } if (!dirty) return; - this._state = assign(Object.create(this._proto), oldState, newState); + this._state = assign({}, oldState, newState); - for (var key in changed) this._makeDirty(key); + for (var i = 0; i < this._sortedComputedProperties.length; i += 1) { + this._sortedComputedProperties[i].update(this._state, changed); + } for (var i = 0; i < this._changeHandlers.length; i += 1) { this._changeHandlers[i](this._state, changed); @@ -177,8 +163,6 @@ function combineStores(store, children) { }); } - console.log('updates', updates); - store.set(updates); return store; } diff --git a/test/store/index.js b/test/store/index.js index 258bc01c7c16..2f5666152cee 100644 --- a/test/store/index.js +++ b/test/store/index.js @@ -1,7 +1,7 @@ import assert from 'assert'; import { Store, combineStores } from '../../store.js'; -describe('store', () => { +describe.only('store', () => { describe('get', () => { it('gets a specific key', () => { const store = new Store({ @@ -177,6 +177,17 @@ describe('store', () => { }, total: 10 }); + + const values = []; + + c.observe('total', total => { + values.push(total); + }); + + a.set({ x: 2, y: 3 }); + b.set({ x: 5, y: 6 }); + + assert.deepEqual(values, [10, 12, 16]); }); }); }); From adc248f6395cb7319f5db97c2208bd8bceb089f9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Nov 2017 14:02:24 -0500 Subject: [PATCH 16/19] make target store optional --- store.js | 3 ++- test/store/index.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/store.js b/store.js index 2c573b247fd1..9c3962d63bbc 100644 --- a/store.js +++ b/store.js @@ -149,7 +149,8 @@ assign(Store.prototype, { } }); -function combineStores(store, children) { +function combineStores(children, store) { + if (!store) store = new Store(); var updates = {}; for (const key in children) { diff --git a/test/store/index.js b/test/store/index.js index 2f5666152cee..246e65522918 100644 --- a/test/store/index.js +++ b/test/store/index.js @@ -160,7 +160,7 @@ describe.only('store', () => { b.compute('z', ['x', 'y'], (x, y) => x + y); - const c = combineStores(new Store(), { a, b }); + const c = combineStores({ a, b }); c.compute('total', ['a', 'b'], (a, b) => a.z + b.z); From 7779f26f7f33766f81a5d7c3be9df0d68f3b5b5b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Nov 2017 14:09:41 -0500 Subject: [PATCH 17/19] remove ES2015+ from store.js --- store.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/store.js b/store.js index 9c3962d63bbc..025d96a5ce3a 100644 --- a/store.js +++ b/store.js @@ -19,9 +19,6 @@ function Store(state) { } assign(Store.prototype, { - get, - observe -}, { _add: function(component, props) { this._dependents.push({ component: component, @@ -31,7 +28,7 @@ assign(Store.prototype, { _init: function(props) { var state = {}; - for (let i = 0; i < props.length; i += 1) { + for (var i = 0; i < props.length; i += 1) { var prop = props[i]; state['$' + prop] = this._state[prop]; } @@ -39,7 +36,7 @@ assign(Store.prototype, { }, _remove: function(component) { - let i = this._dependents.length; + var i = this._dependents.length; while (i--) { if (this._dependents[i].component === component) { this._dependents.splice(i, 1); @@ -48,7 +45,7 @@ assign(Store.prototype, { } }, - _sortComputedProperties() { + _sortComputedProperties: function() { var computed = this._computed; var sorted = this._sortedComputedProperties = []; var visited = blankObject(); @@ -95,6 +92,10 @@ assign(Store.prototype, { this._sortComputedProperties(); }, + get: get, + + observe: observe, + onchange: function(callback) { this._changeHandlers.push(callback); return { @@ -153,11 +154,11 @@ function combineStores(children, store) { if (!store) store = new Store(); var updates = {}; - for (const key in children) { - const child = children[key]; + for (var key in children) { + var child = children[key]; updates[key] = child.get(); - child.onchange(state => { + child.onchange(function(state) { var update = {}; update[key] = state; store.set(update); From 96eac06e2d18f9d1b18fa892118cdf1008430e45 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Nov 2017 14:23:57 -0500 Subject: [PATCH 18/19] fix ES5 scoping bug --- store.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/store.js b/store.js index 025d96a5ce3a..d3f3a3a5d9f9 100644 --- a/store.js +++ b/store.js @@ -154,7 +154,7 @@ function combineStores(children, store) { if (!store) store = new Store(); var updates = {}; - for (var key in children) { + function addChild(key) { var child = children[key]; updates[key] = child.get(); @@ -165,6 +165,8 @@ function combineStores(children, store) { }); } + for (var key in children) addChild(key); + store.set(updates); return store; } From d5d1eccb288734d2db1f410cd2961cc5bbd0bce3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Nov 2017 17:12:10 -0500 Subject: [PATCH 19/19] remove combineStores --- store.js | 23 +-------------------- test/store/index.js | 49 +-------------------------------------------- 2 files changed, 2 insertions(+), 70 deletions(-) diff --git a/store.js b/store.js index d3f3a3a5d9f9..a1d6dc4d2e89 100644 --- a/store.js +++ b/store.js @@ -150,25 +150,4 @@ assign(Store.prototype, { } }); -function combineStores(children, store) { - if (!store) store = new Store(); - var updates = {}; - - function addChild(key) { - var child = children[key]; - updates[key] = child.get(); - - child.onchange(function(state) { - var update = {}; - update[key] = state; - store.set(update); - }); - } - - for (var key in children) addChild(key); - - store.set(updates); - return store; -} - -export { Store, combineStores }; \ No newline at end of file +export { Store }; \ No newline at end of file diff --git a/test/store/index.js b/test/store/index.js index 246e65522918..25099a9066da 100644 --- a/test/store/index.js +++ b/test/store/index.js @@ -1,7 +1,7 @@ import assert from 'assert'; import { Store, combineStores } from '../../store.js'; -describe.only('store', () => { +describe('store', () => { describe('get', () => { it('gets a specific key', () => { const store = new Store({ @@ -143,51 +143,4 @@ describe.only('store', () => { }, /'bar' is a read-only property/); }); }); - - describe('combineStores', () => { - it('merges stores', () => { - const a = new Store({ - x: 1, - y: 2 - }); - - a.compute('z', ['x', 'y'], (x, y) => x + y); - - const b = new Store({ - x: 3, - y: 4 - }); - - b.compute('z', ['x', 'y'], (x, y) => x + y); - - const c = combineStores({ a, b }); - - c.compute('total', ['a', 'b'], (a, b) => a.z + b.z); - - assert.deepEqual(c.get(), { - a: { - x: 1, - y: 2, - z: 3 - }, - b: { - x: 3, - y: 4, - z: 7 - }, - total: 10 - }); - - const values = []; - - c.observe('total', total => { - values.push(total); - }); - - a.set({ x: 2, y: 3 }); - b.set({ x: 5, y: 6 }); - - assert.deepEqual(values, [10, 12, 16]); - }); - }); });