mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Merge pull request #4749 from w33ble/persisted-state
PersistedState Class
This commit is contained in:
commit
de1332dc3a
4 changed files with 741 additions and 2 deletions
|
@ -5,10 +5,10 @@ require('ui/promises');
|
|||
* replace the Promise service with Bluebird so that tests
|
||||
* can use promises without having to call $rootScope.apply()
|
||||
*
|
||||
* var nonDigestPromises = require('testUtils/non_digest_promises');
|
||||
* var noDigestPromises = require('testUtils/noDigestPromises');
|
||||
*
|
||||
* describe('some module that does complex shit with promises', function () {
|
||||
* beforeEach(nonDigestPromises.activate);
|
||||
* beforeEach(noDigestPromises.activate);
|
||||
*
|
||||
* });
|
||||
*/
|
||||
|
|
|
@ -264,5 +264,13 @@ define(function (require) {
|
|||
};
|
||||
_.class(errors.InvalidWiggleSelection).inherits(KbnError);
|
||||
|
||||
errors.PersistedStateError = function PersistedStateError(msg) {
|
||||
KbnError.call(this,
|
||||
msg || 'PersistedState Error',
|
||||
errors.PersistedStateError);
|
||||
};
|
||||
_.class(errors.PersistedStateError).inherits(KbnError);
|
||||
|
||||
|
||||
return errors;
|
||||
});
|
||||
|
|
535
src/ui/public/persisted_state/__tests__/persisted_state.js
Normal file
535
src/ui/public/persisted_state/__tests__/persisted_state.js
Normal file
|
@ -0,0 +1,535 @@
|
|||
var _ = require('lodash');
|
||||
var sinon = require('auto-release-sinon');
|
||||
var noDigestPromises = require('testUtils/noDigestPromises');
|
||||
var ngMock = require('ngMock');
|
||||
var expect = require('expect.js');
|
||||
var errors = require('ui/errors');
|
||||
|
||||
var PersistedState;
|
||||
var Events;
|
||||
|
||||
describe('Persisted State', function () {
|
||||
noDigestPromises.activateForSuite();
|
||||
|
||||
beforeEach(function () {
|
||||
ngMock.module('kibana');
|
||||
|
||||
ngMock.inject(function (Private) {
|
||||
PersistedState = Private(require('ui/persisted_state/persisted_state'));
|
||||
Events = Private(require('ui/events'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('state creation', function () {
|
||||
var persistedState;
|
||||
|
||||
it('should create an empty state instance', function () {
|
||||
persistedState = new PersistedState();
|
||||
expect(persistedState.get()).to.eql({});
|
||||
});
|
||||
|
||||
it('should be an event emitter', function () {
|
||||
persistedState = new PersistedState();
|
||||
expect(persistedState).to.be.an(Events);
|
||||
});
|
||||
|
||||
it('should create a state instance with data', function () {
|
||||
var val = { red: 'blue' };
|
||||
persistedState = new PersistedState(val);
|
||||
|
||||
expect(persistedState.get()).to.eql(val);
|
||||
// ensure we get a copy of the state, not the actual state object
|
||||
expect(persistedState.get()).to.not.equal(val);
|
||||
});
|
||||
|
||||
it('should create a copy of the state passed in', function () {
|
||||
var val = { red: 'blue' };
|
||||
persistedState = new PersistedState(val);
|
||||
|
||||
expect(persistedState.get()).to.eql(val);
|
||||
expect(persistedState.get()).to.not.equal(val);
|
||||
});
|
||||
|
||||
it('should not throw if creating valid child object', function () {
|
||||
var run = function () {
|
||||
var val = { red: 'blue' };
|
||||
var path = ['test.path'];
|
||||
var parent = new PersistedState();
|
||||
new PersistedState(val, path, parent);
|
||||
};
|
||||
|
||||
expect(run).not.to.throwException();
|
||||
});
|
||||
|
||||
it('should throw if given an invalid value', function () {
|
||||
var run = function () {
|
||||
var val = 'bananas';
|
||||
new PersistedState(val);
|
||||
};
|
||||
|
||||
expect(run).to.throwException(function (err) {
|
||||
expect(err).to.be.a(errors.PersistedStateError);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not throw if given primitive to child', function () {
|
||||
var run = function () {
|
||||
var val = 'bananas';
|
||||
var path = ['test.path'];
|
||||
var parent = new PersistedState();
|
||||
new PersistedState(val, path, parent);
|
||||
};
|
||||
|
||||
expect(run).not.to.throwException();
|
||||
});
|
||||
|
||||
it('should throw if given an invalid parent object', function () {
|
||||
var run = function () {
|
||||
var val = { red: 'blue' };
|
||||
var path = ['test.path'];
|
||||
var parent = {};
|
||||
new PersistedState(val, path, parent);
|
||||
};
|
||||
|
||||
expect(run).to.throwException(function (err) {
|
||||
expect(err).to.be.a(errors.PersistedStateError);
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw if given a parent without a path', function () {
|
||||
var run = function () {
|
||||
var val = { red: 'blue' };
|
||||
var path;
|
||||
var parent = new PersistedState();
|
||||
|
||||
new PersistedState(val, path, parent);
|
||||
};
|
||||
|
||||
expect(run).to.throwException(function (err) {
|
||||
expect(err).to.be.a(errors.PersistedStateError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('child state creation', function () {
|
||||
var childState;
|
||||
|
||||
it('should not append the child state to the parent, without parent value', function () {
|
||||
var childIndex = 'i can haz child';
|
||||
var persistedState = new PersistedState();
|
||||
childState = persistedState.createChild(childIndex);
|
||||
|
||||
// parent state should not contain the child state
|
||||
expect(persistedState.get()).to.not.have.property(childIndex);
|
||||
expect(persistedState.get()).to.eql({});
|
||||
});
|
||||
|
||||
it('should not append the child state to the parent, with parent value', function () {
|
||||
var childIndex = 'i can haz child';
|
||||
var persistedStateValue = { original: true };
|
||||
var persistedState = new PersistedState(persistedStateValue);
|
||||
childState = persistedState.createChild(childIndex);
|
||||
|
||||
// child state should be empty, we didn't give it any default data
|
||||
expect(childState.get()).to.be(undefined);
|
||||
|
||||
// parent state should not contain the child state
|
||||
expect(persistedState.get()).to.not.have.property(childIndex);
|
||||
expect(persistedState.get()).to.eql(persistedStateValue);
|
||||
});
|
||||
|
||||
it('should append the child state to the parent, with parent and child values', function () {
|
||||
var childIndex = 'i can haz child';
|
||||
var childStateValue = { tacos: 'yes please' };
|
||||
var persistedStateValue = { original: true };
|
||||
var persistedState = new PersistedState(persistedStateValue);
|
||||
childState = persistedState.createChild(childIndex, childStateValue);
|
||||
|
||||
// parent state should contain the child and its original state value
|
||||
var parentState = persistedState.get();
|
||||
expect(parentState).to.have.property('original', true);
|
||||
expect(parentState).to.have.property(childIndex);
|
||||
expect(parentState[childIndex]).to.eql(childStateValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deep child state creation', function () {
|
||||
it('should delegate get/set calls to parent state', function () {
|
||||
var children = [{
|
||||
path: 'first*child',
|
||||
value: { first: true, second: false }
|
||||
}, {
|
||||
path: 'second child',
|
||||
value: { first: false, second: true }
|
||||
}];
|
||||
var persistedStateValue = { original: true };
|
||||
var persistedState = new PersistedState(persistedStateValue);
|
||||
|
||||
// first child is a child of the parent persistedState
|
||||
children[0].instance = persistedState.createChild(children[0].path, children[0].value);
|
||||
// second child is a child of the first child
|
||||
children[1].instance = children[0].instance.createChild(children[1].path, children[1].value);
|
||||
|
||||
// second child getter should only return second child value
|
||||
expect(children[1].instance.get()).to.eql(children[1].value);
|
||||
|
||||
// parent should contain original props and first child path, but not the second child path
|
||||
var parentState = persistedState.get();
|
||||
_.keys(persistedStateValue).forEach(function (key) {
|
||||
expect(parentState).to.have.property(key);
|
||||
});
|
||||
expect(parentState).to.have.property(children[0].path);
|
||||
expect(parentState).to.not.have.property(children[1].path);
|
||||
|
||||
// second child path should be inside the first child
|
||||
var firstChildState = children[0].instance.get();
|
||||
expect(firstChildState).to.have.property(children[1].path);
|
||||
expect(firstChildState[children[1].path]).to.eql(children[1].value);
|
||||
|
||||
// check that the second child is still accessible from the parent instance
|
||||
var firstChild = persistedState.get(children[0].path);
|
||||
expect(firstChild).to.have.property(children[1].path);
|
||||
});
|
||||
});
|
||||
|
||||
describe('colliding child paths and parent state values', function () {
|
||||
it('should not change the child path value by default', function () {
|
||||
var childIndex = 'childTest';
|
||||
var persistedStateValue = {};
|
||||
persistedStateValue[childIndex] = { overlapping_index: true };
|
||||
|
||||
var persistedState = new PersistedState(persistedStateValue);
|
||||
var state = persistedState.get();
|
||||
expect(state).to.have.property(childIndex);
|
||||
expect(state[childIndex]).to.eql(persistedStateValue[childIndex]);
|
||||
|
||||
var childState = persistedState.createChild(childIndex);
|
||||
expect(childState.get()).to.eql(persistedStateValue[childIndex]);
|
||||
|
||||
// make sure the parent state is still the same
|
||||
state = persistedState.get();
|
||||
expect(state).to.have.property(childIndex);
|
||||
expect(state[childIndex]).to.eql(persistedStateValue[childIndex]);
|
||||
});
|
||||
|
||||
it('should merge default child state', function () {
|
||||
var childIndex = 'childTest';
|
||||
var childStateValue = { child_index: false };
|
||||
var persistedStateValue = {};
|
||||
persistedStateValue[childIndex] = { parent_index: true };
|
||||
|
||||
var persistedState = new PersistedState(persistedStateValue);
|
||||
var state = persistedState.get();
|
||||
expect(state).to.have.property(childIndex);
|
||||
expect(state[childIndex]).to.eql(persistedStateValue[childIndex]);
|
||||
|
||||
// pass in child state value
|
||||
var childState = persistedState.createChild(childIndex, childStateValue);
|
||||
|
||||
// parent's default state overrides child state
|
||||
var compare = _.merge({}, childStateValue, persistedStateValue[childIndex]);
|
||||
expect(childState.get()).to.eql(compare);
|
||||
state = persistedState.get();
|
||||
expect(state).to.have.property(childIndex);
|
||||
expect(state[childIndex]).to.eql(compare);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mutation', function () {
|
||||
it('should not mutate the internal object', function () {
|
||||
var persistedStateValue = { hello: 'world' };
|
||||
var insertedObj = { farewell: 'cruel world' };
|
||||
var persistedState = new PersistedState(persistedStateValue);
|
||||
|
||||
var obj = persistedState.get();
|
||||
_.assign(obj, insertedObj);
|
||||
|
||||
expect(obj).to.have.property('farewell');
|
||||
expect(persistedState.get()).to.not.have.property('farewell');
|
||||
});
|
||||
});
|
||||
|
||||
describe('JSON importing and exporting', function () {
|
||||
var persistedStateValue;
|
||||
|
||||
beforeEach(function () {
|
||||
persistedStateValue = { one: 1, two: 2, 'meaning of life': 42 };
|
||||
});
|
||||
|
||||
describe('exporting state to JSON', function () {
|
||||
it('should return the full JSON representation', function () {
|
||||
var persistedState = new PersistedState(persistedStateValue);
|
||||
|
||||
var json = persistedState.toJSON();
|
||||
expect(json).to.eql(persistedStateValue);
|
||||
});
|
||||
|
||||
it('should return the JSON representation of the child state', function () {
|
||||
var persistedState = new PersistedState(persistedStateValue);
|
||||
var childState = persistedState.createChild('awesome', { pants: false });
|
||||
|
||||
expect(childState.toJSON()).to.eql({ pants: false });
|
||||
// verify JSON output of the parent state
|
||||
var parentCompare = _.assign({ awesome: { pants: false }}, persistedStateValue);
|
||||
expect(persistedState.toJSON()).to.eql(parentCompare);
|
||||
});
|
||||
|
||||
it('should export stringified version of state', function () {
|
||||
var persistedState = new PersistedState(persistedStateValue);
|
||||
var childState = persistedState.createChild('awesome', { pants: false });
|
||||
|
||||
var data = childState.toString();
|
||||
expect(JSON.parse(data)).to.eql({ pants: false });
|
||||
// verify JSON output of the parent state
|
||||
var parentCompare = _.assign({ awesome: { pants: false }}, persistedStateValue);
|
||||
expect(JSON.parse(persistedState.toString())).to.eql(parentCompare);
|
||||
});
|
||||
});
|
||||
|
||||
describe('importing state from JSON string (hydration)', function () {
|
||||
it('should set the state from JSON string input', function () {
|
||||
var stateJSON = JSON.stringify(persistedStateValue);
|
||||
var persistedState = new PersistedState();
|
||||
expect(persistedState.get()).to.eql({});
|
||||
|
||||
persistedState.fromString(stateJSON);
|
||||
expect(persistedState.get()).to.eql(persistedStateValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get state', function () {
|
||||
it('should perform deep gets with vairous formats', function () {
|
||||
var obj = {
|
||||
red: {
|
||||
green: {
|
||||
blue: 'yellow'
|
||||
}
|
||||
},
|
||||
orange: [1, 2, false, 4],
|
||||
purple: {
|
||||
violet: ''
|
||||
}
|
||||
};
|
||||
var persistedState = new PersistedState(obj);
|
||||
expect(persistedState.get()).to.eql(obj);
|
||||
|
||||
expect(persistedState.get('red')).to.eql({ green: { blue: 'yellow' } });
|
||||
expect(persistedState.get('red.green')).to.eql({ blue: 'yellow' });
|
||||
expect(persistedState.get('red[green]')).to.eql({ blue: 'yellow' });
|
||||
expect(persistedState.get(['red', 'green'])).to.eql({ blue: 'yellow' });
|
||||
expect(persistedState.get('red.green.blue')).to.eql('yellow');
|
||||
expect(persistedState.get('red[green].blue')).to.eql('yellow');
|
||||
expect(persistedState.get('red.green[blue]')).to.eql('yellow');
|
||||
expect(persistedState.get('red[green][blue]')).to.eql('yellow');
|
||||
expect(persistedState.get('red.green.blue')).to.eql('yellow');
|
||||
expect(persistedState.get('orange')).to.eql([1, 2, false, 4]);
|
||||
expect(persistedState.get('orange[0]')).to.equal(1);
|
||||
expect(persistedState.get('orange[2]')).to.equal(false);
|
||||
expect(persistedState.get('purple')).to.eql({ violet: '' });
|
||||
});
|
||||
|
||||
it('should perform deep gets with arrays', function () {
|
||||
var persistedState = new PersistedState({ hello: { nouns: ['world', 'humans', 'everyone'] } });
|
||||
|
||||
expect(persistedState.get()).to.eql({ hello: { nouns: ['world', 'humans', 'everyone'] } });
|
||||
expect(persistedState.get('hello')).to.eql({ nouns: ['world', 'humans', 'everyone'] });
|
||||
expect(persistedState.get('hello.nouns')).to.eql(['world', 'humans', 'everyone']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('set state', function () {
|
||||
describe('path format support', function () {
|
||||
it('should create deep objects from dot notation', function () {
|
||||
var persistedState = new PersistedState();
|
||||
persistedState.set('one.two.three', 4);
|
||||
expect(persistedState.get()).to.eql({ one: { two: { three: 4 } } });
|
||||
});
|
||||
|
||||
it('should create deep objects from array notation', function () {
|
||||
var persistedState = new PersistedState();
|
||||
persistedState.set('one[two][three]', 4);
|
||||
expect(persistedState.get()).to.eql({ one: { two: { three: 4 } } });
|
||||
});
|
||||
|
||||
it('should create deep objects from arrays', function () {
|
||||
var persistedState = new PersistedState();
|
||||
persistedState.set(['one', 'two', 'three'], 4);
|
||||
expect(persistedState.get()).to.eql({ one: { two: { three: 4 } } });
|
||||
});
|
||||
|
||||
it('should create deep objects with an existing path', function () {
|
||||
var persistedState = new PersistedState({}, 'deep.path');
|
||||
persistedState.set('green[red].blue', 4);
|
||||
expect(persistedState.get()).to.eql({ green: { red: { blue: 4 } }});
|
||||
});
|
||||
});
|
||||
|
||||
describe('simple replace operations', function () {
|
||||
var persistedState;
|
||||
|
||||
it('should replace value with string', function () {
|
||||
persistedState = new PersistedState({ hello: 'world' });
|
||||
expect(persistedState.get()).to.eql({ hello: 'world' });
|
||||
|
||||
persistedState.set('hello', 'fare thee well');
|
||||
expect(persistedState.get()).to.eql({ hello: 'fare thee well' });
|
||||
});
|
||||
|
||||
it('should replace value with array', function () {
|
||||
persistedState = new PersistedState({ hello: ['world', 'everyone'] });
|
||||
expect(persistedState.get()).to.eql({ hello: ['world', 'everyone'] });
|
||||
|
||||
persistedState.set('hello', ['people']);
|
||||
expect(persistedState.get()).to.eql({ hello: ['people'] });
|
||||
});
|
||||
|
||||
it('should replace value with object', function () {
|
||||
persistedState = new PersistedState({ hello: 'world' });
|
||||
expect(persistedState.get()).to.eql({ hello: 'world' });
|
||||
|
||||
persistedState.set('hello', { message: 'fare thee well' });
|
||||
expect(persistedState.get()).to.eql({ hello: { message: 'fare thee well' } });
|
||||
});
|
||||
|
||||
it('should replace value with object, removing old properties', function () {
|
||||
persistedState = new PersistedState({ hello: { message: 'world' } });
|
||||
expect(persistedState.get()).to.eql({ hello: { message: 'world' } });
|
||||
|
||||
persistedState.set('hello', { length: 5 });
|
||||
expect(persistedState.get()).to.eql({ hello: { length: 5 }});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deep replace operations', function () {
|
||||
var persistedState;
|
||||
|
||||
it('should append to the object', function () {
|
||||
persistedState = new PersistedState({ hello: { message: 'world' } });
|
||||
expect(persistedState.get()).to.eql({ hello: { message: 'world' } });
|
||||
|
||||
persistedState.set('hello.length', 5);
|
||||
expect(persistedState.get()).to.eql({ hello: { message: 'world', length: 5 } });
|
||||
});
|
||||
|
||||
it('should change the value in the array', function () {
|
||||
persistedState = new PersistedState({ hello: { nouns: ['world', 'humans', 'everyone'] } });
|
||||
persistedState.set('hello.nouns[1]', 'aliens');
|
||||
|
||||
expect(persistedState.get()).to.eql({ hello: { nouns: ['world', 'aliens', 'everyone'] } });
|
||||
expect(persistedState.get('hello')).to.eql({ nouns: ['world', 'aliens', 'everyone'] });
|
||||
expect(persistedState.get('hello.nouns')).to.eql(['world', 'aliens', 'everyone']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('internal state tracking', function () {
|
||||
it('should be an empty object', function () {
|
||||
var persistedState = new PersistedState();
|
||||
expect(persistedState._defaultState).to.eql({});
|
||||
});
|
||||
|
||||
it('should store the default state value', function () {
|
||||
var val = { one: 1, two: 2 };
|
||||
var persistedState = new PersistedState(val);
|
||||
expect(persistedState._defaultState).to.eql(val);
|
||||
});
|
||||
|
||||
it('should keep track of changes', function () {
|
||||
var val = { one: 1, two: 2 };
|
||||
var persistedState = new PersistedState(val);
|
||||
|
||||
persistedState.set('two', 22);
|
||||
expect(persistedState._defaultState).to.eql(val);
|
||||
expect(persistedState._changedState).to.eql({ two: 22 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', function () {
|
||||
var persistedState;
|
||||
var emitter;
|
||||
|
||||
var getByType = function (type, spy) {
|
||||
spy = spy || emitter;
|
||||
return spy.getCalls().filter(function (call) {
|
||||
return call.args[0] === type;
|
||||
});
|
||||
};
|
||||
|
||||
var watchEmitter = function (state) {
|
||||
return sinon.spy(state, 'emit');
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
persistedState = new PersistedState({ checker: { events: 'event tests' } });
|
||||
emitter = watchEmitter(persistedState);
|
||||
});
|
||||
|
||||
it('should emit set when setting values', function () {
|
||||
expect(getByType('set')).to.have.length(0);
|
||||
persistedState.set('checker.time', 'now');
|
||||
expect(getByType('set')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should emit change when changing values', function () {
|
||||
expect(getByType('change')).to.have.length(0);
|
||||
persistedState.set('checker.time', 'now');
|
||||
expect(getByType('change')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should not emit change when values are identical', function () {
|
||||
expect(getByType('change')).to.have.length(0);
|
||||
// check both forms of setting the same value
|
||||
persistedState.set('checker', { events: 'event tests' });
|
||||
expect(getByType('change')).to.have.length(0);
|
||||
persistedState.set('checker.events', 'event tests');
|
||||
expect(getByType('change')).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should emit change when values change', function () {
|
||||
expect(getByType('change')).to.have.length(0);
|
||||
persistedState.set('checker.events', 'i changed');
|
||||
expect(getByType('change')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should not emit change when createChild has no value', function () {
|
||||
expect(getByType('change')).to.have.length(0);
|
||||
persistedState.createChild('checker');
|
||||
expect(getByType('change')).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should not emit change when createChild is same value', function () {
|
||||
expect(getByType('change')).to.have.length(0);
|
||||
persistedState.createChild('checker', { events: 'event tests' });
|
||||
expect(getByType('change')).to.have.length(0);
|
||||
persistedState.createChild('checker.events', 'event tests');
|
||||
expect(getByType('change')).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should emit change when createChild changes existing value', function () {
|
||||
expect(getByType('change')).to.have.length(0);
|
||||
persistedState.createChild('checker', { events: 'changed via child' });
|
||||
expect(getByType('change')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should emit change when createChild adds new value', function () {
|
||||
expect(getByType('change')).to.have.length(0);
|
||||
persistedState.createChild('new.path', { another: 'thing' });
|
||||
expect(getByType('change')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should emit on parent and child instances', function (done) {
|
||||
var child = persistedState.createChild('checker');
|
||||
expect(getByType('change')).to.have.length(0);
|
||||
|
||||
// parent and child should emit, set up listener to test
|
||||
child.on('change', function () {
|
||||
// child fired, make sure parent fires as well
|
||||
expect(getByType('change')).to.have.length(1);
|
||||
done();
|
||||
});
|
||||
|
||||
child.set('events', 'changed via child set');
|
||||
});
|
||||
});
|
||||
});
|
196
src/ui/public/persisted_state/persisted_state.js
Normal file
196
src/ui/public/persisted_state/persisted_state.js
Normal file
|
@ -0,0 +1,196 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
var toPath = require('lodash/internal/toPath');
|
||||
var errors = require('ui/errors');
|
||||
|
||||
return function (Private) {
|
||||
var Events = Private(require('ui/events'));
|
||||
var SimpleEmitter = require('ui/utils/SimpleEmitter');
|
||||
|
||||
function validateParent(parent, path) {
|
||||
if (path.length <= 0) {
|
||||
throw new errors.PersistedStateError('PersistedState child objects must contain a path');
|
||||
}
|
||||
|
||||
if (parent instanceof PersistedState) return;
|
||||
throw new errors.PersistedStateError('Parent object must be an instance of PersistedState');
|
||||
}
|
||||
|
||||
function validateValue(value) {
|
||||
var msg = 'State value must be a plain object';
|
||||
if (!value) return;
|
||||
if (!_.isPlainObject(value)) throw new errors.PersistedStateError(msg);
|
||||
}
|
||||
|
||||
function parentDelegationMixin(from, to) {
|
||||
_.forOwn(from.prototype, function (method, methodName) {
|
||||
to.prototype[methodName] = function () {
|
||||
return from.prototype[methodName].apply(this._parent || this, arguments);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
_.class(PersistedState).inherits(Events);
|
||||
parentDelegationMixin(SimpleEmitter, PersistedState);
|
||||
parentDelegationMixin(Events, PersistedState);
|
||||
|
||||
function PersistedState(value, path, parent) {
|
||||
PersistedState.Super.call(this);
|
||||
|
||||
this._path = this._setPath(path);
|
||||
this._parent = parent || false;
|
||||
|
||||
if (this._parent) {
|
||||
validateParent(this._parent, this._path);
|
||||
} else if (!this._path.length) {
|
||||
validateValue(value);
|
||||
}
|
||||
|
||||
value = value || this._getDefault();
|
||||
|
||||
// copy passed state values and create internal trackers
|
||||
this.set(value);
|
||||
this._initialized = true; // used to track state changes
|
||||
}
|
||||
|
||||
PersistedState.prototype.get = function (key, def) {
|
||||
return _.cloneDeep(this._get(key, def));
|
||||
};
|
||||
|
||||
PersistedState.prototype.set = function (key, value) {
|
||||
// key must be the value, set the entire state using it
|
||||
if (_.isUndefined(value) && _.isPlainObject(key)) {
|
||||
// swap the key and value to write to the state
|
||||
value = key;
|
||||
key = undefined;
|
||||
}
|
||||
|
||||
// ensure the value being passed in is never mutated
|
||||
value = _.cloneDeep(value);
|
||||
|
||||
var val = this._set(key, value);
|
||||
this.emit('set');
|
||||
return val;
|
||||
};
|
||||
|
||||
PersistedState.prototype.reset = function (key) {
|
||||
this.set(key, undefined);
|
||||
};
|
||||
|
||||
PersistedState.prototype.clear = function (key) {
|
||||
this.set(key, null);
|
||||
};
|
||||
|
||||
PersistedState.prototype.createChild = function (path, value) {
|
||||
return new PersistedState(value, this._getIndex(path), this._parent || this);
|
||||
};
|
||||
|
||||
PersistedState.prototype.getChanges = function () {
|
||||
return _.cloneDeep(this._changedState);
|
||||
};
|
||||
|
||||
PersistedState.prototype.toJSON = function () {
|
||||
return this.get();
|
||||
};
|
||||
|
||||
PersistedState.prototype.toString = function () {
|
||||
return JSON.stringify(this.toJSON());
|
||||
};
|
||||
|
||||
PersistedState.prototype.fromString = function (input) {
|
||||
return this.set(JSON.parse(input));
|
||||
};
|
||||
|
||||
PersistedState.prototype._getIndex = function (key) {
|
||||
if (_.isUndefined(key)) return this._path;
|
||||
return (this._path || []).concat(toPath(key));
|
||||
};
|
||||
|
||||
PersistedState.prototype._getDefault = function () {
|
||||
var def = (this._hasPath()) ? undefined : {};
|
||||
return (this._parent ? this.get() : def);
|
||||
};
|
||||
|
||||
PersistedState.prototype._setPath = function (path) {
|
||||
var isString = _.isString(path);
|
||||
var isArray = _.isArray(path);
|
||||
|
||||
if (!isString && !isArray) return [];
|
||||
return (isString) ? [this._getIndex(path)] : path;
|
||||
};
|
||||
|
||||
PersistedState.prototype._hasPath = function () {
|
||||
return this._path.length > 0;
|
||||
};
|
||||
|
||||
PersistedState.prototype._get = function (key, def) {
|
||||
// delegate to parent instance
|
||||
if (this._parent) return this._parent._get(this._getIndex(key), key);
|
||||
|
||||
// no path and no key, get the whole state
|
||||
if (!this._hasPath() && _.isUndefined(key)) {
|
||||
return this._mergedState;
|
||||
}
|
||||
|
||||
return _.get(this._mergedState, this._getIndex(key), def);
|
||||
};
|
||||
|
||||
PersistedState.prototype._set = function (key, value, initialChildState, defaultChildState) {
|
||||
var self = this;
|
||||
var stateChanged = false;
|
||||
var initialState = !this._initialized;
|
||||
var keyPath = this._getIndex(key);
|
||||
|
||||
// if this is the initial state value, save value as the default
|
||||
if (initialState) {
|
||||
this._changedState = {};
|
||||
if (!this._hasPath() && _.isUndefined(key)) this._defaultState = value;
|
||||
else this._defaultState = _.set({}, keyPath, value);
|
||||
}
|
||||
|
||||
// delegate to parent instance, passing child's default value
|
||||
if (this._parent) {
|
||||
return this._parent._set(keyPath, value, initialState, this._defaultState);
|
||||
}
|
||||
|
||||
// everything in here affects only the parent state
|
||||
if (!initialState) {
|
||||
// no path and no key, set the whole state
|
||||
if (!this._hasPath() && _.isUndefined(key)) {
|
||||
// check for changes and emit an event when found
|
||||
stateChanged = !_.isEqual(this._changedState, value);
|
||||
if (!initialChildState) this._changedState = value;
|
||||
} else {
|
||||
// check for changes and emit an event when found
|
||||
stateChanged = !_.isEqual(this.get(keyPath), value);
|
||||
|
||||
// arrays merge by index, not the desired behavior - ensure they are replaced
|
||||
if (!initialChildState) {
|
||||
if (_.isArray(_.get(this._mergedState, keyPath))) {
|
||||
_.set(this._mergedState, keyPath, undefined);
|
||||
}
|
||||
_.set(this._changedState, keyPath, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var targetObj = this._mergedState || _.cloneDeep(this._defaultState);
|
||||
var sourceObj = _.merge({}, defaultChildState, this._changedState);
|
||||
var mergeMethod = function (targetValue, sourceValue, mergeKey) {
|
||||
// If `mergeMethod` is provided it is invoked to produce the merged values of the destination and
|
||||
// source properties. If `mergeMethod` returns `undefined` merging is handled by the method instead
|
||||
// handler arguments are (targetValue, sourceValue, key, target, source)
|
||||
if (!initialState && !initialChildState && _.isEqual(keyPath, self._getIndex(mergeKey))) {
|
||||
return !_.isUndefined(sourceValue) ? sourceValue : targetValue;
|
||||
}
|
||||
};
|
||||
this._mergedState = _.merge(targetObj, sourceObj, mergeMethod);
|
||||
|
||||
if (stateChanged) this.emit('change');
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
return PersistedState;
|
||||
};
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue