diff --git a/.eslintignore b/.eslintignore index bc31435419..4a113378ce 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 0000000000..4fee967aaf --- /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 0000000000..414e49bae4 --- /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 } + }); + }); + }); +});