vuex-dot
Version:
Simplifies two-way data binding and v-model usage on vuex state, providing full reactivity via generated setters/getters
454 lines (406 loc) • 13.6 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(factory((global.VuexDot = {})));
}(this, (function (exports) { 'use strict';
/*!
* isobject <https://github.com/jonschlinkert/isobject>
*
* Copyright (c) 2014-2017, Jon Schlinkert.
* Released under the MIT License.
*/
var isobject = function isObject(val) {
return val != null && typeof val === 'object' && Array.isArray(val) === false;
};
/*!
* get-value <https://github.com/jonschlinkert/get-value>
*
* Copyright (c) 2014-2018, Jon Schlinkert.
* Released under the MIT License.
*/
var getValue = function(target, path, options) {
if (!isobject(options)) {
options = { default: options };
}
if (!isValidObject(target)) {
return typeof options.default !== 'undefined' ? options.default : target;
}
if (typeof path === 'number') {
path = String(path);
}
const isArray = Array.isArray(path);
const isString = typeof path === 'string';
const splitChar = options.separator || '.';
const joinChar = options.joinChar || (typeof splitChar === 'string' ? splitChar : '.');
if (!isString && !isArray) {
return target;
}
if (isString && path in target) {
return isValid(path, target, options) ? target[path] : options.default;
}
let segs = isArray ? path : split(path, splitChar, options);
let len = segs.length;
let idx = 0;
do {
let prop = segs[idx];
if (typeof prop === 'number') {
prop = String(prop);
}
while (prop && prop.slice(-1) === '\\') {
prop = join([prop.slice(0, -1), segs[++idx] || ''], joinChar, options);
}
if (prop in target) {
if (!isValid(prop, target, options)) {
return options.default;
}
target = target[prop];
} else {
let hasProp = false;
let n = idx + 1;
while (n < len) {
prop = join([prop, segs[n++]], joinChar, options);
if ((hasProp = prop in target)) {
if (!isValid(prop, target, options)) {
return options.default;
}
target = target[prop];
idx = n - 1;
break;
}
}
if (!hasProp) {
return options.default;
}
}
} while (++idx < len && isValidObject(target));
if (idx === len) {
return target;
}
return options.default;
};
function join(segs, joinChar, options) {
if (typeof options.join === 'function') {
return options.join(segs);
}
return segs[0] + joinChar + segs[1];
}
function split(path, splitChar, options) {
if (typeof options.split === 'function') {
return options.split(path);
}
return path.split(splitChar);
}
function isValid(key, target, options) {
if (typeof options.isValid === 'function') {
return options.isValid(key, target);
}
return true;
}
function isValidObject(val) {
return isobject(val) || Array.isArray(val) || typeof val === 'function';
}
/**
* Maps subject (Target or TargetExposition) or some field of it into `vm` compatible getter/setter pair
* @param {Target,TargetExposition} subject
* @param {String} field
* @param {Boolean} sendTarget send
* @return {*}
*/
function map(subject, field, sendTarget) {
const omitKey = !field || !!subject.customPayload;
const composeSetter = () =>
sendTarget ?
omitKey ?
function (value) {
subject.dispatcher.call(this, this.$store, value, targetGetter.call(this));
} :
function (value) {
subject.dispatcher.call(this, this.$store, value, field, targetGetter.call(this));
} :
omitKey ?
function (value) {
subject.dispatcher.call(this, this.$store, value);
} :
function (value) {
subject.dispatcher.call(this, this.$store, value, field);
};
const fieldGetter = function () {
return getValue(this, subject.path + '.' + field);
};
const targetGetter = function () {
return getValue(this, subject.path);
};
const resultGetter = field ? fieldGetter : targetGetter;
const result = {
get: subject.getterGate ? subject.getterGate(field, resultGetter) : resultGetter
};
const method = !!subject.action ? 'dispatch' : !!subject.mutation ? 'commit' : null;
const storeAction = !!subject.action ? subject.action : !!subject.mutation ? subject.mutation : null;
if (!!method) subject.hook(
sendTarget ?
omitKey ?
(store, payload, target) => store[method](storeAction, { target, payload }) :
(store, value, key, target) => store[method](storeAction, { target, key, value }) :
omitKey ?
(store, value) => store[method](storeAction, value) :
(store, value, key) => store[method](storeAction, { key, value }));
if (subject.dispatcher) result.set = subject.gate ? subject.gate(field, composeSetter()) : composeSetter();
return result;
}
var map_1 = map;
/**
* Exposes some properties of target object into computed properties compatible bunch
* of getters or/and setters
*/
class TargetExposition {
/**
*
* @param {Target} target
* @param {Array} projection
*/
constructor(target, projection) {
this.target = target;
this.projection = projection;
this.sendTarget = false;
}
/**
* Sets `mutation` to be commited on exposed field change
* if `sendTarget` is `false`, `action` shall be called in format:
*
* `commit(mutation, { key, value })`
*
* otherwise, if `sendTarget` is set to `true`
*
* `commit(mutation, { target, key, value })`
*
* **Hint**: That's just syntax sugar for `hook()` method.
* @param {String} mutation name of mutation
* @param {Boolean} sendTarget send target to action
* @return {TargetExposition}
*/
commit(mutation, sendTarget = false) {
this.target.commit(mutation);
this.sendTarget = sendTarget || false;
return this;
}
/**
* Sets `action` to be dispatched on exposed field change.
* if `sendTarget` is `false`, `action` shall be called in format:
*
* `dispatch(action, { key, value })`
*
* otherwise, if `sendTarget` is set to `true`
*
* `dispatch(action, { target, key, value })`
*
* **Hint**: That's just syntax sugar for `hook()` method.
* @param {String} action name of action
* @param {Boolean} sendTarget send target to action
* @return {TargetExposition}
*/
dispatch(action, sendTarget = false) {
this.target.dispatch(action);
this.sendTarget = sendTarget || false;
return this;
}
/**
* set callback to be run on property change
*
* @param {TargetExposition~dispatcher} dispatcher
* @param {Boolean} sendTarget
* @return {TargetExposition}
*/
hook(dispatcher, sendTarget) {
this.target.hook(dispatcher);
this.sendTarget = sendTarget || false;
return this;
}
/**
* @callback TargetExposition~dispatcher
* @param {Store} store `vuex` store
* @param {*} value changed value
* @param {String} key key of changed field
* @param {*} target parent object of changed field
*/
/**
* generates map of getters or/and setters for specified projection
*
* @return {Object}
*/
map() {
const result = {};
const { target, sendTarget } = this;
this.projection.forEach(field => {
let camelCasedField = (field.indexOf('.') === -1) ? field : field.replace(/\.(.)/g, (all, matched) => matched.toUpperCase());
result[camelCasedField] = map_1(target, field, sendTarget);
});
if (target.inject) Object.assign(result, target.inject);
return result;
}
/**
* look [Target.use(plugin)](#Target+use)
*
* @param plugin
* @return {TargetExposition}
*/
use(plugin) {
this.target.use(plugin);
return this;
}
}
var TargetExposition_1 = TargetExposition;
/**
* Target mapper
*/
class Target {
/**
* @param {string} path dot-notation path to some property of your `vm` instance
*/
constructor(path) {
this.path = path;
// properties:
// this.action = undefined;
// this.mutation = undefined;
// this.dispatcher = undefined;
// this.customPayload = undefined
}
/**
* Should be used if you need to map some properties of the object, selected as a target into your computed properties.
* It allows to attach action dispatcher or hook callback on each property change.
*
* Also, both `dispatch()` and `hook()` can provide object mapped by Target instance to the callee, while setting
* the second argument `true` (you can read more in the documentation for them)
*
* @param {array} projection `target` object properties to be exposed
* @returns {TargetExposition}
*/
expose(projection) {
return new TargetExposition_1(this, projection);
}
/**
* In fact, that's syntax sugar for `hook()` method.
*
* Sets `mutation` to be commited on mapped property change
*
* `mutation` shall be called in the format:
*
* `commit(mutation, newValue)`
*
* @param {string} mutation mutation name
* @returns {Target}
*/
commit(mutation) {
this.mutation = mutation;
return this;
}
/**
* In fact, that's syntax sugar for `hook()` method.
*
* Sets `action` to be dispatched on mapped property change
*
* Your `action` shall be called in the format:
*
* `dispatch(action, newValue)`
*
* @param {string} action action name
* @returns {Target}
*/
dispatch(action) {
this.action = action;
return this;
}
/**
* Set hook that should be run on mapped property change.
*
* @param {Target~dispatcher} dispatcher
* @returns {Target}
*/
hook(dispatcher) {
this.dispatcher = dispatcher;
return this;
}
/**
* @callback Target~dispatcher
* @param {Store} store `vuex` store
* @param {mixed} value
*/
/**
* returns computed property pair of getters or/and setters for specified projection.
*
* If an alias is set, it can be used with spread operator setting provided alias as the computed property name
*
* @param {String} alias name of computed field target to be accessible
* @returns {*}
*/
map(alias) {
if (!alias) return map_1(this);
return Object.assign({ [ alias ]: map_1(this) }, this.inject || {});
}
/**
* apply plugin
*
* plugin is described by object, composed in such format:
*
* ```javascript
* {
* setter: function(key, value, nextSetter) { //setter is mandatory
* nextSetter(value);
* },
* getter: function(key, nextGetter) { //getter is optional
* return nextGetter();
* },
* inject: { // optional, here you can describe additional fields, you want to inject into result map
* $internal: {
* get() { ... },
* set(value) { ... }
* }
* },
* customPayload: if set to `true` it means that your plugin shall use own payload format, so we do not need to pass
* key to hook (action, commit)
* }
* ```
*
* @param {Object} plugin object, describing your plugin.
* @return {Target}
*/
use(plugin) {
const makeSetterGate = oldGate => !!oldGate ?
(key, setter) => function (value) { plugin.setter.call(this, key, value, oldGate(key, setter).bind(this)); } :
(key, setter) => function (value) { plugin.setter.call(this, key, value, setter.bind(this)); };
const makeGetterGate = oldGate => !!oldGate ?
(key, getter) => function () { return plugin.getter.call(this, key, oldGate(key, getter).bind(this)); } :
(key, getter) => function () { return plugin.getter.call(this, key, getter.bind(this)); };
this.gate = makeSetterGate(this.gate);
if (!!plugin.getter) this.getterGate = makeGetterGate(this.getterGate);
this.inject = Object.assign({}, plugin.inject, this.inject);
this.customPayload = this.customPayload || plugin.customPayload;
return this;
}
}
var Target_1 = Target;
/**
* returns Target instance with specified path
* @param {string} path dotted path to target property of your component instance
* @returns {Target}
*/
const take = path => new Target_1(path);
/**
* returns Target instance with specified state path
* @param namespace
* @param path
* @return {Target}
*/
const takeState = (namespace, path) => {
if (typeof path === 'undefined') path = namespace;
else path = `${namespace.replace('/', '.')}.${path}`;
const fullPath = `$store.state.${path}`;
return new Target_1(fullPath);
};
var vuexDot = { take, takeState };
var vuexDot_1 = vuexDot.take;
var vuexDot_2 = vuexDot.takeState;
exports.default = vuexDot;
exports.take = vuexDot_1;
exports.takeState = vuexDot_2;
Object.defineProperty(exports, '__esModule', { value: true });
})));