├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── Interfaces │ └── Posts.ts ├── api │ └── index.ts ├── assets │ ├── logo.png │ └── styles │ │ ├── app.scss │ │ ├── common.scss │ │ ├── element.scss │ │ ├── modules │ │ └── card.scss │ │ └── responsive.scss ├── components │ ├── HelloWorld.vue │ └── Navbar.vue ├── main.ts ├── router │ └── index.ts ├── shims-tsx.d.ts ├── shims-vue.d.ts ├── store │ ├── index.ts │ └── modules │ │ ├── globalValues.ts │ │ └── posts.ts ├── utils │ └── utils.ts └── views │ ├── 404.vue │ ├── About.vue │ ├── Article.vue │ ├── Edit.vue │ ├── Home.vue │ └── ModifyBlog.vue ├── tests └── unit │ └── example.spec.ts └── tsconfig.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | "plugin:vue/essential", 8 | "eslint:recommended", 9 | "@vue/typescript/recommended", 10 | "@vue/prettier", 11 | "@vue/prettier/@typescript-eslint" 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 2020 15 | }, 16 | rules: { 17 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 18 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" 19 | }, 20 | overrides: [ 21 | { 22 | files: [ 23 | "**/__tests__/*.{j,t}s?(x)", 24 | "**/tests/unit/**/*.spec.{j,t}s?(x)" 25 | ], 26 | env: { 27 | jest: true 28 | } 29 | } 30 | ] 31 | }; 32 | -------------------------------------------------------------------------------- /.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.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ts-vue-sample 2 | 3 | Mock server used https://jsonplaceholder.typicode.com/ 4 | 5 | **This is a simple Blog (with CRUD Operation) to showcase the usage of:** 6 | 7 | - Vue 8 | - Vuex and Modules 9 | - vue-router 10 | - Typescript (Class based API) 11 | - Axios 12 | - Code splitting/ Lazy loading 13 | 14 | ### Styling 15 | 16 | - SASS/SCSS pre-processor is used for CSS styling. 17 | - Element UI library is used to enhance the look and feel of the app. 18 | - Basic responsive design 19 | - CSS Resets 20 | 21 | ### Vuex 22 | 23 | Using Vuex to store some common data and accessing them using Helpers in components. Using modular approach by creating specific modules to store and access data. 24 | 25 | ### Typing 26 | 27 | Class based API approach is used here. Using `vue-property-decorator` to decorate modules in Component files. 28 | Using `vuex-module-decorators` to decorate Vuex modules and `vuex-class` library to access the Vuex properties such as `state`, `actions`, `getters` etc in components. 29 | 30 | ## Project setup 31 | 32 | ``` 33 | npm install 34 | ``` 35 | 36 | ### Compiles and hot-reloads for development 37 | 38 | ``` 39 | npm run serve 40 | ``` 41 | 42 | ### Compiles and minifies for production 43 | 44 | ``` 45 | npm run build 46 | ``` 47 | 48 | ### Run your unit tests 49 | 50 | ``` 51 | npm run test:unit 52 | ``` 53 | 54 | ### Lints and fixes files 55 | 56 | ``` 57 | npm run lint 58 | ``` 59 | 60 | ### Customize configuration 61 | 62 | See [Configuration Reference](https://cli.vuejs.org/config/). 63 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"] 3 | }; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "@vue/cli-plugin-unit-jest/presets/typescript-and-babel" 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-vue-sample", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "test:unit": "vue-cli-service test:unit", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.19.2", 13 | "core-js": "^3.6.4", 14 | "element-ui": "^2.13.0", 15 | "vue": "^2.6.11", 16 | "vue-class-component": "^7.2.2", 17 | "vue-property-decorator": "^8.3.0", 18 | "vue-router": "^3.1.5", 19 | "vuex": "^3.1.2", 20 | "vuex-class": "^0.3.2", 21 | "vuex-module-decorators": "^0.16.1" 22 | }, 23 | "devDependencies": { 24 | "@types/jest": "^24.0.19", 25 | "@typescript-eslint/eslint-plugin": "^2.18.0", 26 | "@typescript-eslint/parser": "^2.18.0", 27 | "@vue/cli-plugin-babel": "^4.2.0", 28 | "@vue/cli-plugin-eslint": "^4.2.0", 29 | "@vue/cli-plugin-router": "^4.2.0", 30 | "@vue/cli-plugin-typescript": "^4.2.0", 31 | "@vue/cli-plugin-unit-jest": "^4.2.0", 32 | "@vue/cli-plugin-vuex": "^4.2.0", 33 | "@vue/cli-service": "^4.2.0", 34 | "@vue/eslint-config-prettier": "^6.0.0", 35 | "@vue/eslint-config-typescript": "^5.0.1", 36 | "@vue/test-utils": "1.0.0-beta.31", 37 | "eslint": "^6.7.2", 38 | "eslint-plugin-prettier": "^3.1.1", 39 | "eslint-plugin-vue": "^6.1.2", 40 | "node-sass": "^4.12.0", 41 | "prettier": "^1.19.1", 42 | "sass-loader": "^8.0.2", 43 | "typescript": "~3.7.5", 44 | "vue-template-compiler": "^2.6.11" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preetishhs/Vue-typescript-example/07dda167dd57841557f565a7eedb2691c7adf93b/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 22 | 39 | -------------------------------------------------------------------------------- /src/Interfaces/Posts.ts: -------------------------------------------------------------------------------- 1 | interface Post { 2 | title: string 3 | id: number 4 | body: string 5 | } 6 | 7 | export { Post } 8 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import store from '@/store' 3 | 4 | const config = { 5 | baseURL: 'https://jsonplaceholder.typicode.com', 6 | headers: { Accept: 'application/json' } 7 | } 8 | 9 | const call = axios.create(config) 10 | call.interceptors.request.use(request => { 11 | store.dispatch('globalValues/setLoading', true) 12 | return request 13 | }) 14 | call.interceptors.response.use( 15 | response => { 16 | store.dispatch('globalValues/setLoading', false) 17 | return response 18 | }, 19 | error => { 20 | store.dispatch('globalValues/setLoading', false) 21 | return error 22 | } 23 | ) 24 | export default call 25 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preetishhs/Vue-typescript-example/07dda167dd57841557f565a7eedb2691c7adf93b/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import 'responsive'; 2 | @import 'common'; 3 | @import './modules/card.scss'; 4 | -------------------------------------------------------------------------------- /src/assets/styles/common.scss: -------------------------------------------------------------------------------- 1 | .page-title { 2 | text-align: center; 3 | font-size: 32px; 4 | padding: 4rem; 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/styles/element.scss: -------------------------------------------------------------------------------- 1 | $--font-path: '~element-ui/lib/theme-chalk/fonts'; 2 | @import '~element-ui/packages/theme-chalk/src/index'; 3 | -------------------------------------------------------------------------------- /src/assets/styles/modules/card.scss: -------------------------------------------------------------------------------- 1 | .card-container { 2 | display: flex; 3 | padding: 2rem 5rem; 4 | flex-wrap: wrap; 5 | justify-content: center; 6 | } 7 | .article { 8 | width: 40rem; 9 | height: 40rem; 10 | margin: 1.5rem; 11 | position: relative; 12 | &:hover { 13 | transform: scale(1.02); 14 | transition: all 0.2s; 15 | box-shadow: 0.1rem 0.1rem 1.4rem #ccc; 16 | } 17 | .image { 18 | height: 30rem; 19 | width: 100%; 20 | object-fit: cover; 21 | } 22 | .title { 23 | font-size: 2.4rem; 24 | font-weight: bold; 25 | padding: 1rem; 26 | height: 10rem; 27 | text-align: center; 28 | } 29 | } 30 | 31 | .overlay { 32 | display: flex; 33 | flex-flow: column; 34 | padding: 5rem; 35 | align-items: center; 36 | justify-content: center; 37 | z-index: 5; 38 | position: absolute; 39 | width: 100%; 40 | height: 100%; 41 | background-image: linear-gradient( 42 | to right, 43 | rgba(0, 0, 0, 0.7), 44 | rgba(0, 0, 0, 0.7) 45 | ); 46 | .el-button { 47 | width: 80%; 48 | margin: 2rem; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/assets/styles/responsive.scss: -------------------------------------------------------------------------------- 1 | @mixin respond($breakpoint) { 2 | @if $breakpoint == big-desktop { 3 | @media (min-width: 1800px) { 4 | @content; 5 | } 6 | } 7 | @if $breakpoint == small-desktop { 8 | @media (max-width: 1200px) { 9 | @content; 10 | } 11 | } 12 | @if $breakpoint == tab { 13 | @media (max-width: 992px) { 14 | @content; 15 | } 16 | } 17 | @if $breakpoint == phone { 18 | @media (max-width: 768px) { 19 | @content; 20 | } 21 | } 22 | } 23 | 24 | html { 25 | font-size: 62.5%; 26 | /* 16px (browser default) * 62.5 = 10px 27 | now we can use rem units at all places 28 | 1rem = 10px; 29 | 1.6rem = 16px; 30 | */ 31 | @include respond(small-desktop) { 32 | font-size: 56.25%; 33 | } 34 | @include respond(tab) { 35 | font-size: 52%; 36 | } 37 | @include respond(phone) { 38 | font-size: 50%; 39 | } 40 | @include respond(big-desktop) { 41 | font-size: 70%; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 120 | 121 | 129 | 130 | 131 | 147 | -------------------------------------------------------------------------------- /src/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 21 | 26 | 27 | 65 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | 6 | // Element UI 7 | import ElementUI from 'element-ui' 8 | import '@/assets/styles/element.scss' 9 | Vue.use(ElementUI) 10 | 11 | //styles 12 | import '@/assets/styles/app.scss' 13 | Vue.config.productionTip = false 14 | 15 | new Vue({ 16 | router, 17 | store, 18 | render: h => h(App) 19 | }).$mount('#app') 20 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Home from '@/views/Home.vue' 4 | 5 | Vue.use(VueRouter) 6 | 7 | const routes = [ 8 | { 9 | path: '/', 10 | name: 'home', 11 | component: Home 12 | }, 13 | { 14 | path: '/article/:id', 15 | name: 'article', 16 | component: () => 17 | import(/* webpackChunkName: "ModifyBlog" */ '@/views/Article.vue') 18 | }, 19 | { 20 | path: '/modify', 21 | name: 'modify', 22 | component: () => 23 | import(/* webpackChunkName: "ModifyBlog" */ '@/views/ModifyBlog.vue') 24 | }, 25 | { 26 | path: '/create', 27 | name: 'create', 28 | component: () => 29 | import(/* webpackChunkName: "CreateItem" */ '@/views/Edit.vue') 30 | }, 31 | { 32 | path: '/edit/:id', 33 | name: 'edit', 34 | component: () => 35 | import(/* webpackChunkName: "EditItem" */ '@/views/Edit.vue') 36 | }, 37 | { 38 | path: '/about', 39 | name: 'about', 40 | component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue') 41 | }, 42 | { 43 | path: '*', 44 | name: 'notFound', 45 | component: () => import(/* webpackChunkName: "about" */ '@/views/404.vue') 46 | } 47 | ] 48 | 49 | const router = new VueRouter({ 50 | mode: 'history', 51 | base: process.env.BASE_URL, 52 | routes 53 | }) 54 | 55 | export default router 56 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from "vue"; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue"; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import globalValues from '@/store/modules/globalValues' 4 | import posts from '@/store/modules/posts' 5 | Vue.use(Vuex) 6 | const store = new Vuex.Store({ 7 | modules: { 8 | globalValues, 9 | posts 10 | } 11 | }) 12 | export default store 13 | -------------------------------------------------------------------------------- /src/store/modules/globalValues.ts: -------------------------------------------------------------------------------- 1 | import { VuexModule, Module, Mutation, Action } from 'vuex-module-decorators' 2 | @Module({ namespaced: true }) 3 | class GlobalValues extends VuexModule { 4 | public isLoading?: boolean = false 5 | @Mutation 6 | public updateLoading(newVal: boolean): void { 7 | this.isLoading = newVal 8 | } 9 | @Action 10 | public setLoading(newVal: boolean): void { 11 | this.context.commit('updateLoading', newVal) 12 | } 13 | } 14 | export default GlobalValues 15 | -------------------------------------------------------------------------------- /src/store/modules/posts.ts: -------------------------------------------------------------------------------- 1 | import { VuexModule, Module, Mutation, Action } from 'vuex-module-decorators' 2 | import { notify } from '@/utils/utils' 3 | import api from '@/api' 4 | import { Post } from '@/Interfaces/Posts' 5 | 6 | @Module({ namespaced: true }) 7 | class Posts extends VuexModule { 8 | public list: Array = [] 9 | public fetchedList = false 10 | public post: Post = { 11 | title: '', 12 | id: 0, 13 | body: '' 14 | } 15 | 16 | @Mutation 17 | public saveList(data: Array): void { 18 | this.fetchedList = true 19 | this.list = [...this.list, ...data.slice(0, 6)] 20 | } 21 | @Action 22 | public getAllPosts(): Promise { 23 | if (this.fetchedList) { 24 | return Promise.resolve(true) 25 | } else { 26 | return api 27 | .get('/posts') 28 | .then(response => { 29 | this.context.commit('saveList', response.data) 30 | return true 31 | }) 32 | .catch(() => { 33 | notify({ 34 | title: 'Error', 35 | type: 'error', 36 | message: 'Could not fetch the list' 37 | }) 38 | return false 39 | }) 40 | } 41 | } 42 | 43 | @Mutation 44 | public save(data: Post): void { 45 | this.post = data 46 | } 47 | @Action 48 | public async getPost(id: number): Promise { 49 | if (this.list.length !== 0) { 50 | const fetched = await this.list.filter(item => { 51 | return item.id === id 52 | }) 53 | 54 | if (fetched.length) { 55 | return Promise.resolve(fetched[0]) 56 | } else { 57 | return Promise.reject(false) 58 | } 59 | } else { 60 | return api 61 | .get(`/posts/${id}`) 62 | .then(response => { 63 | this.context.commit('save', response.data) 64 | return response.data 65 | }) 66 | .catch(() => { 67 | notify({ 68 | title: 'Error', 69 | type: 'error', 70 | message: 'Could not fetch Article' 71 | }) 72 | return false 73 | }) 74 | } 75 | } 76 | 77 | @Mutation 78 | public edit(data: Post): void { 79 | this.list.map(item => { 80 | if (item.id === data.id) { 81 | item = data 82 | } 83 | }) 84 | } 85 | @Action 86 | public async editPost(data: Post): Promise { 87 | return api 88 | .put(`/posts/${data.id}`, { 89 | data 90 | }) 91 | .then(() => { 92 | notify({ 93 | title: 'Success', 94 | type: 'success', 95 | message: 'Successfully edited' 96 | }) 97 | this.context.commit('edit', data) 98 | return true 99 | }) 100 | .catch(() => { 101 | notify({ 102 | title: 'Error', 103 | type: 'error', 104 | message: 'Could not edit' 105 | }) 106 | return false 107 | }) 108 | } 109 | @Mutation 110 | public create(newPost: Post): void { 111 | this.list.unshift(newPost) 112 | } 113 | @Action 114 | public async createPost(data: Post): Promise { 115 | return api 116 | .post('/posts', { 117 | data 118 | }) 119 | .then(response => { 120 | notify({ 121 | title: 'Success', 122 | type: 'success', 123 | message: 'Successfully created an Article' 124 | }) 125 | response.data.data.id = response.data.id 126 | this.context.commit('create', response.data.data) 127 | return true 128 | }) 129 | .catch(() => { 130 | notify({ 131 | title: 'Error', 132 | type: 'error', 133 | message: 'Could not create' 134 | }) 135 | return false 136 | }) 137 | } 138 | 139 | @Mutation 140 | public delete(id: number): void { 141 | this.list = this.list.filter(item => { 142 | return item.id !== id 143 | }) 144 | } 145 | @Action 146 | public async deletePost(id: number): Promise { 147 | return api 148 | .delete(`/posts/${id}`) 149 | .then(() => { 150 | notify({ 151 | title: 'Success', 152 | type: 'success', 153 | message: 'Successfully deleted' 154 | }) 155 | this.context.commit('delete', id) 156 | }) 157 | .catch(() => { 158 | notify({ 159 | title: 'Error', 160 | type: 'error', 161 | message: 'Could not delete' 162 | }) 163 | }) 164 | } 165 | } 166 | export default Posts 167 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { Notification } from 'element-ui' 2 | 3 | interface NotifyObj { 4 | title: string 5 | message: string 6 | type: 'success' | 'warning' | 'info' | 'error' | undefined 7 | } 8 | function notify(data: NotifyObj) { 9 | Notification({ 10 | title: data.title, 11 | message: data.message, 12 | type: data.type, 13 | offset: 100, 14 | duration: 5000 15 | }) 16 | } 17 | 18 | export { notify } 19 | -------------------------------------------------------------------------------- /src/views/404.vue: -------------------------------------------------------------------------------- 1 | 6 | 16 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preetishhs/Vue-typescript-example/07dda167dd57841557f565a7eedb2691c7adf93b/src/views/About.vue -------------------------------------------------------------------------------- /src/views/Article.vue: -------------------------------------------------------------------------------- 1 | 17 | 46 | 68 | -------------------------------------------------------------------------------- /src/views/Edit.vue: -------------------------------------------------------------------------------- 1 | 21 | 66 | 76 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 23 | 39 | 45 | -------------------------------------------------------------------------------- /src/views/ModifyBlog.vue: -------------------------------------------------------------------------------- 1 | 30 | 56 | 62 | -------------------------------------------------------------------------------- /tests/unit/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | 3 | describe('HelloWorld.vue', () => { 4 | it('Sanity test', () => { 5 | expect(1).toBe(1) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": ["webpack-env", "jest"], 15 | "paths": { 16 | "@/*": ["src/*"] 17 | }, 18 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 19 | }, 20 | "include": [ 21 | "src/**/*.ts", 22 | "src/**/*.tsx", 23 | "src/**/*.vue", 24 | "tests/**/*.ts", 25 | "tests/**/*.tsx" 26 | ], 27 | "exclude": ["node_modules"] 28 | } 29 | --------------------------------------------------------------------------------