├── static
└── .gitkeep
├── src
├── components
│ └── form
│ │ ├── Form.scss
│ │ ├── Form.js
│ │ └── Form.vue
├── main.js
├── assets
│ └── loader.svg
├── App.vue
└── resources
│ └── translations.js
├── config
├── dev.env.js
├── prod.env.js
└── index.js
├── .editorconfig
├── .gitignore
├── .babelrc
├── .postcssrc.js
├── README.md
├── index.html
└── package.json
/static/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/form/Form.scss:
--------------------------------------------------------------------------------
1 | pre {
2 | white-space: pre-line;
3 | }
4 |
5 | form {
6 | background: #efefef;
7 | padding: 2rem 2rem 1rem;
8 | }
9 |
--------------------------------------------------------------------------------
/config/dev.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const merge = require('webpack-merge')
3 | const prodEnv = require('./prod.env')
4 |
5 | module.exports = merge(prodEnv, {
6 | NODE_ENV: '"development"'
7 | })
8 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | /dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Editor directories and files
9 | .idea
10 | .vscode
11 | *.suo
12 | *.ntvs*
13 | *.njsproj
14 | *.sln
15 |
--------------------------------------------------------------------------------
/config/prod.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | NODE_ENV: '"production"',
4 | FORM_API_URL: '"https://www.mocky.io/v2/5adb5a8c2900002b003e3df1"', // Success
5 | //FORM_API_URL: '"https://www.mocky.io/v2/5ade0bf2300000272b4b29b9"', // Failure
6 | }
7 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "modules": false,
5 | "targets": {
6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
7 | }
8 | }],
9 | "stage-2"
10 | ],
11 | "plugins": ["transform-vue-jsx", "transform-runtime"]
12 | }
13 |
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | "plugins": {
5 | "postcss-import": {},
6 | "postcss-url": {},
7 | // to edit target browsers: use "browserslist" field in package.json
8 | "autoprefixer": {}
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueI18n from 'vue-i18n'
3 | import Vuelidate from 'vuelidate';
4 | import App from './App.vue'
5 | import translations from "./resources/translations";
6 |
7 | Vue.use(VueI18n);
8 | Vue.use(Vuelidate);
9 |
10 | Vue.config.formApiUrl = process.env.FORM_API_URL;
11 |
12 | const i18n = new VueI18n({
13 | locale: 'en',
14 | fallbackLocale: 'en',
15 | messages: translations
16 | })
17 |
18 | new Vue({
19 | el: '#app',
20 | i18n,
21 | render: h => h(App)
22 | })
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vue Demo Form
2 |
3 | Demo version available here: https://teroauralinna.github.io/vue-demo-form/
4 |
5 | Read more: https://auralinna.blog/post/2018/how-to-build-a-complete-form-with-vue-js
6 |
7 | ## Build Setup
8 |
9 | ``` bash
10 | # install dependencies
11 | npm install
12 |
13 | # serve with hot reload at localhost:8080
14 | npm run dev
15 |
16 | # build for production with minification
17 | npm run build
18 |
19 | # build for production and view the bundle analyzer report
20 | npm run build --report
21 | ```
22 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vue.js Demo Form
7 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/assets/loader.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
28 |
29 |
38 |
--------------------------------------------------------------------------------
/src/resources/translations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | en: {
3 | form: {
4 | firstName: "First name",
5 | lastName: "Last name",
6 | email: "Email",
7 | terms: "I accept the terms and conditions",
8 | type: "Select subscription type",
9 | additionalInfo: "Additional info",
10 | submitted: "The form is submitted!",
11 | sentInfo: "Here is the info you sent:",
12 | return: "Return to the form",
13 | submit: "Submit",
14 | submitting: "Submitting",
15 | charactersLeft: "You have {charCount} character left. | You have {charCount} characters left.",
16 | types: {
17 | free: "Free trial subscription",
18 | starter: "Starter subscription (50 € / month)",
19 | enterprise: "Enterprise subscription (250 € / month)"
20 | }
21 | },
22 | error: {
23 | invalidFields: "Following fields have an invalid or a missing value:",
24 | general: "An error happened during submit.",
25 | generalMessage: "Form sending failed due to technical problems. Try again later.",
26 | fieldRequired: "{field} is required.",
27 | fieldInvalid: "{field} is invalid or missing.",
28 | fieldMaxLength: "{field} maximum characters exceeded."
29 | }
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-demo-form",
3 | "version": "1.0.0",
4 | "description": "A Vue.js project",
5 | "author": "Tero Auralinna ",
6 | "private": true,
7 | "scripts": {
8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
9 | "start": "npm run dev",
10 | "build": "node build/build.js"
11 | },
12 | "dependencies": {
13 | "vue": "^2.5.2",
14 | "axios": "^0.18.0",
15 | "vue-i18n": "^7.6.0",
16 | "vuelidate": "^0.6.2"
17 | },
18 | "devDependencies": {
19 | "autoprefixer": "^7.1.2",
20 | "babel-core": "^6.26.3",
21 | "babel-helper-vue-jsx-merge-props": "^2.0.3",
22 | "babel-loader": "^7.1.1",
23 | "babel-plugin-syntax-jsx": "^6.18.0",
24 | "babel-plugin-transform-runtime": "^6.22.0",
25 | "babel-plugin-transform-vue-jsx": "^3.5.0",
26 | "babel-preset-env": "^1.3.2",
27 | "babel-preset-stage-2": "^6.22.0",
28 | "chalk": "^2.4.1",
29 | "copy-webpack-plugin": "^4.0.1",
30 | "css-loader": "^0.28.0",
31 | "extract-text-webpack-plugin": "^3.0.0",
32 | "file-loader": "^1.1.4",
33 | "friendly-errors-webpack-plugin": "^1.6.1",
34 | "html-webpack-plugin": "^2.30.1",
35 | "node-notifier": "^5.1.2",
36 | "node-sass": "^4.9.0",
37 | "optimize-css-assets-webpack-plugin": "^3.2.0",
38 | "ora": "^1.2.0",
39 | "portfinder": "^1.0.13",
40 | "postcss-import": "^11.0.0",
41 | "postcss-loader": "^2.0.8",
42 | "postcss-url": "^7.2.1",
43 | "rimraf": "^2.6.0",
44 | "sass-loader": "^7.0.1",
45 | "semver": "^5.3.0",
46 | "shelljs": "^0.7.6",
47 | "uglifyjs-webpack-plugin": "^1.1.1",
48 | "url-loader": "^0.5.8",
49 | "vue-loader": "^13.3.0",
50 | "vue-style-loader": "^3.0.1",
51 | "vue-template-compiler": "^2.5.2",
52 | "webpack": "^3.6.0",
53 | "webpack-bundle-analyzer": "^2.9.0",
54 | "webpack-dev-server": "^2.9.1",
55 | "webpack-merge": "^4.1.0"
56 | },
57 | "engines": {
58 | "node": ">= 6.0.0",
59 | "npm": ">= 3.0.0"
60 | },
61 | "browserslist": [
62 | "> 1%",
63 | "last 2 versions",
64 | "not ie <= 8"
65 | ]
66 | }
67 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | // Template version: 1.3.1
3 | // see http://vuejs-templates.github.io/webpack for documentation.
4 |
5 | const path = require('path')
6 |
7 | module.exports = {
8 | dev: {
9 |
10 | // Paths
11 | assetsSubDirectory: 'static',
12 | assetsPublicPath: '/',
13 | proxyTable: {},
14 |
15 | // Various Dev Server settings
16 | host: 'localhost', // can be overwritten by process.env.HOST
17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
18 | autoOpenBrowser: false,
19 | errorOverlay: true,
20 | notifyOnErrors: true,
21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
22 |
23 |
24 | /**
25 | * Source Maps
26 | */
27 |
28 | // https://webpack.js.org/configuration/devtool/#development
29 | devtool: 'cheap-module-eval-source-map',
30 |
31 | // If you have problems debugging vue-files in devtools,
32 | // set this to false - it *may* help
33 | // https://vue-loader.vuejs.org/en/options.html#cachebusting
34 | cacheBusting: true,
35 |
36 | cssSourceMap: true
37 | },
38 |
39 | build: {
40 | // Template for index.html
41 | index: path.resolve(__dirname, '../dist/index.html'),
42 |
43 | // Paths
44 | assetsRoot: path.resolve(__dirname, '../dist'),
45 | assetsSubDirectory: 'static',
46 | assetsPublicPath: '/vue-demo-form',
47 |
48 | /**
49 | * Source Maps
50 | */
51 |
52 | productionSourceMap: true,
53 | // https://webpack.js.org/configuration/devtool/#production
54 | devtool: '#source-map',
55 |
56 | // Gzip off by default as many popular static hosts such as
57 | // Surge or Netlify already gzip all static assets for you.
58 | // Before setting to `true`, make sure to:
59 | // npm install --save-dev compression-webpack-plugin
60 | productionGzip: false,
61 | productionGzipExtensions: ['js', 'css'],
62 |
63 | // Run the build command with an extra argument to
64 | // View the bundle analyzer report after build finishes:
65 | // `npm run build --report`
66 | // Set to `true` or `false` to always turn it on or off
67 | bundleAnalyzerReport: process.env.npm_config_report
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/form/Form.js:
--------------------------------------------------------------------------------
1 | import { required, email, maxLength } from 'vuelidate/lib/validators';
2 | import axios from 'axios';
3 | import Vue from 'vue';
4 |
5 | export default {
6 | name: 'app-form',
7 | data() {
8 | return {
9 | isSubmitted: false,
10 | isError: false,
11 | errorHeader: 'error.invalidFields',
12 | errors: [],
13 | types: this.getTypes(),
14 | submitting: false,
15 | form: {
16 | firstName: '',
17 | lastName: '',
18 | email: '',
19 | terms: false,
20 | type: null,
21 | additionalInfo: ''
22 | }
23 | }
24 | },
25 | methods: {
26 | submit() {
27 | this.$v.$touch();
28 | if (!this.$v.$error) {
29 | this.sendFormData();
30 | } else {
31 | this.validationError();
32 | }
33 | },
34 | enableSubmitLoader() {
35 | this.submitting = true;
36 | },
37 | disableSubmitLoader() {
38 | this.submitting = false;
39 | },
40 | sendFormData() {
41 | this.enableSubmitLoader();
42 | axios.post(Vue.config.formApiUrl, this.form).then(response => {
43 | this.submitSuccess(response);
44 | this.disableSubmitLoader();
45 | }).catch(error => {
46 | this.submitError(error);
47 | this.disableSubmitLoader();
48 | });
49 | },
50 | submitSuccess(response) {
51 | if (response.data.success) {
52 | this.isSubmitted = true;
53 | this.isError = false;
54 | } else {
55 | this.errorHeader = 'error.invalidFields';
56 | this.errors = response.data.errors;
57 | this.isError = true;
58 | }
59 | },
60 | submitError(error) {
61 | this.errorHeader = 'error.general';
62 | this.errors = [{'field': null, 'message': 'error.generalMessage'}];
63 | this.isError = true;
64 | },
65 | validationError() {
66 | this.errorHeader = 'error.invalidFields';
67 | this.errors = this.getErrors();
68 | this.isError = true;
69 | },
70 | isErrorField(field) {
71 | try {
72 | if (this.getValidationField(field).$error) {
73 | return true;
74 | }
75 | } catch (error) {}
76 |
77 | return this.errors.some(el => el.field === field);
78 | },
79 | getErrors() {
80 | let errors = [];
81 | for (const field of Object.keys(this.form)) {
82 | try {
83 | if (this.getValidationField(field).$error) {
84 | errors.push({'field': field, 'message': null});
85 | }
86 | } catch (error) {}
87 | }
88 | return errors;
89 | },
90 | getFieldClasses(field) {
91 | return { 'is-invalid': this.isErrorField(field) }
92 | },
93 | getCharactersLeft(field) {
94 | try {
95 | return this.getValidationField(field).$params.maxLength.max - this.form[field].length;
96 | } catch (error) {
97 | return 0;
98 | }
99 | },
100 | getTypes() {
101 | return [{
102 | value: 'free',
103 | label: 'form.types.free'
104 | }, {
105 | value: 'starter',
106 | label: 'form.types.starter'
107 | }, {
108 | value: 'enterprise',
109 | label: 'form.types.enterprise'
110 | }];
111 | },
112 | getValidationField(field) {
113 | if (this.$v.form[field]) {
114 | return this.$v.form[field];
115 | }
116 | throw Error('No validation for field ' + field);
117 | },
118 | onFieldBlur(field) {
119 | try {
120 | this.getValidationField(field).$touch();
121 | if (this.getValidationField(field).$error) {
122 | if (!this.errors.some(el => el.field === field)) {
123 | this.errors.push({'field': field, 'message': null});
124 | }
125 | } else {
126 | this.errors = this.errors.filter(el => el.field !== field);
127 | }
128 | } catch (error) {}
129 | },
130 | reload() {
131 | window.location = '';
132 | }
133 | },
134 | validations: {
135 | form: {
136 | email: { required, email },
137 | firstName: { required },
138 | lastName: { required },
139 | type: { required },
140 | terms: { required },
141 | additionalInfo: { maxLength: maxLength(1000) }
142 | }
143 | },
144 | watch: {
145 | errors() {
146 | this.isError = this.errors.length > 0 ? true : false;
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/components/form/Form.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
64 |
65 |
66 | {{ $t('form.submitted' ) }}
67 |
68 |
69 |
{{ $t('form.sentInfo' ) }}
70 |
71 | {{form}}
72 |
73 |
74 |
75 | {{ $t('form.return' ) }}
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------