├── package.json ├── LICENSE ├── .gitignore ├── yarn.lock ├── README.md └── src └── VuexRailsPlugin.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuex-rails-plugin", 3 | "version": "1.0.3", 4 | "description": "A Vuex plugin that easily maps Rails resources to Vuex modules", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/vueonrails/vuex-rails-plugin.git" 12 | }, 13 | "keywords": [ 14 | "Vue", 15 | "Vuex", 16 | "Rails" 17 | ], 18 | "author": "Richard LaFranchi ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/vueonrails/vuex-rails-plugin/issues" 22 | }, 23 | "homepage": "https://github.com/vueonrails/vuex-rails-plugin#readme", 24 | "dependencies": { 25 | "axios": "^0.18.0", 26 | "vue": "^2.5.16", 27 | "vuex": "^3.0.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Richard LaFranchi 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | .idea 63 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | axios@^0.18.0: 6 | version "0.18.0" 7 | resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102" 8 | dependencies: 9 | follow-redirects "^1.3.0" 10 | is-buffer "^1.1.5" 11 | 12 | debug@^3.1.0: 13 | version "3.1.0" 14 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 15 | dependencies: 16 | ms "2.0.0" 17 | 18 | follow-redirects@^1.3.0: 19 | version "1.5.0" 20 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.0.tgz#234f49cf770b7f35b40e790f636ceba0c3a0ab77" 21 | dependencies: 22 | debug "^3.1.0" 23 | 24 | is-buffer@^1.1.5: 25 | version "1.1.6" 26 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" 27 | 28 | ms@2.0.0: 29 | version "2.0.0" 30 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 31 | 32 | vue@^2.5.16: 33 | version "2.5.16" 34 | resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.16.tgz#07edb75e8412aaeed871ebafa99f4672584a0085" 35 | 36 | vuex@^3.0.1: 37 | version "3.0.1" 38 | resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2" 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VuexRailsPlugin 2 | 3 | ### A Vuex plugin that easily maps Rails resources to Vuex modules 4 | 5 | The idea of this plugin is to easily incorporate conventional rest endpoints defined when scaffolding Rails resources and map the responses to appropriate state in Vuex. ex. `resources :posts` generates the following endpoints: 6 | 7 | * GET|POST /posts 8 | * GET|PUT /posts/:id 9 | * DELETE /posts/:id 10 | 11 | ### Install 12 | 13 | ```bash 14 | $ npm i vuex-rails-plugin 15 | ``` 16 | 17 | ### Usage 18 | 19 | The plugin can be imported into any Vuex store like so: 20 | 21 | ```js 22 | // store.js 23 | import Vuex from 'vuex' 24 | import Vue from 'vue' 25 | Vue.use(Vuex) 26 | import VuexRailsPlugin from 'vuex-rails-plugin/src/VuexRailsPlugin' 27 | 28 | export default new Vuex.Store({ 29 | // ... 30 | plugins: [ 31 | VuexRailsPlugin('posts'), 32 | VuexRailsPlugin('categories') 33 | ] 34 | }) 35 | ``` 36 | 37 | This example defines Vuex modules for 'posts' and 'categories' namespaces. 38 | It supports the following state: 39 | 40 | * all - items returned from the index action 41 | * current - set when the show action is called 42 | * error - set if any errors occur such as validations 43 | 44 | The state can be mapped to any Vue component using `mapState` 45 | 46 | ```js 47 | // some-component.vue 48 | import { mapState } from 'vuex' 49 | export default { 50 | computed: { 51 | ...mapState('posts', { 52 | posts: state => state.all, 53 | currentPost: state => state.current 54 | error: state => state.error 55 | }) 56 | } 57 | } 58 | 59 | ``` 60 | 61 | 62 | Actions can also be called by using `mapActions` 63 | 64 | ```js 65 | // some-component.vue 66 | import { mapActions } from 'vuex' 67 | export default { 68 | // ... 69 | methods: { 70 | ...mapActions('posts', { 71 | getPosts: 'getAll', // ex. this.getAll() 72 | getPost: 'get', // ex. this.getPost(id) 73 | createPost: 'create', // ex. this.createPost({title: 'Bad', body: 'Ass'}) 74 | updatePost: 'update', // ex. this.updatePost({id: 1, title: 'Bad', body: 'Mama Jama'}) 75 | destroyPost: 'destroy' // ex. this.destroyPost({}) 76 | }) 77 | } 78 | } 79 | ``` 80 | 81 | Params are supported 82 | 83 | ```js 84 | this.getPosts({page: 1, limit: 10}) 85 | ``` 86 | 87 | Use directly in a form 88 | 89 | ```js 90 | export default { 91 | data() { 92 | return { 93 | newPost: { 94 | title: '', 95 | body: '' 96 | } 97 | } 98 | } 99 | } 100 | ``` 101 | 102 | ```html 103 |
104 | 105 | 106 | 107 | 108 | Submit 109 |
110 | ``` 111 | 112 | Promises supported 113 | 114 | ```js 115 | export default { 116 | // ... 117 | methods: { 118 | promiseMeThisPostWillCreate() { 119 | this.createPost(this.newPost) 120 | .then(createdPost => { 121 | alert('Thanks for keeping your promise!') 122 | }) 123 | .catch(err => { 124 | alert('Why did you break my promise?') 125 | }) 126 | } 127 | } 128 | } 129 | ``` 130 | 131 | ### TODO 132 | 133 | * Support nested resources ex. `/categories/:category_id/posts` 134 | * Support other options that may be necessary to support custom getters, filters, etc. 135 | -------------------------------------------------------------------------------- /src/VuexRailsPlugin.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const http = axios.create({ 4 | headers: { 5 | 'Content-Type': 'application/json' 6 | } 7 | }) 8 | 9 | http.interceptors.request.use(function (config) { 10 | config.headers['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content') 11 | return config 12 | }, function (error) { 13 | return Promise.reject(error.response) 14 | }) 15 | 16 | const ALL = 'ALL' 17 | const GET = 'GET' 18 | const CREATE = 'CREATE' 19 | const UPDATE = 'UPDATE' 20 | const DESTROY = 'DESTROY' 21 | const ERROR = 'ERROR' 22 | 23 | const updateItem = (item, all, remove) => { 24 | remove = remove || false 25 | const thisItem = all.find(i => i.id === item.id) 26 | const thisItemIndex = all.indexOf(thisItem) 27 | if (thisItemIndex > -1) { 28 | if (remove) { 29 | all.splice(thisItemIndex, 1) 30 | } else { 31 | all.splice(thisItemIndex, 1, item) 32 | } 33 | } 34 | } 35 | 36 | const resourcePath = (resource, params) => { 37 | if (params.id) { 38 | return `/${resource}/${params.id}.json` 39 | } else { 40 | return `/${resource}.json` 41 | } 42 | } 43 | 44 | export default function VuexRailsPlugin(resource) { 45 | return store => { 46 | store.registerModule(resource, { 47 | namespaced: true, 48 | state: { 49 | all: [], 50 | current: {}, 51 | error: null 52 | }, 53 | actions: { 54 | getAll({ commit }, params) { 55 | params = params || {} 56 | return new Promise((resolve, reject) => { 57 | http.get(`/${resource}.json`, { params }) 58 | .then(res => { 59 | const items = res.data 60 | commit(ALL, { items }) 61 | resolve(res) 62 | }) 63 | .catch(err => { 64 | commit(ERROR, { err }) 65 | reject(err) 66 | }) 67 | }) 68 | }, 69 | get({ commit }, id, params) { 70 | params = params || {} 71 | return new Promise((resolve, reject) => { 72 | http.get(resourcePath(resource, { id: id }), { params }) 73 | .then(res => { 74 | const item = res.data 75 | commit(GET, { item }) 76 | resolve(res) 77 | }) 78 | .catch(err => { 79 | commit(ERROR, { err }) 80 | reject(err) 81 | }) 82 | }) 83 | }, 84 | create({ commit }, item) { 85 | return new Promise((resolve, reject) => { 86 | http.post(`/${resource}.json`, item) 87 | .then(res => { 88 | const item = res.data 89 | commit(CREATE, { item }) 90 | resolve(res) 91 | }) 92 | .catch(err => { 93 | commit(ERROR, { err }) 94 | reject(err) 95 | }) 96 | }) 97 | }, 98 | update({ commit }, item) { 99 | return new Promise((resolve, reject) => { 100 | http.put(resourcePath(resource, item), item) 101 | .then(res => { 102 | const item = res.data 103 | commit(UPDATE, { item }) 104 | resolve(res) 105 | }) 106 | .catch(err => { 107 | commit(ERROR, { err }) 108 | reject(err) 109 | }) 110 | }) 111 | }, 112 | destroy({ commit }, item) { 113 | return new Promise((resolve, reject) => { 114 | http.delete(resourcePath(resource, item)) 115 | .then(res => { 116 | commit(DESTROY, { item }) 117 | resolve(res) 118 | }) 119 | .catch(err => { 120 | commit(ERROR, { err }) 121 | reject(err) 122 | }) 123 | }) 124 | } 125 | }, 126 | mutations: { 127 | [ALL] (state, { items }) { 128 | state.error = null 129 | state.all = items 130 | }, 131 | [GET] (state, { item }) { 132 | state.error = null 133 | state.current = item 134 | updateItem(state.current, state.all) 135 | }, 136 | [CREATE] (state, { item }) { 137 | state.error = null 138 | state.current = item 139 | if (state.all.find(i => i.id === item.id) !== undefined) { 140 | updateItem(item, state.all) 141 | } else { 142 | state.all.push(item) 143 | } 144 | }, 145 | [UPDATE] (state, { item }) { 146 | state.error = null 147 | state.current = item 148 | updateItem(item, state.all) 149 | }, 150 | [DESTROY] (state, { item }) { 151 | state.error = null 152 | state.current = null 153 | updateItem(item, state.all, true) 154 | }, 155 | [ERROR] (state, { err }) { 156 | state.error = err.response && err.response.data ? err.response.data : err 157 | } 158 | } 159 | }) 160 | } 161 | } --------------------------------------------------------------------------------