├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── main.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | *.tgz -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Richard Tallent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-object-merge 2 | 3 | > Utility function for merging an object into a reactive object in Vue 4 | 5 | ## Purpose 6 | 7 | This library was designed to efficiently and automatically deep-merge objects with Vue-managed objects. 8 | 9 | I really enjoy Vue and Vuex's philosophy -- update your state, and your UI will reactively update automatically. But what I don't enjoy is writing code to wire API JSON responses back into my Vue `data` or Vuex `state` objects (i.e., mutation functions): 10 | 11 | ```JavaScript 12 | state: { 13 | customer { 14 | firstName: "", 15 | lastName: "" 16 | // ... 17 | } 18 | }, 19 | actions: { 20 | getCustomer: id => axios 21 | .get("/api/getCustomer/" + id) 22 | .then(function(response) { 23 | context.commit("COMMIT_CUSTOMER", response.data) 24 | }) 25 | }, 26 | mutations: { 27 | COMMIT_CUSTOMER(state, data) { 28 | state.customer.firstName = data.firstName; 29 | state.customer.lastName = data.lastName; 30 | // ... sigh ... 31 | }, 32 | } 33 | ``` 34 | 35 | (The examples here are specific to Vuex, but apply equally if you're just using the `data` object to store your state.) 36 | 37 | If an object is replaced _entirely_, all you have to do is replace one object with another. But if the API needs to return _partial object changes_ (perhaps even changes across several portions of the page state, such as updating a table and a page header and three menu options), you have to manually wire the bits and pieces. Worse, if each API call can return different portions of state, you'll be writing a LOT of boilerplate code. 38 | 39 | Vue-Object-Merge provides a single function, `stateMerge`, that performs a **deep merge** of one object into another. Basically, it greatly simplifies mapping keys from object `A` into Vue object `B`, _without_ disturbing keys in `B` that are not included in `A`: 40 | 41 | ```JavaScript 42 | mutations: { 43 | // I can call this single mutation for any API response that sparely 44 | // matches anything in the Vuex state. 45 | MERGE_STATE: (state, response) => stateMerge(state, response), 46 | } 47 | ``` 48 | 49 | Now, many of my Vuex `actions` call their API endpoint and all commit a _single mutation_ (as above). This `MERGE_STATE` mutation folds the keys and values returned from the API into the application state in one step, and it's all done in a way that Vue can react properly to the changes (_i.e._, it uses `Vue.set()`). 50 | 51 | The API need not return a full representation of the state. For example, if an API call needs to update `state.ui.userForm.fields`, the API can just return the contents of the `fields` object (or any portion thereof) and your action can look like this: 52 | 53 | ```JavaScript 54 | context.commit("MERGE_STATE", { ui: { userForm: { fields: response.data } } }) 55 | ``` 56 | 57 | This will navigate `stateMerge` directly to the place in your state where the response data should be merged, leaving the rest of your state unaffected (and even any keys of `ui.userForm.fields` that aren't part of the JSON response). 58 | 59 | In addition to API calls, it can also be used for, say, updating `data` elements that are two-way-bound to form fields to set them to a Vuex state object, or vice versa to merge changes from the form back into the Vuex state. 60 | 61 | ## Function definition 62 | 63 | ```JavaScript 64 | stateMerge(state, value, propName, ignoreNull) 65 | ``` 66 | 67 | where: 68 | 69 | - `state` is the object to be updated. 70 | - `value` is the object or value containing the change(s). 71 | - `propName` is the key of `state` to be modified (required if `value` isn't an object, otherwise optional). This was primarily intended for internal use for recursion, but can be handy in other situations. 72 | - `ignoreNull` should be set to true (default is false) if a `null` value should _not_ overwrite `state`'s value. 73 | 74 | ## Basic Logic 75 | 76 | Given objects `state` and `value`, `stateMerge` will: 77 | 78 | - Traverse the attributes of `value` 79 | - If the attribute is a _primitive_ (number, boolean, etc.) or _built-in object_ (array, date, regex, etc.), it will overwrite `state`'s attribute with the new value (adding the attribute if it doesn't exist). 80 | - If the attribute is a _custom object_ (normal JavaScript associative array), it will recurse into that object's attributes with the same logic. 81 | - If the attribute is null, it will decide what to do based on the `ignoreNull` argument. 82 | 83 | The `ignoreNull` option was added to make it easier to use the same server-side object (.NET "POCOs" in my case) to service different requests, where portions of the response object _not_ modified are set to null. This has a small data overhead over bespoke responses for each API endpoint, but encourages code reuse and consistency. 84 | 85 | _Technical implementation note:_ testing the "type" of a value can be complicated--`typeof`, `instanceof`, and other methods all have pros and cons. In this case, there was a simple answer: `Object.prototype.toString.call(value)`. This returns the string `[object Object]` for all user-defined objects (the ones we want to recurse into), and returns various other values for built-in JavaScript types like `Date`, `Array`, `RegEx`, `Number`, `String`, `Boolean`, `Math`, `Function`, `null`, and `undefined`--the keys we usually want to overwrite. 86 | 87 | ## Caveats 88 | 89 | - This only works if your `value` object forms a **directed acyclic graph**, otherwise you'll have an endless loop when updating. 90 | - This _overwrites_ arrays, it does not merge them. It only merges _objects_. 91 | 92 | ## Demo 93 | 94 | Here's a CodePen where you can play with merging an object into a sample Vuex state: 95 | https://codepen.io/richardtallent/pen/eyWKGN 96 | 97 | ## Examples 98 | 99 | Basic example: 100 | 101 | ```JavaScript 102 | var a = { foo: 1, bar: 0 } 103 | var b = { foo: 2, fizz: 4, fee: null } 104 | stateMerge(a, b) 105 | console.log(a) 106 | // { foo: 2, bar: 0, fizz: 4, fee: null } 107 | ``` 108 | 109 | With the `ignoreNull` option: 110 | 111 | ```JavaScript 112 | var a = { foo: 1, fee: { id: 1 } } 113 | var b = { foo: 2, fee: null } 114 | stateMerge(a, b, null, true) 115 | console.log(a) 116 | // { foo: 2, fee: { id: 1 } } 117 | ``` 118 | 119 | Example of a deeper merge: 120 | 121 | ```JavaScript 122 | var a = { foo: [0, 1], bar: { "1": "Marcia", "2": "Peter" } } 123 | var b = { foo: [2, 3], bar: { "1": "Jan" } } 124 | stateMerge(a, b) 125 | console.log(a) 126 | // { foo: [2, 3], bar: { "1" : "Jan", "2": "Peter" } } 127 | ``` 128 | 129 | Using the optional `propName` parameter: 130 | 131 | ```JavaScript 132 | var a = { foo: [0, 1], bar: { "1": "Marcia", "2": "Peter" } } 133 | var b = { "1": "Jan" } 134 | stateMerge(a, b, "bar") 135 | console.log(a) 136 | // { foo: [0, 1], bar: { "1" : "Jan", "2": "Peter" } } 137 | ``` 138 | 139 | ## Vuex Example (assumes you've installed it from npm, etc.) 140 | 141 | ```JavaScript 142 | import { stateMerge } from "vue-object-merge" 143 | 144 | export const store = new Vuex.Store({ 145 | ... 146 | actions: { 147 | getOrders(context)) { 148 | return HTTP.get("/orders") 149 | .then(function(response)) { 150 | context.commit("MERGE_STATE", response.data) 151 | }) 152 | } 153 | }, 154 | mutations: { 155 | MERGE_STATE(state, data) { 156 | stateMerge(state, data) 157 | } 158 | } 159 | ``` 160 | 161 | ## Release History 162 | 163 | | Date | Version | Notes | 164 | | ---------- | ------- | ---------------------------------------- | 165 | | 2018.01.01 | 0.1.0 | First release | 166 | | 2018.01.01 | 0.1.1 | Cleaned up and simplified type checking. | 167 | | 2018.01.01 | 0.1.2 | Fixed module export | 168 | | 2018.01.03 | 0.1.3 | IE11 doesn't like `for(const...)` | 169 | | 2018.01.11 | 0.1.4 | npm build doesn't like ES6 at all | 170 | | 2018.01.15 | 0.1.5 | Added ignoreNull parameter | 171 | | 2018.11.19 | 0.1.6 | Add proper ESM/CJS, update Vue dep | 172 | | 2018.12.18 | 0.1.7 | remove =>, IE11 issue (#2) | 173 | | 2020.02.18 | 0.1.8 | Fix null state prop issue (#5) | 174 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | 3 | export const stateMerge = function(state, value, propName, ignoreNull) { 4 | if ( 5 | Object.prototype.toString.call(value) === "[object Object]" && 6 | (propName == null || state.hasOwnProperty(propName)) 7 | ) { 8 | const o = propName == null ? state : state[propName]; 9 | if (o != null) { 10 | for (var prop in value) { 11 | stateMerge(o, value[prop], prop, ignoreNull); 12 | } 13 | return; 14 | } 15 | } 16 | if (!ignoreNull || value !== null) Vue.set(state, propName, value); 17 | }; 18 | 19 | export default stateMerge; 20 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line no-global-assign */ 2 | require = require('esm')(module); 3 | module.exports = require('./index.js'); -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-object-merge", 3 | "version": "0.1.8", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "esm": { 8 | "version": "3.1.0", 9 | "resolved": "https://registry.npmjs.org/esm/-/esm-3.1.0.tgz", 10 | "integrity": "sha512-r4Go7Wh7Wh0WPinRXeeM9PIajRsUdt8SAyki5R1obVc0+BwtqvtjbngVSSdXg0jCe2xZkY8hyBMx6q/uymUkPw==" 11 | }, 12 | "vue": { 13 | "version": "2.5.21", 14 | "resolved": "https://registry.npmjs.org/vue/-/vue-2.5.21.tgz", 15 | "integrity": "sha512-Aejvyyfhn0zjVeLvXd70h4hrE4zZDx1wfZqia6ekkobLmUZ+vNFQer53B4fu0EjWBSiqApxPejzkO1Znt3joxQ==" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-object-merge", 3 | "version": "0.1.8", 4 | "description": "Utility function for merging an object into a reactive object in Vue", 5 | "main": "main.js", 6 | "module": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "Richard Tallent (https://www.tallent.us)", 11 | "keywords": [ 12 | "vue", 13 | "vuejs", 14 | "vuex", 15 | "merge", 16 | "deep assign" 17 | ], 18 | "repository": "https://github.com/richardtallent/vue-object-merge", 19 | "license": "MIT", 20 | "dependencies": { 21 | "esm": "^3.0.84", 22 | "vue": "^2.5.21" 23 | }, 24 | "prettier": { 25 | "useTabs": true, 26 | "semi": true, 27 | "singleQuote": false, 28 | "bracketSpacing": true, 29 | "trailingComma": "es5", 30 | "printWidth": 80 31 | }, 32 | "stylelint": { 33 | "extends": "stylelint-config-standard", 34 | "rules": { 35 | "indentation": "tab" 36 | } 37 | } 38 | } 39 | --------------------------------------------------------------------------------