├── static
└── .gitkeep
├── .eslintignore
├── config
├── prod.env.js
├── test.env.js
├── dev.env.js
└── index.js
├── .travis.yml
├── .gitignore
├── src
├── plugin
│ ├── actions.js
│ ├── index.js
│ ├── helpers
│ │ ├── appendStateWithResponse.js
│ │ ├── __snapshots__
│ │ │ └── appendStateWithResponse.spec.js.snap
│ │ └── appendStateWithResponse.spec.js
│ ├── api-handler-helper.js
│ ├── mixin.js
│ ├── __snapshots__
│ │ ├── api-handler-component.spec.js.snap
│ │ └── module.spec.js.snap
│ ├── utils.js
│ ├── api-handler-component.spec.js
│ ├── api-handler-component.js
│ ├── vuex-api-hoc.js
│ ├── module.spec.js
│ └── module.js
├── store.js
├── child.vue
├── main.js
└── App.vue
├── .npmignore
├── .editorconfig
├── .babelrc
├── index.html
├── .eslintrc.js
├── package.json
└── README.md
/static/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | config/*.js
3 |
--------------------------------------------------------------------------------
/config/prod.env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | NODE_ENV: '"production2"'
3 | }
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "7"
4 | notifications:
5 | email: false
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log
5 | test/unit/coverage
6 | test/e2e/reports
7 | selenium-debug.log
8 | .idea
9 | lib/
10 | docs/
11 |
--------------------------------------------------------------------------------
/config/test.env.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var devEnv = require('./dev.env')
3 |
4 | module.exports = merge(devEnv, {
5 | NODE_ENV: '"testing"'
6 | })
7 |
--------------------------------------------------------------------------------
/config/dev.env.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var prodEnv = require('./prod.env')
3 |
4 | module.exports = merge(prodEnv, {
5 | NODE_ENV: '"development"'
6 | })
7 |
--------------------------------------------------------------------------------
/src/plugin/actions.js:
--------------------------------------------------------------------------------
1 | export default {
2 | request: 'Request_Api_Call',
3 | success: 'Success_Api_Call',
4 | clear: 'Clear_Api_Call',
5 | error: 'Error_Api_Call'
6 | }
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log
5 | test/unit/coverage
6 | test/e2e/reports
7 | selenium-debug.log
8 | .idea
9 | config
10 | build
11 | index.html
12 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import vuexApi from './plugin'
4 | Vue.use(Vuex)
5 |
6 | const debug = process.env.NODE_ENV !== 'production'
7 |
8 | export default new Vuex.Store({
9 | modules: {
10 | vuexApi
11 | },
12 | strict: debug
13 | })
14 |
--------------------------------------------------------------------------------
/src/child.vue:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by vouill on 7/1/18.
3 | */
4 |
5 |
6 |
7 | Component child vuexApiData: {{ vuexApiData }}
8 |
9 |
10 |
11 |
19 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["es2015", { "modules": false }],
4 | "stage-2"
5 | ],
6 | "comments": false,
7 | "env": {
8 | "test": {
9 | "presets": ["es2015", "stage-2"],
10 | "plugins": [
11 | ["module-resolver", {
12 | "root": ["./src"],
13 | "alias": {
14 | "src": "./src"
15 | }
16 | }]]
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/plugin/index.js:
--------------------------------------------------------------------------------
1 | import { default as module } from './module'
2 | export { default as actions } from './actions'
3 | export { ApiHandlerComponent } from './api-handler-component'
4 | export { getApiResp, getApiState, getApiStatus, getApiData } from './api-handler-helper'
5 | export { default as hoc } from './vuex-api-hoc'
6 | export { vuexApiCallMixin, vuexApiGetStateMixin } from './mixin'
7 | export default module
8 |
--------------------------------------------------------------------------------
/src/plugin/helpers/appendStateWithResponse.js:
--------------------------------------------------------------------------------
1 | import set from 'lodash/fp/set'
2 |
3 | export default (state, keyPath, responseStateObject) => {
4 | if(!keyPath){
5 | throw new Error('keyPath need to be defined')
6 | }
7 | if(typeof keyPath === 'string'){
8 | return { stateObject: responseStateObject , head: keyPath }
9 |
10 | }
11 | const [head, ...tail] = keyPath;
12 | return { stateObject: tail.length > 0 ? set(tail, responseStateObject, state[head]): responseStateObject , head }
13 | }
14 |
--------------------------------------------------------------------------------
/src/plugin/api-handler-helper.js:
--------------------------------------------------------------------------------
1 | import get from 'lodash/get'
2 |
3 | export const getApiState = (keyPath, path = [], defaultValue) => state => get(state, ['vuexApi', keyPath, ...path], defaultValue)
4 | export const getApiResp = (keyPath, path = [], defaultValue) => state => get(state, ['vuexApi', keyPath, 'resp', ...path], defaultValue)
5 | export const getApiData = (keyPath, path = [], defaultValue) => state => get(state, ['vuexApi', keyPath, 'resp', 'data', ...path], defaultValue)
6 | export const getApiStatus = (keyPath, path = [], defaultValue) => state => get(state, ['vuexApi', keyPath, 'status', ...path], defaultValue)
7 |
--------------------------------------------------------------------------------
/src/plugin/mixin.js:
--------------------------------------------------------------------------------
1 | import get from 'lodash/get'
2 | import { mapState } from 'vuex'
3 | import pluginActions from './actions'
4 |
5 | export const vuexApiCallMixin = {
6 | methods: {
7 | vuexApiCall: function (obj) {
8 | return this.$store.dispatch(pluginActions.request, obj)
9 | },
10 | vuexApiClear: function (keyPath) {
11 | return this.$store.dispatch(pluginActions.request, keyPath)
12 | }
13 | }
14 | }
15 |
16 | export const vuexApiGetStateMixin = (keyPath, attrName) => ({
17 | computed: mapState({
18 | [attrName || 'apiData']: state => get(state.vuexApi,keyPath),
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | // The Vue build version to load with the `import` command
2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
3 | import Vue from 'vue'
4 | import App from './App'
5 | import store from './store'
6 | import TreeView from 'vue-json-tree-view'
7 | import { ApiHandlerComponent } from './plugin'
8 | Vue.use(TreeView)
9 | Vue.component('github-api', ApiHandlerComponent({ requestConfig: { baseURL: 'https://api.github.com' } }))
10 | Vue.component('json-api', ApiHandlerComponent({ requestConfig: { baseURL: 'https://jsonplaceholder.typicode.com' } }))
11 | /* eslint-disable no-new */
12 | new Vue({
13 | el: '#app',
14 | store,
15 | components: { App },
16 | template: '',
17 | })
18 |
--------------------------------------------------------------------------------
/src/plugin/__snapshots__/api-handler-component.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Api handler call the correct hooks when mounting 1`] = `
4 | Array [
5 | Array [
6 | "Request_Api_Call",
7 | Object {
8 | "keyPath": "totally-a-keypath",
9 | "params": undefined,
10 | "url": "some/url",
11 | },
12 | ],
13 | ]
14 | `;
15 |
16 | exports[`Api handler should be called after destroy 1`] = `
17 | Array [
18 | Array [
19 | "Request_Api_Call",
20 | Object {
21 | "keyPath": "totally-a-keypath",
22 | "params": undefined,
23 | "requestConfig": Object {
24 | "baseURL": "https://api.github.com",
25 | },
26 | "url": "some/url",
27 | },
28 | ],
29 | Array [
30 | "Clear_Api_Call",
31 | "totally-a-keypath",
32 | ],
33 | ]
34 | `;
35 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | vuex-api
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/plugin/utils.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-self-compare */
2 | 'use strict'
3 |
4 | var hasOwnProperty = Object.prototype.hasOwnProperty
5 |
6 | function is (x, y) {
7 | if (x === y) {
8 | return x !== 0 || 1 / x === 1 / y
9 | } else {
10 | return x !== x && y !== y
11 | }
12 | }
13 |
14 | export default function shallowEqual (objA, objB) {
15 | if (is(objA, objB)) {
16 | return true
17 | }
18 | if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
19 | return false
20 | }
21 | var keysA = Object.keys(objA)
22 | var keysB = Object.keys(objB)
23 | if (keysA.length !== keysB.length) {
24 | return false
25 | }
26 | for (var i = 0; i < keysA.length; i++) {
27 | if (!hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {
28 | return false
29 | }
30 | }
31 | return true
32 | }
33 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // http://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | root: true,
5 | parserOptions: {
6 | parser: "babel-eslint",
7 | ecmaVersion: 2017,
8 | sourceType: "module"
9 | },
10 | env: {
11 | browser: true,
12 | jest: true,
13 | },
14 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
15 | "extends": ["standard", "eslint:recommended", "plugin:vue/recommended"],
16 | // required to lint *.vue files
17 | plugins: [
18 | "vue"
19 | ],
20 | // add your custom rules here
21 | 'rules': {
22 | "vue/max-attributes-per-line": 0,
23 | "object-curly-spacing": [2,"always"],
24 | // allow paren-less arrow functions
25 | 'arrow-parens': 0,
26 | // allow async-await
27 | 'generator-star-spacing': 0,
28 | // allow debugger during development
29 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/plugin/helpers/__snapshots__/appendStateWithResponse.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Api handler should mutate correctly already existing key on keyPath with more than one item 1`] = `
4 | Object {
5 | "foo": Object {
6 | "bar": Object {
7 | "baz": Object {
8 | "data": Object {
9 | "toto": "tutu",
10 | },
11 | "status": "loading",
12 | },
13 | },
14 | "data": "bax",
15 | "status": "success",
16 | },
17 | }
18 | `;
19 |
20 | exports[`Api handler should mutate correctly already existing key on keyPath with one item 1`] = `
21 | Object {
22 | "foo": Object {
23 | "data": Object {
24 | "toto": "tutu",
25 | },
26 | "status": "loading",
27 | },
28 | }
29 | `;
30 |
31 | exports[`Api handler should mutate correctly on keyPath with more than one item 1`] = `
32 | Object {
33 | "foo": Object {
34 | "bar": Object {
35 | "baz": Object {
36 | "data": Object {
37 | "toto": "tutu",
38 | },
39 | "status": "loading",
40 | },
41 | },
42 | },
43 | }
44 | `;
45 |
46 | exports[`Api handler should mutate correctly on keyPath with one item 1`] = `
47 | Object {
48 | "foo": Object {
49 | "data": Object {
50 | "toto": "tutu",
51 | },
52 | "status": "loading",
53 | },
54 | }
55 | `;
56 |
--------------------------------------------------------------------------------
/src/plugin/api-handler-component.spec.js:
--------------------------------------------------------------------------------
1 | import { mount } from 'vue-test-utils'
2 | import ApiHandlerComponent from './api-handler-component'
3 |
4 | describe('Api handler', () => {
5 | it('call the correct hooks when mounting', () => {
6 | const dispatch = jest.fn()
7 | mount(ApiHandlerComponent(), { mocks: { $store: { dispatch } }, propsData: { url: 'some/url', keyPath: 'totally-a-keypath' } })
8 | expect(dispatch.mock.calls).toMatchSnapshot()
9 | })
10 |
11 | it('should be called after destroy', () => {
12 | const dispatch = jest.fn()
13 | const wrapper = mount(ApiHandlerComponent({ requestConfig: { baseURL: 'https://api.github.com' } }), { mocks: { $store: { dispatch } }, propsData: { persistent: false, url: 'some/url', keyPath: 'totally-a-keypath' } })
14 | wrapper.vm.$destroy()
15 | expect(dispatch.mock.calls).toMatchSnapshot()
16 | })
17 |
18 | // it('should call the api after arg change', () => {
19 | // const dispatch = jest.fn()
20 | // const wrapper = mount(ApiHandlerComponent({ requestConfig: { baseURL: 'https://api.github.com' } }), { mocks: { $store: { dispatch } }, propsData: { persistent: false, url: 'some/url', keyPath: 'totally-a-keypath', args: { post: 1 } } })
21 | // wrapper.setProps({ args: { post: 2 } })
22 | // wrapper.setProps({ args: { post: 3 } })
23 | // expect(dispatch.mock.calls).toMatchSnapshot()
24 | // })
25 | })
26 |
--------------------------------------------------------------------------------
/src/plugin/api-handler-component.js:
--------------------------------------------------------------------------------
1 | import pluginActions from './actions'
2 | import isShallowEqual from './utils';
3 |
4 | export const ApiHandlerComponent = (initArgs) => ({
5 | name: 'api-handled',
6 | props: {
7 | keyPath: { type: String, required: true },
8 | url: { type: String, required: true },
9 | period: Number,
10 | params: Object,
11 | persistent: { type: Boolean, default: true }
12 | },
13 | methods: {
14 | apiRequest: function () {
15 | const { keyPath, url, params } = this
16 | this.$store.dispatch(pluginActions.request, { keyPath, url, params, ...initArgs })
17 | }
18 | },
19 | watch: {
20 | params: {
21 | handler: function (newParams, oldParams) {
22 | if(!isShallowEqual(newParams, oldParams)){
23 | this.apiRequest()
24 | }
25 | },
26 | deep: true
27 | },
28 | url: function () {
29 | this.apiRequest()
30 | }
31 | },
32 | created: function () {
33 | if (this.period) {
34 | this.intervalId = setInterval(this.apiRequest, this.period)
35 | }
36 | this.apiRequest()
37 |
38 | },
39 | destroyed: function () {
40 | if (!this.persistent) {
41 | this.$store.dispatch(pluginActions.clear, this.keyPath)
42 | }
43 | if (this.intervalId) {
44 | clearInterval(this.intervalId)
45 | }
46 | },
47 | render () {
48 | return null
49 | }
50 | })
51 |
52 | export default ApiHandlerComponent
53 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | // see http://vuejs-templates.github.io/webpack for documentation.
2 | var path = require('path')
3 |
4 | module.exports = {
5 | build: {
6 | env: require('./prod.env'),
7 | index: path.resolve(__dirname, '../docs/index.html'),
8 | assetsRoot: path.resolve(__dirname, '../docs'),
9 | assetsSubDirectory: 'static',
10 | assetsPublicPath: './',
11 | productionSourceMap: true,
12 | // Gzip off by default as many popular static hosts such as
13 | // Surge or Netlify already gzip all static assets for you.
14 | // Before setting to `true`, make sure to:
15 | // npm install --save-dev compression-webpack-plugin
16 | productionGzip: false,
17 | productionGzipExtensions: ['js', 'css'],
18 | // Run the build command with an extra argument to
19 | // View the bundle analyzer report after build finishes:
20 | // `npm run build --report`
21 | // Set to `true` or `false` to always turn it on or off
22 | bundleAnalyzerReport: process.env.npm_config_report
23 | },
24 | dev: {
25 | env: require('./dev.env'),
26 | port: 8080,
27 | autoOpenBrowser: true,
28 | assetsSubDirectory: 'static',
29 | assetsPublicPath: '/',
30 | proxyTable: {},
31 | // CSS Sourcemaps off by default because relative paths are "buggy"
32 | // with this option, according to the CSS-Loader README
33 | // (https://github.com/webpack/css-loader#sourcemaps)
34 | // In our experience, they generally work as expected,
35 | // just be aware of this issue when enabling this option.
36 | cssSourceMap: false
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/plugin/vuex-api-hoc.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import get from 'lodash/get'
3 |
4 | export const ApiHandlerComponent = {
5 | name: 'vuex-helper',
6 | props: {
7 | keyPath: { type: [String, Array], required: true },
8 | loadingComponent: Object,
9 | },
10 | computed: {
11 | storeObject () {
12 | return get(this.$store.state.vuexApi, this.keyPath)
13 | },
14 | },
15 | render (h) {
16 | if(!this.storeObject){
17 | return null
18 | }
19 | if(this.storeObject.status === 'loading' && this.$slots.loading){
20 | if(this.$slots.loading[0].componentOptions){
21 | this.$slots.loading[0].componentOptions.propsData.vuexApiStoreObject = this.storeObject
22 | }
23 | return h('div', this.$slots.loading)
24 | }
25 | if(!this.storeObject.firstCallDone && this.$slots.firstCallDone){
26 | if(this.$slots.firstCallDone[0].componentOptions){
27 | this.$slots.firstCallDone[0].componentOptions.propsData.vuexApiStoreObject = this.storeObject
28 | }
29 | return h('div', this.$slots.firstCallDone)
30 | }
31 | if(this.storeObject.status === 'success' && this.$slots.success){
32 | if(this.$slots.success[0].componentOptions){
33 | this.$slots.success[0].componentOptions.propsData.vuexApiStoreObject = this.storeObject
34 | this.$slots.success[0].componentOptions.propsData.vuexApiData = this.storeObject.resp.data
35 | }
36 | return h('div', this.$slots.success)
37 | }
38 | if(this.storeObject.status === 'error' && this.$slots.error){
39 | if(this.$slots.error[0].componentOptions){
40 | this.$slots.error[0].componentOptions.propsData.vuexApiStoreObject = this.storeObject
41 | this.$slots.error[0].componentOptions.propsData.vuexApiError = this.storeObject.resp.err
42 | }
43 | return h('div', this.$slots.error)
44 | }
45 | return null
46 | }
47 | }
48 |
49 | export default ApiHandlerComponent
50 |
--------------------------------------------------------------------------------
/src/plugin/helpers/appendStateWithResponse.spec.js:
--------------------------------------------------------------------------------
1 | import mutateStateWithResponse from './appendStateWithResponse'
2 |
3 | const set = (state, key, obj) => {
4 | state[key] = obj;
5 | return state
6 | }
7 |
8 |
9 |
10 | describe('Api handler', () => {
11 | it('should throw error when no keyPath', () => {
12 | const state = {};
13 | const resp = { status: 'loading', data: { toto: 'tutu' } }
14 | expect(() => mutateStateWithResponse(state, undefined, resp)).toThrow()
15 | })
16 |
17 | it('should mutate correctly on keyPath with one item', () => {
18 | const state = {};
19 | const resp = { status: 'loading', data: { toto: 'tutu' } }
20 | const keyPath = ['foo']
21 | const { stateObject, head } = mutateStateWithResponse(state, keyPath, resp)
22 | expect(set(state, head, stateObject )).toMatchSnapshot()
23 | })
24 |
25 | it('should mutate correctly on keyPath with more than one item', () => {
26 | const state = {};
27 | const resp = { status: 'loading', data: { toto: 'tutu' } }
28 | const keyPath = ['foo', 'bar', 'baz'];
29 | const { stateObject, head } = mutateStateWithResponse(state, keyPath, resp)
30 | expect(set(state, head, stateObject )).toMatchSnapshot()
31 | })
32 |
33 | it('should mutate correctly already existing key on keyPath with one item', () => {
34 | const state = { foo: { status: 'success', data: 'bax' } };
35 | const resp = { status: 'loading', data: { toto: 'tutu' } }
36 | const keyPath = ['foo']
37 | const { stateObject, head } = mutateStateWithResponse(state, keyPath, resp)
38 | expect(set(state, head, stateObject )).toMatchSnapshot()
39 | })
40 |
41 | it('should mutate correctly already existing key on keyPath with more than one item', () => {
42 | const state = { foo: { status: 'success', data: 'bax' } };
43 | const resp = { status: 'loading', data: { toto: 'tutu' } }
44 | const keyPath = ['foo', 'bar', 'baz'];
45 | const { stateObject, head } = mutateStateWithResponse(state, keyPath, resp)
46 | expect(set(state, head, stateObject )).toMatchSnapshot()
47 | })
48 | })
49 |
--------------------------------------------------------------------------------
/src/plugin/module.spec.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by vouill on 10/24/17.
3 | */
4 | import module from './module'
5 | import pluginActions from './actions'
6 | import moxios from 'moxios'
7 |
8 | const testRequest = { url: 'testUrl', method: 'testMethod', baseURL: 'baseUrl' }
9 |
10 | describe('Module Actions', () => {
11 | beforeEach(function () {
12 | moxios.install()
13 | })
14 |
15 | afterEach(function () {
16 | moxios.uninstall()
17 | })
18 | it('should process request', () => {
19 | const commit = jest.fn()
20 | module.actions[pluginActions.request]({ commit }, testRequest)
21 | expect(commit.mock.calls).toMatchSnapshot()
22 | })
23 |
24 | it('should process clear', () => {
25 | const commit = jest.fn()
26 | module.actions[pluginActions.clear]({ commit }, { keyPath: 'someKeypath' })
27 | expect(commit.mock.calls).toMatchSnapshot()
28 | })
29 |
30 | it('should process onSuccess', () => {
31 | moxios.wait(() => {
32 | const request = moxios.requests.mostRecent()
33 | request.respondWith({
34 | status: 200,
35 | response: 'test'
36 | })
37 | })
38 |
39 | const commit = jest.fn()
40 | const dispatch = jest.fn()
41 | return module.actions[pluginActions.request]({ commit, dispatch }, { keyPath: 'someKeypath', url: 'testUrl', onSuccess: { dispatchAction: { type: 'test-type', payload: 'test-payload' } } }).then(() => {
42 | expect(commit.mock.calls).toMatchSnapshot()
43 | expect(dispatch.mock.calls).toMatchSnapshot()
44 | })
45 | })
46 | })
47 |
48 | describe('Module mutations', () => {
49 | let state = {}
50 | const keyPath = 'someKeyPath'
51 | beforeEach(() => {
52 | state = {}
53 | })
54 | it('should process request', () => {
55 | module.mutations[pluginActions.request](state, { keyPath })
56 | expect(state).toMatchSnapshot()
57 | })
58 |
59 | it('should process success', () => {
60 | module.mutations[pluginActions.success](state, { keyPath, resp: { date: 'fakeData' } })
61 | expect(state).toMatchSnapshot()
62 | })
63 |
64 | it('should process error', () => {
65 | module.mutations[pluginActions.error](state, { keyPath, err: { data: 'fakeError' } })
66 | expect(state).toMatchSnapshot()
67 | })
68 |
69 | it('should process clear', () => {
70 | module.mutations[pluginActions.clear](state, keyPath)
71 | expect(state).toMatchSnapshot()
72 | })
73 |
74 | it('should process request -> success', () => {
75 | module.mutations[pluginActions.clear](state, keyPath)
76 | module.mutations[pluginActions.success](state, { keyPath, resp: { date: 'fakeData' } })
77 | expect(state).toMatchSnapshot()
78 | })
79 |
80 | it('should process request -> error', () => {
81 | module.mutations[pluginActions.clear](state, keyPath)
82 | expect(state).toMatchSnapshot()
83 | module.mutations[pluginActions.error](state, { keyPath, err: { data: 'fakeError' } })
84 | expect(state).toMatchSnapshot()
85 | })
86 | })
87 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
error
6 |
7 |
8 |
9 |
10 |
json api w/ param
11 |
12 |
13 |
14 |
15 |
16 |
json api Post
17 |
18 |
19 |
20 |
21 |
Using helper
22 |
23 |
24 | loading
25 | error
26 |
27 |
28 |
29 |
30 |
35 |
96 |
97 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vuex-api",
3 | "version": "0.2.9",
4 | "description": "Effortlessly handle api calls with vuex without repeating yourself.",
5 | "author": "Vouill ",
6 | "main": "./lib/vuex-api.js",
7 | "bugs": "https://github.com/vouill/vuex-api/issues",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/vouill/vuex-api"
11 | },
12 | "scripts": {
13 | "dev": "node build/dev-server.js",
14 | "build:docs": "node build/build.js",
15 | "build:lib": "webpack --bail --progress --hide-modules --config build/webpack.lib.conf.js",
16 | "test": "npm run lint; jest src",
17 | "test-watch": "jest --watch",
18 | "lint": "eslint --ext .js,.vue src"
19 | },
20 | "dependencies": {
21 | "axios": "^0.16.2",
22 | "vue": "^2.4.2"
23 | },
24 | "devDependencies": {
25 | "autoprefixer": "^6.7.2",
26 | "babel-core": "^6.22.1",
27 | "babel-eslint": "^7.1.1",
28 | "babel-jest": "^21.0.0",
29 | "babel-loader": "^6.2.10",
30 | "babel-plugin-istanbul": "^3.1.2",
31 | "babel-plugin-module-resolver": "^2.7.1",
32 | "babel-plugin-transform-runtime": "^6.22.0",
33 | "babel-preset-es2015": "^6.22.0",
34 | "babel-preset-stage-2": "^6.22.0",
35 | "babel-register": "^6.22.0",
36 | "connect-history-api-fallback": "^1.3.0",
37 | "cross-env": "^3.1.4",
38 | "cross-spawn": "^5.0.1",
39 | "css-loader": "^0.26.1",
40 | "eslint": "^3.14.1",
41 | "eslint-config-standard": "^6.2.1",
42 | "eslint-friendly-formatter": "^2.0.7",
43 | "eslint-loader": "^1.6.1",
44 | "eslint-plugin-html": "^2.0.0",
45 | "eslint-plugin-promise": "^3.4.0",
46 | "eslint-plugin-standard": "^2.0.1",
47 | "eslint-plugin-vue": "^4.0.0-beta.2",
48 | "eventsource-polyfill": "^0.9.6",
49 | "express": "^4.14.1",
50 | "extract-text-webpack-plugin": "^2.0.0-rc.2",
51 | "file-loader": "^0.10.0",
52 | "friendly-errors-webpack-plugin": "^1.1.3",
53 | "function-bind": "^1.1.0",
54 | "html-webpack-plugin": "^2.28.0",
55 | "http-proxy-middleware": "^0.17.3",
56 | "inject-loader": "^2.0.1",
57 | "jest": "^21.0.1",
58 | "jest-serializer-vue": "^0.2.0",
59 | "jest-vue": "^0.5.2",
60 | "lodash": "^4.17.4",
61 | "lodash-es": "^4.17.4",
62 | "lolex": "^1.5.2",
63 | "moxios": "^0.4.0",
64 | "opn": "^4.0.2",
65 | "ora": "^1.1.0",
66 | "semver": "^5.3.0",
67 | "shelljs": "^0.7.6",
68 | "url-loader": "^0.5.7",
69 | "vue": "^2.4.2",
70 | "vue-json-tree-view": "^2.1.1",
71 | "vue-loader": "^11.3.4",
72 | "vue-router": "^2.7.0",
73 | "vue-style-loader": "^3.0.1",
74 | "vue-template-compiler": "^2.4.2",
75 | "vue-test-utils": "^1.0.0-beta.2",
76 | "vuex": "^3.0.0",
77 | "webpack": "^2.4.1",
78 | "webpack-bundle-analyzer": "^2.2.1",
79 | "webpack-dev-middleware": "^1.10.0",
80 | "webpack-hot-middleware": "^2.16.1",
81 | "webpack-merge": "^2.6.1"
82 | },
83 | "engines": {
84 | "node": ">= 4.0.0",
85 | "npm": ">= 3.0.0"
86 | },
87 | "jest": {
88 | "moduleFileExtensions": [
89 | "js",
90 | "vue"
91 | ],
92 | "transform": {
93 | "^.+\\.js$": "/node_modules/babel-jest",
94 | ".*\\.(vue)$": "/node_modules/jest-vue"
95 | },
96 | "snapshotSerializers": [
97 | "/node_modules/jest-serializer-vue"
98 | ]
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/plugin/module.js:
--------------------------------------------------------------------------------
1 | import appendStateWithResponse from './helpers/appendStateWithResponse';
2 | import pluginActions from './actions'
3 | import vue from 'vue'
4 | import axios from 'axios'
5 | import shallowEqual from './utils'
6 |
7 | const { CancelToken } = axios
8 |
9 | const state = {}
10 |
11 | // used for request cancelation
12 | const previousStateRequest = { cancel: null, url: null, params: {}, state: 'finished' }
13 |
14 | const actions = {
15 | [pluginActions.request]: ({ commit, dispatch }, { requestConfig, ...otherConfig }) => {
16 | const { url, method, onSuccess, onError, keyPath, params } = otherConfig
17 | return new Promise((resolve, reject) => {
18 | commit(pluginActions.request, { keyPath })
19 | if (url === previousStateRequest.url && previousStateRequest.state === 'started' && shallowEqual(params, previousStateRequest.params)) {
20 | previousStateRequest.cancel()
21 | }
22 | previousStateRequest.url = url
23 | previousStateRequest.state = 'started'
24 | previousStateRequest.params = params
25 | axios({
26 | ...requestConfig,
27 | ...otherConfig,
28 | method: method || 'GET',
29 | url,
30 | cancelToken: new CancelToken(c => {
31 | previousStateRequest.cancel = c
32 | })
33 | }).then(resp => {
34 | previousStateRequest.state = 'finished'
35 | commit(pluginActions.success, { resp: resp, keyPath })
36 | if (onSuccess) {
37 | const { dispatchAction, executeFunction, commitAction } = onSuccess
38 | if (dispatchAction) {
39 | const { type, payload } = dispatchAction
40 | dispatch(type, payload)
41 | }
42 | if (commitAction) {
43 | const { type, payload } = commitAction
44 | commit(type, payload)
45 | }
46 | if (executeFunction) {
47 | executeFunction(resp, { commit, dispatch })
48 | }
49 | }
50 | resolve(resp)
51 | }).catch(err => {
52 | if (axios.isCancel(err)) {
53 | return
54 | }
55 | if (onError) {
56 | const { dispatchAction, executeFunction, commitAction } = onError
57 | if (dispatchAction) {
58 | const { type, payload } = dispatchAction
59 | dispatch(type, payload)
60 | }
61 | if (commitAction) {
62 | const { type, payload } = commitAction
63 | commit(type, payload)
64 | }
65 | if (executeFunction) {
66 | executeFunction(err, { commit, dispatch })
67 | }
68 | }
69 |
70 | commit(pluginActions.error, { err: err.response, keyPath })
71 | reject(err)
72 | })
73 | })
74 | },
75 | [pluginActions.clear]: ({ commit }, keyPath) => {
76 | return new Promise((resolve, reject) => {
77 | commit(pluginActions.clear, keyPath)
78 | resolve()
79 | })
80 | }
81 | }
82 |
83 | const mutations = {
84 | [pluginActions.request]: (state, { keyPath }) => {
85 | const responseStateObject = { status: 'loading' }
86 | const { stateObject, head } = appendStateWithResponse(state, keyPath, responseStateObject)
87 | vue.set(state, head, stateObject)
88 | },
89 | [pluginActions.success]: (state, { keyPath, resp }) => {
90 | delete resp.config // make this configurable
91 | const responseStateObject = { status: 'success', firstCallDone: true, resp }
92 | const { stateObject, head } = appendStateWithResponse(state, keyPath, responseStateObject)
93 | vue.set(state, head, stateObject)
94 | },
95 | [pluginActions.error]: (state, { keyPath, err }) => {
96 | const responseStateObject = { status: 'error', err, firstCallDone: true }
97 | const { stateObject, head } = appendStateWithResponse(state, keyPath, responseStateObject)
98 | vue.set(state, head, stateObject)
99 | },
100 | [pluginActions.clear]: (state, keyPath) => {
101 | const responseStateObject = {}
102 | const { stateObject, head } = appendStateWithResponse(state, keyPath, responseStateObject)
103 | vue.set(state, head, stateObject)
104 | }
105 | }
106 |
107 | const module = {
108 | state,
109 | actions,
110 | mutations
111 | }
112 |
113 | export default module
114 |
--------------------------------------------------------------------------------
/src/plugin/__snapshots__/module.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Module Actions should process clear 1`] = `
4 | Array [
5 | Array [
6 | "Clear_Api_Call",
7 | Object {
8 | "keyPath": "someKeypath",
9 | },
10 | ],
11 | ]
12 | `;
13 |
14 | exports[`Module Actions should process onSuccess 1`] = `
15 | Array [
16 | Array [
17 | "Request_Api_Call",
18 | Object {
19 | "keyPath": "someKeypath",
20 | },
21 | ],
22 | Array [
23 | "Success_Api_Call",
24 | Object {
25 | "keyPath": "someKeypath",
26 | "resp": Response {
27 | "code": undefined,
28 | "config": Object {
29 | "adapter": [Function],
30 | "cancelToken": CancelToken {
31 | "promise": Promise {},
32 | },
33 | "data": undefined,
34 | "headers": Object {
35 | "Accept": "application/json, text/plain, */*",
36 | },
37 | "keyPath": "someKeypath",
38 | "maxContentLength": -1,
39 | "method": "get",
40 | "onSuccess": Object {
41 | "dispatchAction": Object {
42 | "payload": "test-payload",
43 | "type": "test-type",
44 | },
45 | },
46 | "timeout": 0,
47 | "transformRequest": Object {
48 | "0": [Function],
49 | },
50 | "transformResponse": Object {
51 | "0": [Function],
52 | },
53 | "url": "testUrl",
54 | "validateStatus": [Function],
55 | "xsrfCookieName": "XSRF-TOKEN",
56 | "xsrfHeaderName": "X-XSRF-TOKEN",
57 | },
58 | "data": "test",
59 | "headers": undefined,
60 | "request": Request {
61 | "config": Object {
62 | "adapter": [Function],
63 | "cancelToken": CancelToken {
64 | "promise": Promise {},
65 | },
66 | "data": undefined,
67 | "headers": Object {
68 | "Accept": "application/json, text/plain, */*",
69 | },
70 | "keyPath": "someKeypath",
71 | "maxContentLength": -1,
72 | "method": "get",
73 | "onSuccess": Object {
74 | "dispatchAction": Object {
75 | "payload": "test-payload",
76 | "type": "test-type",
77 | },
78 | },
79 | "timeout": 0,
80 | "transformRequest": Object {
81 | "0": [Function],
82 | },
83 | "transformResponse": Object {
84 | "0": [Function],
85 | },
86 | "url": "testUrl",
87 | "validateStatus": [Function],
88 | "xsrfCookieName": "XSRF-TOKEN",
89 | "xsrfHeaderName": "X-XSRF-TOKEN",
90 | },
91 | "headers": Object {
92 | "Accept": "application/json, text/plain, */*",
93 | },
94 | "reject": [Function],
95 | "resolve": [Function],
96 | "responseType": undefined,
97 | "timeout": 0,
98 | "url": "testUrl",
99 | "withCredentials": false,
100 | },
101 | "status": 200,
102 | "statusText": undefined,
103 | },
104 | },
105 | ],
106 | ]
107 | `;
108 |
109 | exports[`Module Actions should process onSuccess 2`] = `
110 | Array [
111 | Array [
112 | "test-type",
113 | "test-payload",
114 | ],
115 | ]
116 | `;
117 |
118 | exports[`Module Actions should process request 1`] = `
119 | Array [
120 | Array [
121 | "Request_Api_Call",
122 | Object {
123 | "keyPath": undefined,
124 | },
125 | ],
126 | ]
127 | `;
128 |
129 | exports[`Module mutations should process clear 1`] = `
130 | Object {
131 | "someKeyPath": Object {},
132 | }
133 | `;
134 |
135 | exports[`Module mutations should process error 1`] = `
136 | Object {
137 | "someKeyPath": Object {
138 | "err": Object {
139 | "data": "fakeError",
140 | },
141 | "firstCallDone": true,
142 | "status": "error",
143 | },
144 | }
145 | `;
146 |
147 | exports[`Module mutations should process request -> error 1`] = `
148 | Object {
149 | "someKeyPath": Object {},
150 | }
151 | `;
152 |
153 | exports[`Module mutations should process request -> error 2`] = `
154 | Object {
155 | "someKeyPath": Object {
156 | "err": Object {
157 | "data": "fakeError",
158 | },
159 | "firstCallDone": true,
160 | "status": "error",
161 | },
162 | }
163 | `;
164 |
165 | exports[`Module mutations should process request -> success 1`] = `
166 | Object {
167 | "someKeyPath": Object {
168 | "firstCallDone": true,
169 | "resp": Object {
170 | "date": "fakeData",
171 | },
172 | "status": "success",
173 | },
174 | }
175 | `;
176 |
177 | exports[`Module mutations should process request 1`] = `
178 | Object {
179 | "someKeyPath": Object {
180 | "status": "loading",
181 | },
182 | }
183 | `;
184 |
185 | exports[`Module mutations should process success 1`] = `
186 | Object {
187 | "someKeyPath": Object {
188 | "firstCallDone": true,
189 | "resp": Object {
190 | "date": "fakeData",
191 | },
192 | "status": "success",
193 | },
194 | }
195 | `;
196 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/vouill/vuex-api)
2 |
3 | # Deprecated
4 |
5 | Not maintenained anymore.
6 |
7 |
8 | # Vuex api
9 |
10 | ## Intro
11 |
12 | `vuex-api` is a library that wants to help you handle API as easily as possible. Yes, that's it !
13 |
14 |
15 |
16 | ## Install
17 |
18 | 1. Make sure you have `vuex` set up in your app.
19 | 2. In your `store.js`, install the `vuex-api` module:
20 |
21 | ```javascript
22 | import vuexApi from 'vuex-api'
23 |
24 | export default new Vuex.Store({
25 | modules: {
26 | vuexApi,
27 | },
28 | })
29 | ```
30 |
31 |
32 |
33 | ## Usage
34 |
35 | This library allows you [multiple](#documentation) way to make and consume API calls. Here is an exemple of one of them:
36 |
37 | ```javascript
38 | created: function () {
39 | this.vuexApiCall(
40 | {
41 | baseURL: 'https://jsonplaceholder.typicode.com',
42 | url: 'posts',
43 | keyPath: ['typicode', 'posts'] // is the to reach the data in your vuex state
44 | }
45 | )
46 | },
47 | ```
48 |
49 |
50 |
51 | ```html
52 | // Render components based on the status of the API call. The component will have the api call status injected in their props automatically
53 |
54 |
55 |
56 |
57 |
58 | ```
59 |
60 |
61 |
62 |
63 |
64 | ## Motivation
65 |
66 | You can do API calls for anything:
67 |
68 | - getting a blog post
69 | - create a recipe
70 | - delete a user
71 |
72 | However, API calls are a pretty repetitive when you think about it.
73 |
74 | 1. Emit the API call
75 | 2. Display loading (or not)
76 | 3. Receive Data
77 | 4. Display Data (or error)
78 | 5. Maybe as consequence execute custom code
79 |
80 |
81 |
82 | This Library goals is to abstract this repetition from you. You only need to give the necessary data to perform the API call and that's it. The library will:
83 |
84 | - Handle the whole api call from loading to success and error
85 | - Give you mixins and HOC to help you make and retrieve API calls.
86 |
87 |
88 |
89 | ## Documentation
90 |
91 | Vuex-api is nothing but a vuex module. It listen to a certain action, with a payload that will be used to perform the API Call. Then it will store the different states of the response in the vuex state.
92 |
93 | Because we have all the logic handled at one place, we can add tooling around that will help us reduce the pain to handle api calls.
94 |
95 | This doc is split in 2 parts.
96 |
97 | -> Sending API calls
98 |
99 | -> Retrieve API call data
100 |
101 | ## Emit API calls
102 |
103 | Emitting an API call will always require you to send the info necessary to perform the api request.
104 |
105 | ```
106 | baseURL: 'https://jsonplaceholder.typicode.com'
107 | method: 'GET'
108 | url: 'posts'
109 | keyPath: ['article', 'slugId'] // the only attribute not shapped in the axios request object. I will define the path where the api call states will be stored in vuex.
110 | ```
111 |
112 | The following are 3 independant methods to emit the same API call.
113 |
114 | ### Using store dispatch
115 |
116 | [>> Interactive demo](https://codesandbox.io/s/5yq65jrqop)
117 |
118 | To emit a vuex-API call you need to fire an action with the following data:
119 |
120 | ```javascript
121 | import { actions } from 'vuex-api'
122 |
123 | created: function(){
124 | this.$store.dispatch(actions.request,{
125 | baseURL: 'https://jsonplaceholder.typicode.com',
126 | url: 'posts',
127 | keyPath: ['typicode', 'posts']
128 | }
129 | )
130 | },
131 | ```
132 |
133 |
134 |
135 | ### Using mixins
136 |
137 | [>> Interactive demo](https://codesandbox.io/s/n92rmvk844)
138 |
139 | ```
140 | mixins:[
141 | vuexApiCallMixin,
142 | ],
143 | ```
144 |
145 |
146 |
147 | `vuexApiCallMixin` exposes to the vue component 2 methods:
148 |
149 | - `vuexApiCall(VuexApiRequestObject)` Perform an API call using the passed VuexApiRequestObject
150 | - `vuexApiClear(keyPath)` Clears the state at the given keypath
151 |
152 |
153 |
154 | ```javascript
155 | created: function () {
156 | this.vuexApiCall(
157 | {
158 | baseURL: 'https://jsonplaceholder.typicode.com',
159 | url: 'posts',
160 | keyPath: ['typicode', 'posts']
161 | }
162 | )
163 | },
164 | ```
165 |
166 |
167 |
168 | ### Using a component
169 |
170 | [>> Interactive demo](https://codesandbox.io/s/2p4q04pxj)
171 |
172 | This one is mostly suited for GET calls. It's a pretty powerfull way to handle GET calls in a breaze.
173 |
174 | ```javascript
175 | Vue.component('json-api', ApiHandlerComponent({
176 | requestConfig: { baseURL: 'https://jsonplaceholder.typicode.com' }
177 | }))
178 | ```
179 |
180 | ```html
181 |
182 | ```
183 |
184 |
185 |
186 | ## Retrieve API call data
187 |
188 | ### Accessing state
189 |
190 | [>> Interactive demo](https://codesandbox.io/s/5yq65jrqop)
191 |
192 | You can get the data related to your API call directly from the vuex state as you would do normally
193 |
194 | ```javascript
195 | this.$store.state.vuexApi.popular.feed
196 |
197 | // or
198 |
199 | computed: mapState({
200 | data = state => state.vuexApi.popular.feed
201 | })
202 | ```
203 |
204 | ### Using Mixin
205 |
206 | [>> Interactive demo](https://codesandbox.io/s/n92rmvk844)
207 |
208 | `vuexApiGetStateMixin(keyPath, attributeName?)` exposes to the vue component a computed value representing the state at the given keyPath under `this[attributeName]`.
209 |
210 |
211 |
212 | ### Use HOC
213 |
214 | [>> Interactive demo](https://codesandbox.io/s/25lq28o0p)
215 |
216 | This hoc will render the given child component depending on the status ate of the API Call. More over it will auto inject the api call response to the children in its props.
217 |
218 | ```html
219 |
220 |
221 |
222 |
223 |
224 | ```
225 |
226 |
227 |
228 |
--------------------------------------------------------------------------------