├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .nvmrc ├── .postcssrc.js ├── README.md ├── babel.config.js ├── jest.config.js ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── components │ ├── AppModal.spec.js │ ├── AppModal.vue │ ├── PageHome.spec.js │ ├── PageHome.vue │ └── modal │ │ ├── ModalLogin.spec.js │ │ └── ModalLogin.vue ├── main.js └── store │ ├── __mocks__ │ └── index.js │ └── index.js ├── vue.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | jest: true, 5 | node: true, 6 | }, 7 | 'extends': [ 8 | "@avalanche/eslint-config", 9 | "plugin:vue/recommended", 10 | ], 11 | rules: { 12 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn', 13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn' 14 | }, 15 | parserOptions: { 16 | parser: 'babel-eslint' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | src/**/*.spec.js linguist-detectable=false 2 | __mocks__/* linguist-detectable=false 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.log 5 | *.orig 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *.zip 11 | *~ 12 | 13 | # OS or Editor folders 14 | ._* 15 | .cache 16 | .DS_Store 17 | .idea 18 | .project 19 | .settings 20 | .tmproj 21 | *.esproj 22 | *.sublime-project 23 | *.sublime-workspace 24 | nbproject 25 | Thumbs.db 26 | 27 | # Folders to ignore 28 | dist 29 | node_modules 30 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v9.3.0 2 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building a Modal Dialog with Vue.js and Vuex 2 | 3 | [![Patreon](https://img.shields.io/badge/patreon-donate-blue.svg)](https://www.patreon.com/maoberlehner) 4 | [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/maoberlehner) 5 | 6 | This is an example project for the following article: [Building a Modal Dialog with Vue.js and Vuex](https://markus.oberlehner.net/blog/building-a-modal-dialog-with-vue-and-vuex/) 7 | 8 | ## Build Setup 9 | 10 | ``` bash 11 | # Install dependencies. 12 | npm install 13 | 14 | # Serve with hot reload at localhost:8080 15 | npm run serve 16 | 17 | # Build for production with minification. 18 | npm run build 19 | ``` 20 | 21 | ## About 22 | 23 | ### Author 24 | 25 | Markus Oberlehner 26 | Website: https://markus.oberlehner.net 27 | Twitter: https://twitter.com/MaOberlehner 28 | PayPal.me: https://paypal.me/maoberlehner 29 | Patreon: https://www.patreon.com/maoberlehner 30 | 31 | ### License 32 | 33 | MIT 34 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | `@vue/app`, 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | `js`, 4 | `jsx`, 5 | `json`, 6 | `vue`, 7 | ], 8 | transform: { 9 | '^.+\\.vue$': `vue-jest`, 10 | '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': `jest-transform-stub`, 11 | '^.+\\.jsx?$': `babel-jest`, 12 | }, 13 | snapshotSerializers: [ 14 | `jest-serializer-vue`, 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "building-a-modal-dialog-with-vue-and-vuex", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "Markus Oberlehner ", 6 | "private": true, 7 | "scripts": { 8 | "serve": "vue-cli-service serve", 9 | "build": "vue-cli-service build", 10 | "lint": "vue-cli-service lint", 11 | "test:unit": "vue-cli-service test:unit" 12 | }, 13 | "dependencies": { 14 | "vue": "^2.5.16", 15 | "vuex": "^3.0.1" 16 | }, 17 | "devDependencies": { 18 | "@avalanche/eslint-config": "^2.0.0", 19 | "@vue/cli-plugin-babel": "^3.0.0-beta.15", 20 | "@vue/cli-plugin-eslint": "^3.0.0-beta.15", 21 | "@vue/cli-plugin-unit-jest": "^3.0.0-beta.15", 22 | "@vue/cli-service": "^3.0.0-beta.15", 23 | "@vue/test-utils": "^1.0.0-beta.16", 24 | "babel-core": "7.0.0-bridge.0", 25 | "babel-jest": "^23.0.1", 26 | "eslint-plugin-import": "^2.12.0", 27 | "node-sass": "^4.9.0", 28 | "sass-loader": "^7.0.1", 29 | "vue-template-compiler": "^2.5.16" 30 | }, 31 | "browserslist": [ 32 | "> 1%", 33 | "last 2 versions", 34 | "not ie <= 8" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoberlehner/building-a-modal-dialog-with-vue-and-vuex/933e67a77691193f4b30cf9575d342e25fbf20e6/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | building-a-modal-dialog-with-vue-and-vuex 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /src/components/AppModal.spec.js: -------------------------------------------------------------------------------- 1 | import Vuex from 'vuex'; 2 | import { createLocalVue, mount, shallowMount } from '@vue/test-utils'; 3 | 4 | import { __createMocks as createStoreMocks } from '../store'; 5 | 6 | import AppModal from './AppModal.vue'; 7 | 8 | // Tell Jest to use the mock 9 | // implementation of the store. 10 | jest.mock(`../store`); 11 | 12 | const localVue = createLocalVue(); 13 | 14 | localVue.use(Vuex); 15 | 16 | describe(`AppModal`, () => { 17 | let storeMocks; 18 | let wrapper; 19 | 20 | beforeEach(() => { 21 | // Create a fresh store and wrapper 22 | // instance for every test case. 23 | storeMocks = createStoreMocks(); 24 | wrapper = shallowMount(AppModal, { 25 | store: storeMocks.store, 26 | localVue, 27 | }); 28 | }); 29 | 30 | test(`It should render an overlay and the content when active.`, () => { 31 | storeMocks.state.modalVisible = true; 32 | 33 | expect(wrapper.contains(`.c-appModal__overlay`)).toBe(true); 34 | expect(wrapper.contains(`.c-appModal__content`)).toBe(true); 35 | }); 36 | 37 | test(`It should not render an overlay and the content when inactive.`, () => { 38 | storeMocks.state.modalVisible = false; 39 | 40 | expect(wrapper.contains(`.c-appModal__overlay`)).toBe(false); 41 | expect(wrapper.contains(`.c-appModal__content`)).toBe(false); 42 | }); 43 | 44 | test(`It should close the modal when the user clicks on the background.`, () => { 45 | storeMocks.state.modalVisible = true; 46 | 47 | wrapper.find(`.c-appModal__content`).trigger(`click`); 48 | 49 | expect(storeMocks.mutations.hideModal).toBeCalled(); 50 | }); 51 | 52 | test(`It should close the modal when the user presses the escape key.`, () => { 53 | storeMocks.state.modalVisible = true; 54 | 55 | document.dispatchEvent(new KeyboardEvent(`keydown`, { key: `Escape` })); 56 | 57 | expect(storeMocks.mutations.hideModal).toBeCalled(); 58 | }); 59 | 60 | test(`It should render the given component.`, async () => { 61 | storeMocks.state.modalVisible = true; 62 | 63 | wrapper = mount(AppModal, { 64 | store: storeMocks.store, 65 | localVue, 66 | }); 67 | wrapper.setComputed({ 68 | modalComponent: `ModalLogin`, 69 | }); 70 | 71 | // For some reason the dynamic import is triggered 72 | // twice in tests (but not in production) to compensate 73 | // for that, we have to wait twice for the next tick 74 | // (I guess this is a bug in vue-test-utils). 75 | await wrapper.vm.$nextTick(); 76 | await wrapper.vm.$nextTick(); 77 | 78 | expect(wrapper.contains(`.c-modalLogin`)).toBe(true); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/components/AppModal.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 74 | 75 | 170 | -------------------------------------------------------------------------------- /src/components/PageHome.spec.js: -------------------------------------------------------------------------------- 1 | import Vuex from 'vuex'; 2 | import { createLocalVue, shallowMount } from '@vue/test-utils'; 3 | 4 | import { __createMocks as createStoreMocks } from '../store'; 5 | 6 | import PageHome from './PageHome.vue'; 7 | 8 | // Tell Jest to use the mock 9 | // implementation of the store. 10 | jest.mock(`../store`); 11 | 12 | const localVue = createLocalVue(); 13 | 14 | localVue.use(Vuex); 15 | 16 | describe(`PageHome`, () => { 17 | let storeMocks; 18 | let wrapper; 19 | 20 | beforeEach(() => { 21 | // Create a fresh store and wrapper 22 | // instance for every test case. 23 | storeMocks = createStoreMocks(); 24 | wrapper = shallowMount(PageHome, { 25 | store: storeMocks.store, 26 | localVue, 27 | }); 28 | }); 29 | 30 | test(`It should open the modal when clicking the login button.`, () => { 31 | wrapper.find(`.c-pageHome__login`).trigger(`click`); 32 | 33 | expect(storeMocks.mutations.showModal).toBeCalled(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/components/PageHome.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | -------------------------------------------------------------------------------- /src/components/modal/ModalLogin.spec.js: -------------------------------------------------------------------------------- 1 | import Vuex from 'vuex'; 2 | import { createLocalVue, shallowMount } from '@vue/test-utils'; 3 | 4 | import { __createMocks as createStoreMocks } from '../../store'; 5 | 6 | import ModalLogin from './ModalLogin.vue'; 7 | 8 | // Tell Jest to use the mock 9 | // implementation of the store. 10 | jest.mock(`../../store`); 11 | 12 | const localVue = createLocalVue(); 13 | 14 | localVue.use(Vuex); 15 | 16 | describe(`ModalLogin`, () => { 17 | let storeMocks; 18 | let wrapper; 19 | 20 | beforeEach(() => { 21 | // Create a fresh store and wrapper 22 | // instance for every test case. 23 | storeMocks = createStoreMocks(); 24 | wrapper = shallowMount(ModalLogin, { 25 | store: storeMocks.store, 26 | localVue, 27 | }); 28 | }); 29 | 30 | test(`It should close the modal when clicking cancel.`, () => { 31 | wrapper.find(`.c-modalLogin__cancel`).trigger(`click`); 32 | 33 | expect(storeMocks.mutations.hideModal).toBeCalled(); 34 | }); 35 | 36 | test(`It should close the modal after successfully logging in.`, () => { 37 | wrapper.find(`.c-modalLogin__login`).trigger(`click`); 38 | 39 | expect(storeMocks.mutations.hideModal).toBeCalled(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/components/modal/ModalLogin.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 50 | 51 | 62 | 63 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import { store } from './store'; 4 | import App from './App.vue'; 5 | 6 | Vue.config.productionTip = false; 7 | 8 | // eslint-disable-next-line no-new 9 | new Vue({ 10 | el: `#app`, 11 | render: h => h(App), 12 | store, 13 | }); 14 | -------------------------------------------------------------------------------- /src/store/__mocks__/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | Vue.use(Vuex); 5 | 6 | // eslint-disable-next-line no-underscore-dangle 7 | export function __createMockMutations(customMutations) { 8 | return Object.assign({ 9 | showModal: jest.fn(), 10 | hideModal: jest.fn(), 11 | }, customMutations); 12 | } 13 | 14 | export const mutations = __createMockMutations(); 15 | 16 | // eslint-disable-next-line no-underscore-dangle 17 | export function __createMockState(customState) { 18 | return Object.assign({ 19 | modalVisible: false, 20 | modalComponent: null, 21 | }, customState); 22 | } 23 | 24 | export const state = __createMockState(); 25 | 26 | // eslint-disable-next-line no-underscore-dangle 27 | export function __createMocks(custom = { 28 | mutations: {}, 29 | state: {}, 30 | }) { 31 | const mockMutations = __createMockMutations(custom.mutations); 32 | const mockState = __createMockState(custom.state); 33 | 34 | return { 35 | mutations: mockMutations, 36 | state: mockState, 37 | store: new Vuex.Store({ 38 | mutations: mockMutations, 39 | state: mockState, 40 | }), 41 | }; 42 | } 43 | 44 | export const { store } = __createMocks(); 45 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | Vue.use(Vuex); 5 | 6 | export const mutations = { 7 | showModal(state, componentName) { 8 | // eslint-disable-next-line no-param-reassign 9 | state.modalVisible = true; 10 | // eslint-disable-next-line no-param-reassign 11 | state.modalComponent = componentName; 12 | }, 13 | hideModal(state) { 14 | // eslint-disable-next-line no-param-reassign 15 | state.modalVisible = false; 16 | }, 17 | }; 18 | 19 | export const state = { 20 | modalVisible: false, 21 | modalComponent: null, 22 | }; 23 | 24 | export const store = new Vuex.Store({ 25 | mutations, 26 | state, 27 | }); 28 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lintOnSave: false, 3 | }; 4 | --------------------------------------------------------------------------------