Skip to content

Commit

Permalink
enforcing strict lodash path strings, readme update
Browse files Browse the repository at this point in the history
  • Loading branch information
bhoriuchi committed Jan 17, 2018
1 parent 77c0e73 commit f4e1118
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 109 deletions.
5 changes: 4 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
dist
archive
test
test
index.js
vue-deepset.js
vue-deepset.min.js
45 changes: 38 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# vue-deepset
Deep set Vue.js objects
Deep set Vue.js objects using dynamic paths

---

Binding deeply nested data properties and vuex data to a form or component can be tricky. The following set of tools aims to simplify data bindings. Compatible with `Vue 1.x`, `Vue 2.x`, `Vuex 1.x`, and `Vuex 2.x`

**Note** `vueModel` and `vuexModel` use [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) objects if supported by the browser and fallback to an object with generated fields based on the target object. Because of this, it is always best to pre-define the properties of an object.
**Note** `vueModel` and `vuexModel` use [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) objects if supported by the browser and fallback to an object with generated fields based on the target object. Because of this, it is always best to pre-define the properties of an object when using older browsers.

Also note that models are flat and once built can set vue/vuex directly using `model[path] = value`
Also note that models are flat and once built can set vue/vuex directly using `model[path] = value` where path is a lodash formatted path string or path array.

### Examples

Expand All @@ -18,9 +18,41 @@ Full examples can be found in the [tests](/~https://github.com/bhoriuchi/vue-deeps
* `vue@>=1.0.0`
* `vuex@>=1.0.0` (optional)

### Dynamic paths

If you knew what every path you needed ahead of time you could (while tedious) create custom computed properties with getter and setter methods for each of those properties. But what if you have a dynamic and deeply nested property? This problem was actually what inspired the creation of this library. Using a Proxy, `vue-deepset` is able to dynamically create new, deep, reactive properties as well as return `undefined` for values that are not yet set.

### Path Strings

The modeling methods use `lodash.toPath` format for path strings. Please ensure references use this format
The modeling methods use `lodash.toPath` format for path strings. Please ensure references use this format. You may also use `path Arrays` which can be easier to construct when using keys that include dots.

The following 2 path values are the same
```js
const stringPath = 'a.b["c.d"].e[0]'
const arrayPath = [ 'a', 'b', 'c.d', 'e', 0 ]
```

#### Keys with dots

Since dots prefix a nested path and are also valid characters for a key data that looks like the following can be tricky

```js
const data = {
'foo.bar': 'baz',
foo: {
bar: 'qux'
}
}
```

So care should be taken when building the path string (or just use an array path) as the following will be true

```js
'foo.bar' // qux
'["foo.bar"]' // baz
'foo["bar"]' // qux
'["foo"].bar' // qux
```

### Binding `v-model` to deeply nested objects

Expand All @@ -35,7 +67,7 @@ Model objects returned by `$deepModel`, `vueModel`, and `vuexModel` are flat and
## Usage

* Webpack `import * as VueDeepSet from 'vue-deepset'`
* Browser `<script src='./node_modules/vue-deepset/vue-deepset.js'></script>`
* Browser `<script src='./node_modules/vue-deepset/vue-deepset.min.js'></script>`

### As a Plugin

Expand Down Expand Up @@ -212,9 +244,8 @@ import { vueSet } from 'vue-deepset'

export default {
methods: {
vueSet: vueSet,
clearForm () {
this.vueSet(this.localForm, 'message', '')
vueSet(this.localForm, 'message', '')
}
},
data: {
Expand Down
73 changes: 42 additions & 31 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ var rePropName = RegExp(
// Or match "" as the space between consecutive dots or empty brackets.
'(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))', 'g');

// modified from lodash
// modified from lodash - /~https://github.com/lodash/lodash
function toPath(string) {
if (Array.isArray(string)) {
return string;
}
var result = [];
if (string.charCodeAt(0) === charCodeOfDot) {
result.push('');
Expand All @@ -57,6 +60,14 @@ function toPath(string) {

function noop() {}

function hasOwnProperty(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
}

function deepsetError(message) {
return new Error('[vue-deepset]: ' + message);
}

function isObjectLike(object) {
return (typeof object === 'undefined' ? 'undefined' : _typeof(object)) === 'object' && object !== null;
}
Expand Down Expand Up @@ -117,12 +128,11 @@ function getPaths(object) {
pushPaths(val, (current + '.' + key).replace(/^\./, ''), paths);
pushPaths(val, (current + '[' + key + ']').replace(/^\./, ''), paths);
pushPaths(val, (current + '["' + key + '"]').replace(/^\./, ''), paths);
} else if (key.match(invalidKey) !== null) {
// must quote
pushPaths(val, (current + '["' + key + '"]').replace(/^\./, ''), paths);
} else {
} else if (!key.match(invalidKey)) {
pushPaths(val, (current + '.' + key).replace(/^\./, ''), paths);
}
// always add the absolute array notation path
pushPaths(val, (current + '["' + key + '"]').replace(/^\./, ''), paths);
});
}
return [].concat(new Set(paths));
Expand All @@ -145,22 +155,14 @@ function _get(obj, path, defaultValue) {
return defaultValue;
}

function hasOwnProperty(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
}

function getProp(object, property) {
return Object.keys(object).indexOf(property) === -1 ? _get(object, property) : object[property];
}

function getProxy(vm, base, options) {
noop(options); // for future potential options
var isVuex = typeof base === 'string';
var object = isVuex ? _get(vm.$store.state, base) : base;

return new Proxy(object, {
get: function get(target, property) {
return getProp(target, property);
return _get(target, property);
},
set: function set(target, property, value) {
isVuex ? vuexSet.call(vm, pathJoin(base, property), value) : vueSet(target, property, value);
Expand All @@ -183,7 +185,7 @@ function getProxy(vm, base, options) {
},
getOwnPropertyDescriptor: function getOwnPropertyDescriptor(target, property) {
return {
value: getProp(target, property),
value: _get(target, property),
writable: false,
enumerable: true,
configurable: true
Expand Down Expand Up @@ -230,22 +232,30 @@ function buildVuexModel(vm, vuexPath, options) {
}

function vueSet(object, path, value) {
var parts = toPath(path);
var obj = object;
while (parts.length) {
var key = parts.shift();
if (!parts.length) {
_vue2.default.set(obj, key, value);
} else if (!hasOwnProperty(obj, key) || obj[key] === null) {
_vue2.default.set(obj, key, typeof key === 'number' ? [] : {});
try {
var parts = toPath(path);
var obj = object;
while (parts.length) {
var key = parts.shift();
if (!parts.length) {
_vue2.default.set(obj, key, value);
} else if (!hasOwnProperty(obj, key) || obj[key] === null) {
_vue2.default.set(obj, key, typeof key === 'number' ? [] : {});
}
obj = obj[key];
}
obj = obj[key];
return object;
} catch (err) {
throw deepsetError('vueSet unable to set object (' + err.message + ')');
}
}

function vuexSet(path, value) {
if (!this.$store) throw new Error('[vue-deepset]: could not find vuex store object on instance');
this.$store[this.$store.commit ? 'commit' : 'dispatch']('VUEX_DEEP_SET', { path: path, value: value });
if (!isObjectLike(this.$store)) {
throw deepsetError('could not find vuex store object on instance');
}
var method = this.$store.commit ? 'commit' : 'dispatch';
this.$store[method]('VUEX_DEEP_SET', { path: path, value: value });
}

function VUEX_DEEP_SET(state, _ref) {
Expand All @@ -264,7 +274,7 @@ function extendMutation() {
function vueModel(object, options) {
var opts = Object.assign({}, options);
if (!isObjectLike(object)) {
throw new Error('[vue-deepset]: invalid object specified for vue model');
throw deepsetError('invalid object specified for vue model');
} else if (opts.useProxy === false || typeof Proxy === 'undefined') {
return buildVueModel(this, object, opts);
}
Expand All @@ -274,9 +284,11 @@ function vueModel(object, options) {
function vuexModel(vuexPath, options) {
var opts = Object.assign({}, options);
if (typeof vuexPath !== 'string' || vuexPath === '') {
throw new Error('[vue-deepset]: invalid vuex path string');
throw deepsetError('invalid vuex path string');
} else if (!isObjectLike(this.$store) || !isObjectLike(this.$store.state)) {
throw deepsetError('no vuex state found');
} else if (!has(this.$store.state, vuexPath)) {
throw new Error('[vue-deepset]: Cannot find path "' + vuexPath + '" in Vuex store');
throw deepsetError('cannot find path "' + vuexPath + '" in Vuex store');
} else if (opts.useProxy === false || typeof Proxy === 'undefined') {
return buildVuexModel(this, vuexPath, opts);
}
Expand All @@ -288,8 +300,7 @@ function deepModel(base, options) {
}

function install(VueInstance) {
VueInstance.prototype.$vueModel = vueModel;
VueInstance.prototype.$vuexModel = vuexModel;
VueInstance.prototype.$deepModel = deepModel;
VueInstance.prototype.$vueSet = vueSet;
VueInstance.prototype.$vuexSet = vuexSet;
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vue-deepset",
"version": "0.6.0",
"version": "0.6.1",
"description": "Deep set Vue.js objects",
"main": "index.js",
"scripts": {
Expand Down
Loading

0 comments on commit f4e1118

Please sign in to comment.