├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .stylelintrc ├── README.md ├── bin ├── dev-server.js ├── lib │ ├── open-browser.js │ └── open-chrome.applescript └── spa-server.js ├── dist └── _redirects ├── package-lock.json ├── package.json ├── src ├── components │ ├── App.vue │ └── FormDate.vue └── main.js ├── static └── index.html └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ] 9 | ], 10 | "plugins": [ 11 | "@babel/plugin-syntax-dynamic-import" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.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: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "parserOptions": { 8 | "parser": "babel-eslint", 9 | "sourceType": "module" 10 | }, 11 | "extends": [ 12 | "@avalanche/eslint-config", 13 | "plugin:vue/recommended" 14 | ], 15 | "rules": { 16 | "vue/require-default-prop": 0 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.log 5 | *.orig 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.tgz 10 | *.vi 11 | *.zip 12 | *~ 13 | 14 | # OS or Editor folders 15 | ._* 16 | .cache 17 | .DS_Store 18 | .idea 19 | .project 20 | .settings 21 | .tmproj 22 | *.esproj 23 | *.sublime-project 24 | *.sublime-workspace 25 | nbproject 26 | Thumbs.db 27 | 28 | # Folders to ignore 29 | dist/* 30 | !dist/_redirects 31 | node_modules 32 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@avalanche/stylelint-config", 3 | "processors": [ 4 | "@mapbox/stylelint-processor-arbitrary-tags" 5 | ], 6 | "rules": { 7 | "max-nesting-depth": 4, 8 | "no-empty-source": null 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building a Date Input Component with Vue.js 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 Date Input Component with Vue.js](https://markus.oberlehner.net/blog/building-a-date-input-component-with-vue/) 7 | 8 | ## Build Setup 9 | 10 | ``` bash 11 | # Install dependencies. 12 | npm install 13 | 14 | # Serve with hot reload. 15 | npm start 16 | 17 | # Build for production with minification. 18 | npm run build 19 | 20 | # Serve production build. 21 | npm run serve:production 22 | ``` 23 | 24 | ## About 25 | 26 | ### Author 27 | 28 | Markus Oberlehner 29 | Website: https://markus.oberlehner.net 30 | Twitter: https://twitter.com/MaOberlehner 31 | PayPal.me: https://paypal.me/maoberlehner 32 | Patreon: https://www.patreon.com/maoberlehner 33 | 34 | ### License 35 | 36 | MIT 37 | -------------------------------------------------------------------------------- /bin/dev-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const serve = require(`webpack-serve`); 3 | 4 | const openBrowser = require(`./lib/open-browser`); 5 | const config = require(`../webpack.config`); 6 | 7 | serve({ config, clipboard: false }).then((server) => { 8 | server.on(`listening`, () => { 9 | openBrowser(`http://${server.options.host}:${server.options.port}`); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /bin/lib/open-browser.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require(`child_process`); 2 | const opn = require(`opn`); 3 | 4 | module.exports = function openBrowser(url) { 5 | // If we're on OS X, we can try opening 6 | // Chrome with AppleScript. This lets us reuse an 7 | // existing tab when possible instead of creating a new one. 8 | // See: https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-shared-utils/lib/openBrowser.js 9 | const browser = process.env.BROWSER; 10 | const shouldTryOpenChromeWithAppleScript = 11 | process.platform === `darwin` && 12 | (typeof browser !== `string` || browser === `google chrome`); 13 | 14 | if (shouldTryOpenChromeWithAppleScript) { 15 | try { 16 | execSync(`ps cax | grep "Google Chrome"`); 17 | execSync(`osascript open-chrome.applescript "${encodeURI(url)}"`, { 18 | cwd: __dirname, 19 | stdio: `ignore`, 20 | }); 21 | 22 | return true; 23 | } catch (error) { 24 | // Ignore errors. 25 | } 26 | } 27 | 28 | try { 29 | opn(url).catch(() => {}); // Prevent `unhandledRejection` error. 30 | 31 | return true; 32 | } catch (error) { 33 | return false; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /bin/lib/open-chrome.applescript: -------------------------------------------------------------------------------- 1 | (* 2 | Copyright (c) 2015-present, Facebook, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file at 6 | https://github.com/facebookincubator/create-react-app/blob/master/LICENSE 7 | *) 8 | 9 | property targetTab: null 10 | property targetTabIndex: -1 11 | property targetWindow: null 12 | 13 | on run argv 14 | set theURL to item 1 of argv 15 | 16 | tell application "Chrome" 17 | 18 | if (count every window) = 0 then 19 | make new window 20 | end if 21 | 22 | -- 1: Looking for tab running debugger 23 | -- then, Reload debugging tab if found 24 | -- then return 25 | set found to my lookupTabWithUrl(theURL) 26 | if found then 27 | set targetWindow's active tab index to targetTabIndex 28 | tell targetTab to reload 29 | tell targetWindow to activate 30 | set index of targetWindow to 1 31 | return 32 | end if 33 | 34 | -- 2: Looking for Empty tab 35 | -- In case debugging tab was not found 36 | -- We try to find an empty tab instead 37 | set found to my lookupTabWithUrl("chrome://newtab/") 38 | if found then 39 | set targetWindow's active tab index to targetTabIndex 40 | set URL of targetTab to theURL 41 | tell targetWindow to activate 42 | return 43 | end if 44 | 45 | -- 3: Create new tab 46 | -- both debugging and empty tab were not found 47 | -- make a new tab with url 48 | tell window 1 49 | activate 50 | make new tab with properties {URL:theURL} 51 | end tell 52 | end tell 53 | end run 54 | 55 | -- Function: 56 | -- Lookup tab with given url 57 | -- if found, store tab, index, and window in properties 58 | -- (properties were declared on top of file) 59 | on lookupTabWithUrl(lookupUrl) 60 | tell application "Chrome" 61 | -- Find a tab with the given url 62 | set found to false 63 | set theTabIndex to -1 64 | repeat with theWindow in every window 65 | set theTabIndex to 0 66 | repeat with theTab in every tab of theWindow 67 | set theTabIndex to theTabIndex + 1 68 | if (theTab's URL as string) contains lookupUrl then 69 | -- assign tab, tab index, and window to properties 70 | set targetTab to theTab 71 | set targetTabIndex to theTabIndex 72 | set targetWindow to theWindow 73 | set found to true 74 | exit repeat 75 | end if 76 | end repeat 77 | 78 | if found then 79 | exit repeat 80 | end if 81 | end repeat 82 | end tell 83 | return found 84 | end lookupTabWithUrl 85 | -------------------------------------------------------------------------------- /bin/spa-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const compression = require(`compression`); 3 | const express = require(`express`); 4 | const path = require(`path`); 5 | 6 | const openBrowser = require(`./lib/open-browser`); 7 | 8 | const app = express(); 9 | const publicPath = path.join(process.cwd(), `dist`); 10 | const port = 5000; 11 | 12 | app.use(compression()); 13 | app.use(`/`, express.static(publicPath, { index: false })); 14 | app.get(`/*`, (request, response) => { 15 | response.sendFile(`${publicPath}/index.html`); 16 | }); 17 | 18 | app.listen(port); 19 | 20 | // eslint-disable-next-line no-console 21 | console.log(`Server started!`); 22 | // eslint-disable-next-line no-console 23 | console.log(`http://localhost:${port}`); 24 | 25 | openBrowser(`http://localhost:${port}`); 26 | -------------------------------------------------------------------------------- /dist/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "building-a-date-input-component-with-vue", 3 | "version": "0.1.0", 4 | "author": "Markus Oberlehner", 5 | "homepage": "https://github.com/maoberlehner/building-a-date-input-component-with-vue", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "start": "npm run serve:dev", 10 | "build": "cross-env NODE_ENV=production webpack", 11 | "serve:dev": "cross-env NODE_ENV=development node bin/dev-server.js", 12 | "serve:production": "cross-env NODE_ENV=production node bin/spa-server.js", 13 | "lint:scripts": "eslint --ext .js,.vue --ignore-path .gitignore .", 14 | "lint:styles": "stylelint --syntax scss 'src/**/*.+(scss|vue)'", 15 | "lint": "npm run lint:scripts && npm run lint:styles" 16 | }, 17 | "dependencies": { 18 | "normalize.css": "^8.0.0", 19 | "reset-css": "^3.0.0", 20 | "vue": "^2.5.16" 21 | }, 22 | "devDependencies": { 23 | "@avalanche/eslint-config": "^2.0.0", 24 | "@avalanche/stylelint-config": "^0.1.2", 25 | "@babel/core": "^7.0.0-beta.44", 26 | "@babel/plugin-syntax-dynamic-import": "^7.0.0-beta.44", 27 | "@babel/preset-env": "^7.0.0-beta.44", 28 | "@mapbox/stylelint-processor-arbitrary-tags": "^0.2.0", 29 | "babel-eslint": "^8.2.3", 30 | "babel-loader": "^8.0.0-beta.2", 31 | "compression": "^1.7.2", 32 | "cross-env": "^5.1.4", 33 | "css-loader": "^0.28.11", 34 | "eslint": "^4.19.1", 35 | "eslint-plugin-import": "^2.11.0", 36 | "eslint-plugin-vue": "^4.4.0", 37 | "express": "^4.16.3", 38 | "html-webpack-plugin": "^3.2.0", 39 | "mini-css-extract-plugin": "^0.4.0", 40 | "node-sass": "^4.8.3", 41 | "node-sass-magic-importer": "^5.1.2", 42 | "opn": "^5.3.0", 43 | "optimize-css-assets-webpack-plugin": "^4.0.0", 44 | "sass-loader": "^7.0.1", 45 | "stylelint": "^9.2.0", 46 | "uglifyjs-webpack-plugin": "^1.2.4", 47 | "vue-loader": "^15.0.0-beta.7", 48 | "vue-template-compiler": "^2.5.16", 49 | "webpack": "^4.5.0", 50 | "webpack-cli": "^2.0.14", 51 | "webpack-serve": "^0.3.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 36 | -------------------------------------------------------------------------------- /src/components/FormDate.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 96 | 97 | 150 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import App from './components/App.vue'; 4 | 5 | Vue.config.productionTip = false; 6 | 7 | new Vue({ 8 | render: h => h(App), 9 | }).$mount(`#app`); 10 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Building a Date Input Component with Vue.js 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { VueLoaderPlugin } = require(`vue-loader`); 2 | const nodeSassMagicImporter = require(`node-sass-magic-importer`); 3 | const path = require(`path`); 4 | 5 | const HtmlWebpackPlugin = require(`html-webpack-plugin`); 6 | const MiniCssExtractPlugin = require(`mini-css-extract-plugin`); 7 | const OptimizeCSSAssetsPlugin = require(`optimize-css-assets-webpack-plugin`); 8 | const UglifyJsPlugin = require(`uglifyjs-webpack-plugin`); 9 | 10 | const env = process.env.NODE_ENV; 11 | const minify = env === `production`; 12 | const sourceMap = env === `development`; 13 | 14 | const config = { 15 | entry: path.join(__dirname, `src`, `main.js`), 16 | mode: env, 17 | output: { 18 | publicPath: `/`, 19 | }, 20 | optimization: { 21 | splitChunks: { 22 | // Must be specified for HtmlWebpackPlugin to work correctly. 23 | // See: https://github.com/jantimon/html-webpack-plugin/issues/882 24 | chunks: `all`, 25 | }, 26 | }, 27 | devtool: sourceMap ? `cheap-module-eval-source-map` : undefined, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.vue$/, 32 | loader: `vue-loader`, 33 | }, 34 | { 35 | test: /\.js$/, 36 | loader: `babel-loader`, 37 | include: [path.join(__dirname, `src`)], 38 | }, 39 | { 40 | test: /\.scss$/, 41 | use: [ 42 | `vue-style-loader`, 43 | { 44 | loader: `css-loader`, 45 | options: { 46 | sourceMap, 47 | }, 48 | }, 49 | { 50 | loader: `sass-loader`, 51 | options: { 52 | importer: nodeSassMagicImporter(), 53 | sourceMap, 54 | }, 55 | }, 56 | ], 57 | }, 58 | ], 59 | }, 60 | plugins: [ 61 | new VueLoaderPlugin(), 62 | new HtmlWebpackPlugin({ 63 | filename: path.join(__dirname, `dist`, `index.html`), 64 | template: path.join(__dirname, `static`, `index.html`), 65 | inject: true, 66 | minify: minify ? { 67 | removeComments: true, 68 | collapseWhitespace: true, 69 | removeAttributeQuotes: true, 70 | // More options: 71 | // https://github.com/kangax/html-minifier#options-quick-reference 72 | } : false, 73 | }), 74 | ], 75 | }; 76 | 77 | if (minify) { 78 | config.optimization.minimizer = [ 79 | new OptimizeCSSAssetsPlugin(), 80 | // Enabled by default in production mode if 81 | // the `minimizer` option isn't overridden. 82 | new UglifyJsPlugin({ 83 | cache: true, 84 | parallel: true, 85 | }), 86 | ]; 87 | } 88 | 89 | if (env !== `development`) { 90 | config.plugins.push(new MiniCssExtractPlugin()); 91 | 92 | const sassLoader = config.module.rules.find(({ test }) => test.test(`.scss`)); 93 | // Replace the `vue-style-loader` with 94 | // the MiniCssExtractPlugin loader. 95 | sassLoader.use[0] = MiniCssExtractPlugin.loader; 96 | } 97 | 98 | module.exports = config; 99 | --------------------------------------------------------------------------------