├── .gitignore
├── README.md
├── babel.config.js
├── docs
└── images
│ └── screenshot.jpg
├── package-lock.json
├── package.json
├── prettier.config.js
├── public
├── favicon.ico
└── index.html
└── src
├── App.vue
├── assets
├── img
│ └── bg.svg
└── styles
│ └── main.css
├── components
├── ComponentTypes.js
├── FormConfigProvider.vue
├── FormElements
│ ├── FieldError.vue
│ ├── FieldGroup.vue
│ ├── FieldLabel.vue
│ ├── Fields
│ │ ├── CheckBox.vue
│ │ ├── InputBox.vue
│ │ ├── RadioButton.vue
│ │ └── TextArea.vue
│ ├── FormNav.vue
│ ├── FormProgress.vue
│ └── FormResult.vue
├── FormTemplate.vue
└── Transitions
│ ├── DataDrivenTransition.vue
│ └── TypeBasedTransition.vue
├── config
└── formConfig.json
├── directives
└── index.js
├── main.js
├── mixins
└── formMixin.js
└── store
├── form
└── lead.js
└── index.js
/.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # This repository has been archived.
2 |
3 | ## Visit **[Advanced Vue 3 Form](https://github.com/Krutie/advanced-vue3-form)** repo for the latest implementation of the similar form.
4 |
5 | -----
6 |
7 | ### An Interactive and Distraction-free Form with Vue
8 |
9 | This is a supporting GitHub repository for **[Building an Interactive and Distraction-free Form with Vue](https://medium.com/vue-mastery/building-an-interactive-and-distraction-free-form-with-vue-bfe23907e981)** article.
10 |
11 | Learn how to build an interactive and distraction-free form using advanced concepts of the Vue.js framework and other supporting libraries for form validation and animations.
12 |
13 | ### Take a look at the [Demo](http://distraction-free-vue-form.surge.sh/).
14 |
15 | 
16 |
17 | ## Project setup
18 |
19 | ```bash
20 | npm install
21 | ```
22 |
23 | ### Compiles and hot-reloads for development
24 |
25 | ```
26 | npm run serve
27 | ```
28 |
29 | ### Compiles and minifies for production
30 |
31 | ```
32 | npm run build
33 | ```
34 |
35 | ### Run your tests
36 |
37 | ```
38 | npm run test
39 | ```
40 |
41 | ### Lints and fixes files
42 |
43 | ```
44 | npm run lint
45 | ```
46 |
47 | ### Customize configuration
48 |
49 | See [Configuration Reference](https://cli.vuejs.org/config/).
50 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/docs/images/screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Krutie/distraction-free-vue-form/a6a25a0f9270958d5fb2fb588c9d79b6a61fc226/docs/images/screenshot.jpg
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "distraction-free-vue-form",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "core-js": "^2.6.5",
12 | "gsap": "^2.1.3",
13 | "vee-validate": "^2.2.11",
14 | "vue": "^2.6.10",
15 | "vuex": "^3.1.1"
16 | },
17 | "devDependencies": {
18 | "@vue/cli-plugin-babel": "^3.8.0",
19 | "@vue/cli-plugin-eslint": "^3.8.0",
20 | "@vue/cli-service": "^3.8.0",
21 | "babel-eslint": "^10.0.1",
22 | "eslint": "^5.16.0",
23 | "eslint-plugin-vue": "^5.0.0",
24 | "vue-template-compiler": "^2.6.10"
25 | },
26 | "eslintConfig": {
27 | "root": true,
28 | "env": {
29 | "node": true
30 | },
31 | "extends": [
32 | "plugin:vue/essential",
33 | "eslint:recommended"
34 | ],
35 | "rules": {},
36 | "parserOptions": {
37 | "parser": "babel-eslint"
38 | }
39 | },
40 | "postcss": {
41 | "plugins": {
42 | "autoprefixer": {}
43 | }
44 | },
45 | "browserslist": [
46 | "> 1%",
47 | "last 2 versions"
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 120,
3 | tabWidth: 2,
4 | semi: false
5 | }
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Krutie/distraction-free-vue-form/a6a25a0f9270958d5fb2fb588c9d79b6a61fc226/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | advanced-vue-form
11 |
12 |
13 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Advanced Vue.js Form
4 |
5 |
6 |
7 |
8 |
18 |
22 |
--------------------------------------------------------------------------------
/src/assets/img/bg.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
--------------------------------------------------------------------------------
/src/assets/styles/main.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css?family=Rubik:300,400,500,700&display=swap");
2 | body {
3 | margin: 0;
4 | }
5 | #app {
6 | font-family: "Rubik", sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | color: #2c3e50;
10 | background-image: url("../img/bg.svg");
11 | background-position: bottom;
12 | background-size: cover;
13 | height: 100vh;
14 | background-repeat: no-repeat;
15 | }
16 |
17 | /* Logo Title - Advanced Vue.js Form */
18 | .logo {
19 | position: fixed;
20 | left: 0;
21 | right: 0;
22 | }
23 | .logo h1 {
24 | font-weight: 500;
25 | font-size: 1.7em;
26 | text-align: center;
27 | user-select: none;
28 | }
29 |
30 | /* HTML Tags */
31 | h1 {
32 | margin: 15px;
33 | font-weight: 300;
34 | }
35 | h2 {
36 | font-size: 2em;
37 | margin-bottom: 0.75em;
38 | }
39 |
40 | h2:not(:first-child) {
41 | margin-top: 0.75em;
42 | }
43 | input[type="checkbox"] {
44 | }
45 |
46 | *:focus {
47 | outline: none;
48 | }
49 |
50 | input[type="text"],
51 | textarea {
52 | border-color: transparent;
53 | border-bottom: 1px dashed #95c3c4 !important;
54 | height: 30px;
55 | width: 75%;
56 | font-size: 1.5em;
57 | border-radius: 3px;
58 | background-color: transparent;
59 | }
60 | textarea {
61 | height: 120px;
62 | width: 100%;
63 | }
64 |
65 | /* Form */
66 | .field-group {
67 | position: absolute;
68 | top: Calc(25% - 100px);
69 | width: 100%;
70 | }
71 | .field-label {
72 | display: block;
73 | font-size: 1.7em;
74 | padding-bottom: 15px;
75 | }
76 | .field-area {
77 | padding: 3em;
78 | background-color: #d6f1ef;
79 | box-shadow: 1px 1px 1px 1px rgba(106, 145, 146, 0.4);
80 | border-radius: 10px;
81 | margin: 5% 10%;
82 | }
83 | .form-error-message {
84 | font-size: smaller;
85 | margin: 1em;
86 | font-size: 1.3em;
87 | font-weight: 300;
88 | color: #ef6574;
89 | text-align: center;
90 | font-weight: 400;
91 | }
92 | .form-complete {
93 | padding-top: 5%;
94 | font-size: 1.2em;
95 | }
96 | .form-complete h1 {
97 | text-align: center;
98 | font-size: 1.5em;
99 | }
100 | .form-complete p {
101 | padding-left: 100px;
102 | }
103 |
104 | /* Navigation and Progress Bar */
105 | .nav {
106 | position: fixed;
107 | bottom: 0;
108 | left: 0;
109 | right: 0;
110 | background-color: inherit;
111 | padding: 5px;
112 | z-index: 10;
113 | display: flex;
114 | justify-content: center;
115 | transition: background-color 0.5s ease;
116 | }
117 |
118 | .form-button {
119 | border: 2px solid white;
120 | margin: 5px;
121 | border-radius: 5px;
122 | font-size: 1.2em;
123 | left: Calc(50% - 25px);
124 | box-shadow: 1px 1px 1px 1px rgba(106, 145, 146, 0.4);
125 | cursor: pointer;
126 | }
127 |
128 | .nav .form-button {
129 | font-weight: 500;
130 | padding: 10px;
131 | background-color: rgba(96, 125, 139, 0.25);
132 | }
133 | .form-error-message .form-button {
134 | font-weight: 700;
135 | padding: 10px;
136 | position: inherit;
137 | }
138 |
139 | .bar {
140 | height: 7px;
141 | width: 0px;
142 | background-color: #fefaa1;
143 | border-radius: 2px;
144 | position: fixed;
145 | bottom: 66px;
146 | }
147 |
148 | ::-webkit-scrollbar {
149 | width: 0px;
150 | }
151 | /* Generic media query for portrait mode */
152 | @media only screen and (orientation: portrait) {
153 | input[type="text"] {
154 | font-size: 1.4em;
155 | }
156 | textarea {
157 | font-size: 1.2em;
158 | }
159 | .field-label {
160 | font-size: 1.5em;
161 | }
162 | .field-group {
163 | top: Calc(40% - 100px) !important;
164 | }
165 | .field-area {
166 | padding: 2em 1em !important;
167 | }
168 | .form-error-message .form-button {
169 | display: none;
170 | }
171 | .form-complete {
172 | padding-top: 35%;
173 | pading-bottom: 35%;
174 | }
175 |
176 | .form-complete p {
177 | padding-left: 30px !important;
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/components/ComponentTypes.js:
--------------------------------------------------------------------------------
1 | import FormTemplate from "./FormTemplate.vue"
2 | import InputBox from "./FormElements/Fields/InputBox.vue"
3 | import TextArea from "./FormElements/Fields/TextArea.vue"
4 | import RadioButton from "./FormElements/Fields/RadioButton.vue"
5 | import CheckBox from "./FormElements/Fields/CheckBox.vue"
6 |
7 | const COMPONENT_MAP = {
8 | formTemplate: FormTemplate,
9 | text: InputBox,
10 | textarea: TextArea,
11 | radio: RadioButton,
12 | check: CheckBox
13 | }
14 |
15 | export function getComponent(type) {
16 | return COMPONENT_MAP[type]
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/FormConfigProvider.vue:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/src/components/FormElements/FieldError.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
20 |
--------------------------------------------------------------------------------
/src/components/FormElements/FieldGroup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
28 |
--------------------------------------------------------------------------------
/src/components/FormElements/FieldLabel.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/components/FormElements/Fields/CheckBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 |
16 |
47 |
52 |
--------------------------------------------------------------------------------
/src/components/FormElements/Fields/InputBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
--------------------------------------------------------------------------------
/src/components/FormElements/Fields/RadioButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
14 |
--------------------------------------------------------------------------------
/src/components/FormElements/Fields/TextArea.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
16 |
--------------------------------------------------------------------------------
/src/components/FormElements/FormNav.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Previous
6 |
Next
7 |
8 |
9 |
10 |
19 |
--------------------------------------------------------------------------------
/src/components/FormElements/FormProgress.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
25 |
--------------------------------------------------------------------------------
/src/components/FormElements/FormResult.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
27 |
--------------------------------------------------------------------------------
/src/components/FormTemplate.vue:
--------------------------------------------------------------------------------
1 |
2 |
31 |
32 |
33 |
67 |
--------------------------------------------------------------------------------
/src/components/Transitions/DataDrivenTransition.vue:
--------------------------------------------------------------------------------
1 |
25 |
--------------------------------------------------------------------------------
/src/components/Transitions/TypeBasedTransition.vue:
--------------------------------------------------------------------------------
1 |
38 |
--------------------------------------------------------------------------------
/src/config/formConfig.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "text",
4 | "label": "First Name",
5 | "name": "firstName",
6 | "options": {
7 | "attrs": {
8 | "placeholder": "First Name"
9 | }
10 | },
11 | "validation": "required"
12 | },
13 | {
14 | "type": "text",
15 | "label": "Last Name",
16 | "name": "lastName",
17 | "options": {
18 | "attrs": {
19 | "placeholder": "Last Name"
20 | }
21 | },
22 | "validation": ""
23 | },
24 | {
25 | "type": "check",
26 | "label": "Courses",
27 | "name": "courses",
28 | "options": {
29 | "choices": ["Project Management", "Business Analysis", "Product Management"]
30 | },
31 | "validation": "required"
32 | },
33 | {
34 | "type": "radio",
35 | "label": "Study Option",
36 | "name": "studyOption",
37 | "options": {
38 | "choices": ["Full-time", "Part-time", "Distant"]
39 | },
40 | "validation": "required"
41 | },
42 | {
43 | "type": "text",
44 | "label": "Your Email",
45 | "name": "email",
46 | "options": {
47 | "attrs": {
48 | "placeholder": "Your Email"
49 | }
50 | },
51 | "validation": "required|email"
52 | },
53 | {
54 | "type": "text",
55 | "label": "Your phone Number",
56 | "name": "phone",
57 | "options": {
58 | "attrs": {
59 | "placeholder": "Your Contact"
60 | }
61 | },
62 | "validation": "required|numeric"
63 | },
64 | {
65 | "type": "textarea",
66 | "label": "Tell us why do you want to study with us?",
67 | "name": "message",
68 | "options": {
69 | "attrs": {
70 | "placeholder": "Write here..."
71 | }
72 | },
73 | "validation": ""
74 | }
75 | ]
76 |
--------------------------------------------------------------------------------
/src/directives/index.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue"
2 |
3 | Vue.directive("colorswatch", function(el, binding) {
4 | let colors = [
5 | { light: "#d1c4e9", dark: "#4527a0" },
6 | { light: "#c5cae9", dark: "#283593" },
7 | { light: "#bbdefb", dark: "#1565c0" },
8 | { light: "#b3e5fc", dark: "#0277bd" },
9 | { light: "#b2ebf2", dark: "#00838f" },
10 | { light: "#b2dfdb", dark: "#00695c" },
11 | { light: "#c8e6c9", dark: "#2e7d32" },
12 | { light: "#dcedc8", dark: "#558b2f" },
13 | { light: "#f0f4c3", dark: "#9e9d24" },
14 | { light: "#fff9c4", dark: "#f9a825" },
15 | { light: "#ffecb3", dark: "#ff8f00" },
16 | { light: "#ffe0b2", dark: "#ef6c00" },
17 | { light: "#ffccbc", dark: "#d84315" }
18 | ]
19 | colors.map((color, key) => {
20 | if (key === binding.value) {
21 | if (binding.arg === "color") {
22 | el.style.color = color.dark
23 | } else if (binding.arg === "bg") {
24 | el.style.backgroundColor = color.light
25 | }
26 | }
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue"
2 | import App from "./App.vue"
3 |
4 | import store from "./store"
5 | import VeeValidate from "vee-validate"
6 |
7 | import "./directives"
8 | Vue.use(VeeValidate)
9 |
10 | Vue.config.productionTip = false
11 |
12 | new Vue({
13 | store,
14 | render: h => h(App)
15 | }).$mount("#app")
16 |
--------------------------------------------------------------------------------
/src/mixins/formMixin.js:
--------------------------------------------------------------------------------
1 | import { TimelineLite, Elastic } from "gsap"
2 |
3 | export default {
4 | data() {
5 | return {
6 | // local object variable to store form data
7 | formData: {},
8 |
9 | // Reactive object to be used for Provide/Inject
10 | formState: {
11 | activeField: 0,
12 | isNext: true,
13 | formLength: this.formFields.length,
14 | isComplete: false,
15 | isValid: false
16 | }
17 | }
18 | },
19 | computed: {
20 | activeFieldName() {
21 | return this.fields[this.formFields[this.formState.activeField].name]
22 | },
23 | isCurrentFieldValid() {
24 | if (this.isLastField) {
25 | return this.activeFieldName && this.activeFieldName.valid
26 | }
27 | },
28 | isLastField() {
29 | return this.formState.activeField < this.formState.formLength
30 | }
31 | },
32 | watch: {
33 | isLastField(newValue) {
34 | !newValue ? (this.formState.isComplete = true) : (this.formState.isComplete = false)
35 | },
36 | isCurrentFieldValid(newValue) {
37 | newValue ? (this.formState.isValid = true) : (this.formState.isValid = false)
38 | }
39 | },
40 | methods: {
41 | next() {
42 | this.formState.isNext = true
43 | this.isCurrentFieldValid ? this.proceed() : this.decline(".field-area")
44 | },
45 | back() {
46 | this.formState.isNext = false
47 | this.formState.activeField > 0 ? this.formState.activeField-- : ""
48 | },
49 | submit() {
50 | this.formState.isNext = true
51 | this.isCurrentFieldValid ? this.proceed() : ""
52 | },
53 | proceed() {
54 | this.isLastField ? this.formState.activeField++ : ""
55 | },
56 | decline(element) {
57 | // Shake form area when the field is invalid
58 | var tl = new TimelineLite()
59 | tl.to(element, 0.1, { x: 30 })
60 | tl.to(element, 3, {
61 | x: 0,
62 | color: "#ef6574",
63 | ease: Elastic.easeOut.config(0.9, 0.1)
64 | })
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/store/form/lead.js:
--------------------------------------------------------------------------------
1 | import formConfig from "../../config/formConfig.json"
2 |
3 | const mutations = {
4 | updateField(state, payload) {
5 | state.formData[payload.key] = payload.value
6 | }
7 | }
8 |
9 | const state = {
10 | formData: {}
11 | }
12 |
13 | formConfig.forEach(field => {
14 | state.formData[field.name] = ""
15 | })
16 |
17 | export default {
18 | namespaced: true,
19 | mutations,
20 | state
21 | }
22 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue"
2 | import Vuex from "vuex"
3 |
4 | import lead from "./form/lead.js"
5 |
6 | Vue.use(Vuex)
7 |
8 | export default new Vuex.Store({
9 | modules: {
10 | lead
11 | },
12 | strict: process.env.NODE_ENV !== `production`
13 | })
14 |
--------------------------------------------------------------------------------