├── .dockerignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── babel.config.js ├── cypress.json ├── docker-compose.yml ├── images ├── architecture.png └── screenshot.png ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── sandbox.config.json ├── src ├── components │ ├── app │ │ └── app.vue │ ├── copy-right │ │ ├── copy-right.spec.js │ │ └── copy-right.vue │ ├── footer │ │ └── footer.vue │ ├── header │ │ ├── header.spec.js │ │ └── header.vue │ ├── item │ │ ├── item.spec.js │ │ └── item.vue │ └── list │ │ └── list.vue ├── constants │ ├── action-types.js │ └── filter.js ├── index.html ├── main.js ├── services │ └── todo-local.js ├── store │ ├── actions │ │ ├── filter.js │ │ └── todo.js │ ├── getters │ │ └── todo.js │ ├── index.js │ ├── index.spec.js │ └── mutations │ │ ├── filter.js │ │ ├── todo.js │ │ └── todo.spec.js └── web-components │ └── username.js ├── tests └── e2e │ ├── .eslintrc.js │ ├── plugins │ └── index.js │ ├── specs │ └── new-todo.spec.js │ └── support │ ├── commands.js │ └── index.js ├── vite.config.js └── vue.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | browser: true 6 | }, 7 | plugins: ['prettier'], 8 | extends: ['plugin:vue/essential', 'airbnb/base'], 9 | rules: { 10 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 11 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'object-curly-newline': ['error', { consistent: true }], 13 | 'max-len': ['error', { code: 120 }], 14 | 'comma-dangle': ['error', 'never'], 15 | 'arrow-parens': ['error', 'as-needed'], 16 | 'prettier/prettier': 'error', 17 | 'no-return-assign': 0, 18 | 'no-param-reassign': 0, 19 | 'implicit-arrow-linebreak': 0, 20 | 'import/prefer-default-export': 0, 21 | 'import/no-extraneous-dependencies': 0, 22 | 'vue/multi-word-component-names': 0 23 | }, 24 | parserOptions: { 25 | parser: '@babel/eslint-parser' 26 | }, 27 | overrides: [ 28 | { 29 | files: ['**/*.spec.{j,t}s?(x)'], 30 | env: { 31 | mocha: true 32 | } 33 | } 34 | ] 35 | }; 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "semi": true, 6 | "useTabs": false, 7 | "tabWidth": 2, 8 | "arrowParens": "avoid", 9 | "endOfLine": "lf", 10 | "printWidth": 120 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | script: npm test && npm run lint 5 | before_deploy: npm run build 6 | deploy: 7 | provider: pages 8 | local_dir: dist 9 | skip_cleanup: true 10 | github_token: $GITHUB_TOKEN # Set in the settings page of your repository, as a secure variable 11 | keep_history: true 12 | on: 13 | branch: master 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at soos.gabor86@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Issues 4 | 5 | Issues are very valuable to this project. 6 | 7 | * Ideas are a valuable source of contributions others can make 8 | * Problems show where this project is lacking 9 | * With a question you show where contributors can improve the user experience 10 | 11 | Thank you for creating them. 12 | 13 | ## Pull Requests 14 | 15 | Pull requests are, a great way to get your ideas into this repository. 16 | 17 | When deciding if I merge in a pull request I look at the following things: 18 | 19 | ### Does it state intent 20 | 21 | You should be clear which problem you're trying to solve with your contribution. 22 | 23 | For example: 24 | 25 | > Add link to code of conduct in README.md 26 | 27 | Doesn't tell me anything about why you're doing that 28 | 29 | > Add link to code of conduct in README.md because users don't always look in the CONTRIBUTING.md 30 | 31 | Tells me the problem that you have found, and the pull request shows me the action you have taken to solve it. 32 | 33 | 34 | ### Is it of good quality 35 | 36 | * There are no spelling mistakes 37 | * It reads well 38 | * For english language contributions: Has a good score on [Grammarly](grammarly.com) or [Hemingway App](http://www.hemingwayapp.com/) 39 | 40 | ### Does it move this repository closer to my vision for the repository 41 | 42 | The aim of this repository is: 43 | 44 | * To provide a README.md and assorted documents anyone can copy and paste, into their project 45 | * The content is usable by someone who hasn't written something like this before 46 | * Foster a culture of respect and gratitude in the open source community. 47 | 48 | ### Does it follow the contributor covenant 49 | 50 | This repository has a [code of conduct](CODE_OF_CONDUCT.md), This repository has a code of conduct, I will remove things that do not respect it. 51 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | WORKDIR /app 3 | COPY package.json /app 4 | RUN npm install 5 | COPY . /app 6 | EXPOSE 8900 7 | CMD npm start 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Gábor Soós 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 | # TodoMVC built with Vue 3 Composition Api and Vuex 2 | 3 | [![Build Status](https://travis-ci.com/blacksonic/todomvc-vue-composition-api.svg?branch=master)](https://travis-ci.com/blacksonic/todomvc-vue-composition-api) 4 | [![Dependencies Status](https://david-dm.org/blacksonic/todomvc-vue-composition-api/status.svg)](https://david-dm.org/blacksonic/todomvc-vue-composition-api) 5 | 6 | The well-known TodoMVC built with Vue 3 Composition Api and Vuex in a structured and testable way. 7 | 8 | ![TodoMVC Vue](./images/screenshot.png "TodoMVC Vue") 9 | 10 | [Edit and try it out online](https://codesandbox.io/s/github/blacksonic/todomvc-vue-composition-api) 11 | 12 | ## Concepts and tools covered 13 | 14 | - [Vue CLI](https://cli.vuejs.org/) 15 | - [Composition Api](https://composition-api.vuejs.org/#summary) 16 | - [Vuex](https://vuex.vuejs.org/) 17 | - [Unit Testing](https://vue-test-utils.vuejs.org/) 18 | - [E2E Testing](https://www.cypress.io/) 19 | 20 | ## Usage 21 | 22 | After installing the dependencies the following NPM scripts become available: 23 | 24 | - `start`: starts the application in development mode on [http://localhost:9000](http://localhost:9000) 25 | - `build`: bundles the application for production into the `dist` folder 26 | - `test`: runs unit and E2E tests 27 | - `test:unit`: runs unit tests with [Mocha](https://mochajs.org/) and [Chai](https://www.chaijs.com/) in the `src` folder suffixed with `*.spec.js` 28 | - `test:e2e`: runs E2E tests with [Cypress](https://www.cypress.io/) in the `tests/e2e` folder suffixed with `*.spec.js` 29 | - `format`: formats the code with [Prettier](https://prettier.io/) within the `src` folder 30 | - `lint`: lint files with [ESLint](https://eslint.org/) based on [Airbnb's styleguide](https://github.com/airbnb/javascript) and the Prettier config 31 | 32 | ## Component architecture 33 | 34 | ![Architecture](./images/architecture.png) 35 | 36 | Application is compatible with [Vue devtools](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd?hl=en) 37 | 38 | ## Series 39 | 40 | This implementation is part of a series where the same application was implemented with the same architecture. 41 | 42 | - [Vue](https://github.com/blacksonic/todomvc-vue) 43 | - [Vue Composition API](https://github.com/blacksonic/todomvc-vue-composition-api) 44 | - [Angular](https://github.com/blacksonic/todomvc-angular) 45 | - [React](https://github.com/blacksonic/todomvc-react) 46 | - [React Hooks](https://github.com/blacksonic/todomvc-react-hooks) 47 | - [Svelte](https://github.com/blacksonic/todomvc-svelte) 48 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'] 3 | }; 4 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins/index.js", 3 | "video": false 4 | } 5 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | client: 5 | image: node:12 6 | working_dir: /app 7 | volumes: 8 | - ./:/app 9 | ports: 10 | - 8900:8900 11 | command: sh -c "npm install && npm start" 12 | -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonicoder86/todomvc-vue-composition-api/a2efb13d0ae358c4c644624c9d0ce0a6fc1e931e/images/architecture.png -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonicoder86/todomvc-vue-composition-api/a2efb13d0ae358c4c644624c9d0ce0a6fc1e931e/images/screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-vue-composition-api", 3 | "license": "MIT", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/blacksonic/todomvc-vue-composition-api.git" 7 | }, 8 | "scripts": { 9 | "build": "vue-cli-service build", 10 | "test:unit": "vue-cli-service test:unit --recursive 'src/**/*.spec.js'", 11 | "test:e2e": "vue-cli-service test:e2e --headless", 12 | "lint": "vue-cli-service lint --no-fix", 13 | "format": "prettier --write 'src/**/*.{js,vue}'", 14 | "report": "vue-cli-service build --report", 15 | "start": "vue-cli-service serve", 16 | "test": "npm run test:unit && npm run test:e2e", 17 | "vite": "vite", 18 | "vite:build": "vite build" 19 | }, 20 | "dependencies": { 21 | "core-js": "^3.22.0", 22 | "todomvc-app-css": "^2.4.2", 23 | "uuid": "^8.3.2", 24 | "vue": "^3.2.33", 25 | "vuex": "^4.0.2" 26 | }, 27 | "devDependencies": { 28 | "@babel/eslint-parser": "^7.17.0", 29 | "@vitejs/plugin-vue": "^2.3.1", 30 | "@vue/cli-plugin-babel": "^5.0.4", 31 | "@vue/cli-plugin-e2e-cypress": "^5.0.4", 32 | "@vue/cli-plugin-eslint": "^5.0.4", 33 | "@vue/cli-plugin-unit-mocha": "^5.0.4", 34 | "@vue/cli-service": "^5.0.4", 35 | "@vue/compiler-sfc": "^3.2.33", 36 | "@vue/test-utils": "2.0.0-rc.20", 37 | "chai": "^4.3.6", 38 | "cypress": "^9.5.4", 39 | "eslint": "^7.32.0", 40 | "eslint-config-airbnb": "^19.0.4", 41 | "eslint-plugin-import": "^2.26.0", 42 | "eslint-plugin-prettier": "^4.0.0", 43 | "eslint-plugin-vue": "^8.6.0", 44 | "prettier": "^2.6.2", 45 | "sinon": "^13.0.2", 46 | "sinon-chai": "^3.7.0", 47 | "vite": "^2.9.5", 48 | "vue-loader": "^17.0.0" 49 | }, 50 | "postcss": { 51 | "plugins": { 52 | "autoprefixer": {} 53 | } 54 | }, 55 | "browserslist": [ 56 | "last 1 chrome version", 57 | "last 1 firefox version" 58 | ], 59 | "engines": { 60 | "node": "14.15.3" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonicoder86/todomvc-vue-composition-api/a2efb13d0ae358c4c644624c9d0ce0a6fc1e931e/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TodoMVC built with Vue Composition Api and Vuex 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "infiniteLoopProtection": true, 3 | "hardReloadOnChange": false, 4 | "view": "browser", 5 | "server": "true", 6 | "container": { 7 | "port": 9000 8 | }, 9 | "template": "node" 10 | } 11 | -------------------------------------------------------------------------------- /src/components/app/app.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 40 | -------------------------------------------------------------------------------- /src/components/copy-right/copy-right.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { mount } from '@vue/test-utils'; 3 | import CopyRightComponent from './copy-right.vue'; 4 | 5 | describe('CopyRight', () => { 6 | it('should render component', () => { 7 | const wrapper = mount(CopyRightComponent); 8 | 9 | expect(wrapper.find('.info').text()).to.contain('Double-click to edit a todo'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/copy-right/copy-right.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /src/components/footer/footer.vue: -------------------------------------------------------------------------------- 1 | 30 | 46 | -------------------------------------------------------------------------------- /src/components/header/header.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { mount } from '@vue/test-utils'; 3 | import Header from './header.vue'; 4 | import { createStore } from '../../store/index'; 5 | 6 | describe('Header', () => { 7 | it('should add new element to store', () => { 8 | const store = createStore(); 9 | 10 | const wrapper = mount(Header, { global: { provide: { store } } }); 11 | 12 | const input = wrapper.find('input'); 13 | input.element.value = 'Demo'; 14 | input.trigger('input'); 15 | input.trigger('keyup', { key: 'Enter' }); 16 | 17 | expect(store.state.todos).to.have.length(1); 18 | expect(store.state.todos[0].id).to.be.a('string'); 19 | expect(store.state.todos[0].name).to.eql('Demo'); 20 | expect(store.state.todos[0].completed).to.eql(false); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/components/header/header.vue: -------------------------------------------------------------------------------- 1 | 32 | 44 | -------------------------------------------------------------------------------- /src/components/item/item.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { mount } from '@vue/test-utils'; 3 | import Item from './item.vue'; 4 | 5 | describe('Item', () => { 6 | it('should display todo item', () => { 7 | const wrapper = mount(Item, { 8 | props: { 9 | todo: { id: 'e2bb892a-844a-47fb-a2b3-47f491af9d88', name: 'Demo', completed: false } 10 | } 11 | }); 12 | 13 | expect(wrapper.find('label').text()).to.eql('Demo'); 14 | }); 15 | 16 | it('should mark todo item as completed', () => { 17 | const wrapper = mount(Item, { 18 | props: { 19 | todo: { id: 'e2bb892a-844a-47fb-a2b3-47f491af9d88', name: 'Demo', completed: true } 20 | } 21 | }); 22 | 23 | expect(wrapper.find('li').classes()).to.contain('completed'); 24 | }); 25 | 26 | it('should notify about remove button', () => { 27 | const wrapper = mount(Item, { 28 | props: { 29 | todo: { id: 'e2bb892a-844a-47fb-a2b3-47f491af9d88', name: 'Demo', completed: false } 30 | } 31 | }); 32 | 33 | wrapper.find('.destroy').trigger('click'); 34 | 35 | expect(wrapper.emitted().remove).to.have.lengthOf(1); 36 | expect(wrapper.emitted().remove[0]).to.eql(['e2bb892a-844a-47fb-a2b3-47f491af9d88']); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/components/item/item.vue: -------------------------------------------------------------------------------- 1 | 36 | 46 | -------------------------------------------------------------------------------- /src/components/list/list.vue: -------------------------------------------------------------------------------- 1 | 25 | 35 | -------------------------------------------------------------------------------- /src/constants/action-types.js: -------------------------------------------------------------------------------- 1 | export const ACTION_TYPES = { 2 | load: 'load', 3 | create: 'create', 4 | remove: 'remove', 5 | update: 'update', 6 | completeAll: 'complete_all', 7 | clearCompleted: 'clear_completed', 8 | selectFilter: 'select_filter' 9 | }; 10 | -------------------------------------------------------------------------------- /src/constants/filter.js: -------------------------------------------------------------------------------- 1 | export const FILTERS = { 2 | all: 'all', 3 | active: 'active', 4 | completed: 'completed' 5 | }; 6 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TodoMVC built with Vue Composition Api and Vuex 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './components/app/app.vue'; 3 | import 'todomvc-app-css/index.css'; 4 | import { createStore } from './store/index'; 5 | import './web-components/username'; 6 | 7 | const app = createApp(App); 8 | app.config.isCustomElement = tag => /^x-/.test(tag); 9 | app.use(createStore()); 10 | app.mount('app-root'); 11 | -------------------------------------------------------------------------------- /src/services/todo-local.js: -------------------------------------------------------------------------------- 1 | const LOCAL_STORAGE_KEY = 'todoapp_todos'; 2 | 3 | export class TodoLocal { 4 | static loadTodos() { 5 | return JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_KEY) || '[]'); 6 | } 7 | 8 | static storeTodos(todos) { 9 | window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos)); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/store/actions/filter.js: -------------------------------------------------------------------------------- 1 | import { ACTION_TYPES } from '../../constants/action-types'; 2 | 3 | export const filterActions = { 4 | onFilterSelect: ({ commit }, filter) => commit(ACTION_TYPES.selectFilter, { filter }) 5 | }; 6 | -------------------------------------------------------------------------------- /src/store/actions/todo.js: -------------------------------------------------------------------------------- 1 | import { ACTION_TYPES } from '../../constants/action-types'; 2 | 3 | export const todoActions = { 4 | onLoad: ({ commit }, todos) => commit(ACTION_TYPES.load, { todos }), 5 | onCreate: ({ commit }, name) => commit(ACTION_TYPES.create, { name }), 6 | onRemove: ({ commit }, id) => commit(ACTION_TYPES.remove, { id }), 7 | onUpdate: ({ commit }, payload) => commit(ACTION_TYPES.update, payload), 8 | onCompleteAll: ({ commit }) => commit(ACTION_TYPES.completeAll), 9 | onClearCompleted: ({ commit }) => commit(ACTION_TYPES.clearCompleted) 10 | }; 11 | -------------------------------------------------------------------------------- /src/store/getters/todo.js: -------------------------------------------------------------------------------- 1 | import { FILTERS } from '../../constants/filter'; 2 | 3 | export function selectCompleted(todos) { 4 | return todos.filter(todo => todo.completed); 5 | } 6 | 7 | export function selectNotCompleted(todos) { 8 | return todos.filter(todo => !todo.completed); 9 | } 10 | 11 | export function selectVisible(todos, filter) { 12 | switch (filter) { 13 | case FILTERS.all: 14 | return [...todos]; 15 | case FILTERS.completed: 16 | return selectCompleted(todos); 17 | case FILTERS.active: 18 | return selectNotCompleted(todos); 19 | default: 20 | return [...todos]; 21 | } 22 | } 23 | 24 | export const getters = { 25 | visibleTodos: state => selectVisible(state.todos, state.filter), 26 | areAllCompleted: state => state.todos.length && state.todos.every(todo => todo.completed), 27 | itemsLeft: state => selectNotCompleted(state.todos).length, 28 | completedCount: state => selectCompleted(state.todos).length 29 | }; 30 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore as createVuexStore } from 'vuex'; 2 | import { getters } from './getters/todo'; 3 | import { FILTERS } from '../constants/filter'; 4 | 5 | import { todosMutations } from './mutations/todo'; 6 | import { filterMutations } from './mutations/filter'; 7 | 8 | import { todoActions } from './actions/todo'; 9 | import { filterActions } from './actions/filter'; 10 | 11 | const mutations = { 12 | ...todosMutations, 13 | ...filterMutations 14 | }; 15 | 16 | const actions = { 17 | ...todoActions, 18 | ...filterActions 19 | }; 20 | 21 | export const createStore = (state = { todos: [], filter: FILTERS.all }) => 22 | createVuexStore({ state, actions, mutations, getters }); 23 | -------------------------------------------------------------------------------- /src/store/index.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { createStore } from './index'; 3 | 4 | describe('createStore', () => { 5 | it('should create a new instance of store', () => { 6 | const store = createStore(); 7 | 8 | expect(store.state.todos).to.eql([]); 9 | expect(store.state.filter).to.eql('all'); 10 | }); 11 | 12 | it('should add new todo', () => { 13 | const store = createStore(); 14 | 15 | store.dispatch('onCreate', 'Demo'); 16 | 17 | expect(store.state.todos).to.have.length(1); 18 | expect(store.state.todos[0].id).to.be.a('string'); 19 | expect(store.state.todos[0].name).to.eql('Demo'); 20 | expect(store.state.todos[0].completed).to.eql(false); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/store/mutations/filter.js: -------------------------------------------------------------------------------- 1 | import { ACTION_TYPES } from '../../constants/action-types'; 2 | 3 | export const filterMutations = { 4 | [ACTION_TYPES.selectFilter]: (state, { filter }) => (state.filter = filter) 5 | }; 6 | -------------------------------------------------------------------------------- /src/store/mutations/todo.js: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { ACTION_TYPES } from '../../constants/action-types'; 3 | import { selectCompleted, selectNotCompleted } from '../getters/todo'; 4 | 5 | const areAllCompleted = state => state.length && selectCompleted(state).length === state.length; 6 | 7 | export const todosMutations = { 8 | [ACTION_TYPES.load]: (state, { todos }) => (state.todos = todos), 9 | [ACTION_TYPES.create]: (state, { name }) => 10 | (state.todos = [...state.todos, { id: uuidv4(), name, completed: false }]), 11 | [ACTION_TYPES.update]: (state, values) => 12 | (state.todos = state.todos.map(todo => (todo.id === values.id ? { ...todo, ...values } : todo))), 13 | [ACTION_TYPES.remove]: (state, { id }) => (state.todos = state.todos.filter(todo => todo.id !== id)), 14 | [ACTION_TYPES.completeAll]: state => { 15 | state.todos = state.todos.map(todo => ({ ...todo, ...{ completed: !areAllCompleted(state) } })); 16 | }, 17 | [ACTION_TYPES.clearCompleted]: state => (state.todos = selectNotCompleted(state.todos)) 18 | }; 19 | -------------------------------------------------------------------------------- /src/store/mutations/todo.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { todosMutations } from './todo'; 3 | 4 | describe('todosMutations', () => { 5 | it('should set list of items on load', () => { 6 | const state = { todos: [] }; 7 | const todos = [{ id: 'e2bb892a', name: 'Demo', completed: false }]; 8 | 9 | todosMutations.load(state, { todos }); 10 | 11 | expect(state.todos).to.eql(todos); 12 | expect(state.todos).to.have.length(1); 13 | expect(state.todos).to.contain(todos[0]); 14 | }); 15 | 16 | it('should create new todo', () => { 17 | const state = { todos: [] }; 18 | 19 | todosMutations.create(state, { name: 'Demo' }); 20 | 21 | expect(state.todos).to.have.length(1); 22 | expect(state.todos[0].id).to.be.a('string'); 23 | expect(state.todos[0].name).to.eql('Demo'); 24 | expect(state.todos[0].completed).to.eql(false); 25 | }); 26 | 27 | it('should update existing todo', () => { 28 | const state = { todos: [{ id: 'e2bb892a', name: 'Demo', completed: false }] }; 29 | 30 | todosMutations.update(state, { id: 'e2bb892a', name: 'Demo2' }); 31 | 32 | expect(state.todos[0].name).to.eql('Demo2'); 33 | }); 34 | 35 | it('should remove existing todo', () => { 36 | const state = { todos: [{ id: 'e2bb892a', name: 'Demo', completed: false }] }; 37 | 38 | todosMutations.remove(state, { id: 'e2bb892a' }); 39 | 40 | expect(state.todos).to.have.length(0); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/web-components/username.js: -------------------------------------------------------------------------------- 1 | export class XUsername extends HTMLElement { 2 | connectedCallback() { 3 | this.innerText = 'blacksonic'; 4 | } 5 | } 6 | 7 | customElements.define('x-username', XUsername); 8 | -------------------------------------------------------------------------------- /tests/e2e/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['cypress'], 3 | env: { 4 | mocha: true, 5 | 'cypress/globals': true 6 | }, 7 | rules: { 8 | strict: 'off' 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /tests/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/guides/guides/plugins-guide.html 2 | 3 | // if you need a custom webpack configuration you can uncomment the following import 4 | // and then use the `file:preprocessor` event 5 | // as explained in the cypress docs 6 | // https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples 7 | 8 | // const webpack = require('@cypress/webpack-preprocessor') 9 | 10 | module.exports = (on, config) => ({ 11 | ...config, 12 | fixturesFolder: 'tests/e2e/fixtures', 13 | integrationFolder: 'tests/e2e/specs', 14 | screenshotsFolder: 'tests/e2e/screenshots', 15 | videosFolder: 'tests/e2e/videos', 16 | supportFile: 'tests/e2e/support/index.js' 17 | }); 18 | -------------------------------------------------------------------------------- /tests/e2e/specs/new-todo.spec.js: -------------------------------------------------------------------------------- 1 | describe('New todo', () => { 2 | it('it should create new todo', () => { 3 | cy.visit('/'); 4 | cy.contains('h1', 'todos'); 5 | 6 | const newTodo = cy.get('.new-todo'); 7 | newTodo.type('Demo'); 8 | newTodo.type('{enter}'); 9 | 10 | cy.get('.main .todo-list .view').contains('Demo'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /tests/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue'; 2 | 3 | export default { 4 | root: 'src', 5 | plugins: [vue()] 6 | }; 7 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: '', 3 | devServer: { 4 | port: 9000 5 | }, 6 | chainWebpack: config => { 7 | config.module 8 | .rule('vue') 9 | .use('vue-loader') 10 | .loader('vue-loader') 11 | .tap(options => { 12 | options.compilerOptions = { 13 | ...(options.compilerOptions || {}), 14 | isCustomElement: tag => /^x-/.test(tag) 15 | }; 16 | options.isServerBuild = false; 17 | return options; 18 | }); 19 | } 20 | }; 21 | --------------------------------------------------------------------------------