Skip to content

Commit

Permalink
Split out PersistedState business logic from angular (#9687)
Browse files Browse the repository at this point in the history
* - Remove angularization of PersistedState.
- Provide the angularized version with PersistedStateProvider.
- Use named exports
- Es6-ify PersistedState class

This pushes forward the first bullet point of #9686

* Address code review comments

- Use single point of entry
- Use injector instead of private
- Use a new class (‘AngularizedPersistedState’) name instead of an
arrow function

* Remove extra import line
  • Loading branch information
stacey-gammon authored Jan 18, 2017
1 parent 6fd7280 commit 5f4cf32
Show file tree
Hide file tree
Showing 23 changed files with 185 additions and 163 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,19 @@ import noDigestPromises from 'test_utils/no_digest_promises';
import ngMock from 'ng_mock';
import expect from 'expect.js';
import errors from 'ui/errors';
import PersistedStatePersistedStateProvider from 'ui/persisted_state/persisted_state';
import EventsProvider from 'ui/events';
import 'ui/persisted_state';

let PersistedState;
let Events;

describe('Persisted State', function () {
describe('Persisted State Provider', function () {
noDigestPromises.activateForSuite();

beforeEach(function () {
ngMock.module('kibana');

ngMock.inject(function (Private) {
PersistedState = Private(PersistedStatePersistedStateProvider);
Events = Private(EventsProvider);
ngMock.inject(function ($injector) {
PersistedState = $injector.get('PersistedState');
});
});

Expand All @@ -30,11 +28,6 @@ describe('Persisted State', function () {
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 () {
const val = { red: 'blue' };
persistedState = new PersistedState(val);
Expand Down
2 changes: 2 additions & 0 deletions src/ui/public/persisted_state/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import './persisted_state.factory.js';
export { PersistedState } from './persisted_state.js';
33 changes: 33 additions & 0 deletions src/ui/public/persisted_state/persisted_state.factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @name AngularPersistedState
*
* Returns a PersistedState object which uses an EventEmitter instead of
* the SimpleEmitter. The EventEmitter adds digest loops every time a handler is called
* so it's preferable to use this variation when a callback modifies angular UI.
*
* TODO: The handlers themselves should really be responsible for triggering digest loops
* as opposed to having an all or nothing situation. A nice goal would be to get rid
* of the EventEmitter entirely and require handlers that need it to trigger a digest loop
* themselves. We can even supply a service to wrap the callbacks in a function that
* would call the callback, and finish with a $rootScope.$apply().
*/

import EventsProvider from 'ui/events';
import { PersistedState } from './persisted_state';
import uiModules from 'ui/modules';

const module = uiModules.get('kibana');

module.factory('PersistedState', ($injector) => {
const Private = $injector.get('Private');
const Events = Private(EventsProvider);

// Extend PersistedState to override the EmitterClass class with
// our Angular friendly version.
return class AngularPersistedState extends PersistedState {
constructor(value, path, parent, silent) {
super(value, path, parent, silent, Events);
}
};
});

172 changes: 83 additions & 89 deletions src/ui/public/persisted_state/persisted_state.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,63 +8,59 @@ import _ from 'lodash';
import toPath from 'lodash/internal/toPath';
import errors from 'ui/errors';
import SimpleEmitter from 'ui/utils/simple_emitter';
import EventsProvider from 'ui/events';

export default function (Private) {
const Events = Private(EventsProvider);

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) {
const msg = 'State value must be a plain object';
if (!value) return;
if (!_.isPlainObject(value)) throw new errors.PersistedStateError(msg);
function prepSetParams(key, value, path) {
// key must be the value, set the entire state using it
if (_.isUndefined(value) && (_.isPlainObject(key) || path.length > 0)) {
// setting entire tree, swap the key and value to write to the state
value = key;
key = undefined;
}

function prepSetParams(key, value, path) {
// key must be the value, set the entire state using it
if (_.isUndefined(value) && (_.isPlainObject(key) || path.length > 0)) {
// setting entire tree, swap the key and value to write to the state
value = key;
key = undefined;
}
// ensure the value being passed in is never mutated
return {
value: _.cloneDeep(value),
key: key
};
}

// ensure the value being passed in is never mutated
return {
value: _.cloneDeep(value),
key: key
};
}
export class PersistedState {

/**
*
* @param value
* @param path
* @param parent
* @param silent
* @param EmitterClass {SimpleEmitter} - a SimpleEmitter class that this class will extend. Can be used to
* inherit a custom event emitter. For example, the EventEmitter is an "angular-ized" version
* for angular components which automatically triggers a digest loop for every registered
* handler. TODO: Get rid of the need for EventEmitter by wrapping handlers that require it
* in a special function that will handler triggering the digest loop.
*/
constructor(value, path, parent, silent, EmitterClass = SimpleEmitter) {
EmitterClass.call(this);

this._EmitterClass = EmitterClass;
this._path = this._setPath(path);
this._parent = parent || false;

function parentDelegationMixin(from, to) {
_.forOwn(from.prototype, function (method, methodName) {
to.prototype[methodName] = function () {
return from.prototype[methodName].apply(this._parent || this, arguments);
_.forOwn(EmitterClass.prototype, (method, methodName) => {
this[methodName] = function () {
return EmitterClass.prototype[methodName].apply(this._parent || this, arguments);
};
});
}

_.class(PersistedState).inherits(Events);
parentDelegationMixin(SimpleEmitter, PersistedState);
parentDelegationMixin(Events, PersistedState);

function PersistedState(value, path, parent, silent) {
PersistedState.Super.call(this);

this._path = this._setPath(path);
this._parent = parent || false;

// Some validations
if (this._parent) {
validateParent(this._parent, this._path);
} else if (!this._path.length) {
validateValue(value);
if (this._path.length <= 0) {
throw new errors.PersistedStateError('PersistedState child objects must contain a path');
}
if (!(this._parent instanceof PersistedState)) {
throw new errors.PersistedStateError('Parent object must be an instance of PersistedState');
}
} else if (!this._path.length && value && !_.isPlainObject(value)) {
throw new errors.PersistedStateError('State value must be a plain object');
}

value = value || this._getDefault();
Expand All @@ -74,23 +70,23 @@ export default function (Private) {
this._initialized = true; // used to track state changes
}

PersistedState.prototype.get = function (key, def) {
get(key, def) {
return _.cloneDeep(this._get(key, def));
};
}

PersistedState.prototype.set = function (key, value) {
set(key, value) {
const params = prepSetParams(key, value, this._path);
const val = this._set(params.key, params.value);
this.emit('set');
return val;
};
}

PersistedState.prototype.setSilent = function (key, value) {
setSilent(key, value) {
const params = prepSetParams(key, value, this._path);
return this._set(params.key, params.value, true);
};
}

PersistedState.prototype.reset = function (path) {
reset(path) {
const keyPath = this._getIndex(path);
const origValue = _.get(this._defaultState, keyPath);
const currentValue = _.get(this._mergedState, keyPath);
Expand All @@ -106,50 +102,50 @@ export default function (Private) {
this._cleanPath(path, this._defaultChildState);

if (!_.isEqual(currentValue, origValue)) this.emit('change');
};
}

PersistedState.prototype.createChild = function (path, value, silent) {
createChild(path, value, silent) {
this._setChild(this._getIndex(path), value, this._parent || this);
return new PersistedState(value, this._getIndex(path), this._parent || this, silent);
};
return new PersistedState(value, this._getIndex(path), this._parent || this, silent, this._EmitterClass);
}

PersistedState.prototype.removeChild = function (path) {
removeChild(path) {
const origValue = _.get(this._defaultState, this._getIndex(path));

if (_.isUndefined(origValue)) {
this.reset(path);
} else {
this.set(path, origValue);
}
};
}

PersistedState.prototype.getChanges = function () {
getChanges() {
return _.cloneDeep(this._changedState);
};
}

PersistedState.prototype.toJSON = function () {
toJSON() {
return this.get();
};
}

PersistedState.prototype.toString = function () {
toString() {
return JSON.stringify(this.toJSON());
};
}

PersistedState.prototype.fromString = function (input) {
fromString(input) {
return this.set(JSON.parse(input));
};
}

PersistedState.prototype._getIndex = function (key) {
_getIndex(key) {
if (_.isUndefined(key)) return this._path;
return (this._path || []).concat(toPath(key));
};
}

PersistedState.prototype._getPartialIndex = function (key) {
_getPartialIndex(key) {
const keyPath = this._getIndex(key);
return keyPath.slice(this._path.length);
};
}

PersistedState.prototype._cleanPath = function (path, stateTree) {
_cleanPath(path, stateTree) {
const partialPath = this._getPartialIndex(path);
let remove = true;

Expand All @@ -165,31 +161,31 @@ export default function (Private) {
if (remove) delete stateVal[lastKey];
if (Object.keys(stateVal).length > 0) remove = false;
}
};
}

PersistedState.prototype._getDefault = function () {
_getDefault() {
const def = (this._hasPath()) ? undefined : {};
return (this._parent ? this.get() : def);
};
}

PersistedState.prototype._setPath = function (path) {
_setPath(path) {
const isString = _.isString(path);
const isArray = _.isArray(path);

if (!isString && !isArray) return [];
return (isString) ? [this._getIndex(path)] : path;
};
}

PersistedState.prototype._setChild = function (path, value, parent) {
_setChild(path, value, parent) {
parent._defaultChildState = parent._defaultChildState || {};
_.set(parent._defaultChildState, path, value);
};
}

PersistedState.prototype._hasPath = function () {
_hasPath() {
return this._path.length > 0;
};
}

PersistedState.prototype._get = function (key, def) {
_get(key, def) {
// delegate to parent instance
if (this._parent) return this._parent._get(this._getIndex(key), def);

Expand All @@ -199,9 +195,9 @@ export default function (Private) {
}

return _.get(this._mergedState, this._getIndex(key), def);
};
}

PersistedState.prototype._set = function (key, value, silent, initialChildState) {
_set(key, value, silent, initialChildState) {
const self = this;
let stateChanged = false;
const initialState = !this._initialized;
Expand Down Expand Up @@ -272,7 +268,5 @@ export default function (Private) {
if (!silent && stateChanged) this.emit('change');

return this;
};

return PersistedState;
}
}
7 changes: 4 additions & 3 deletions src/ui/public/state_management/app_state.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
import _ from 'lodash';
import modules from 'ui/modules';
import StateManagementStateProvider from 'ui/state_management/state';
import PersistedStatePersistedStateProvider from 'ui/persisted_state/persisted_state';
import 'ui/persisted_state';

const urlParam = '_a';

function AppStateProvider(Private, $rootScope, $location) {
function AppStateProvider(Private, $rootScope, $location, $injector) {
const State = Private(StateManagementStateProvider);
const PersistedState = Private(PersistedStatePersistedStateProvider);
const PersistedState = $injector.get('PersistedState');
let persistedStates;
let eventUnsubscribers;

Expand Down
3 changes: 1 addition & 2 deletions src/ui/public/vis/vis.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ import _ from 'lodash';
import AggTypesIndexProvider from 'ui/agg_types/index';
import RegistryVisTypesProvider from 'ui/registry/vis_types';
import VisAggConfigsProvider from 'ui/vis/agg_configs';
import PersistedStateProvider from 'ui/persisted_state/persisted_state';
import { PersistedState } from 'ui/persisted_state';

export default function VisFactory(Notifier, Private) {
const aggTypes = Private(AggTypesIndexProvider);
const visTypes = Private(RegistryVisTypesProvider);
const AggConfigs = Private(VisAggConfigsProvider);
const PersistedState = Private(PersistedStateProvider);

const notify = new Notifier({
location: 'Vis'
Expand Down
Loading

0 comments on commit 5f4cf32

Please sign in to comment.