Building forms for an engaging UX in VueJS Divya Sasidharan
A presentation at Connect.Tech 2018 in October 2018 in Atlanta, GA, USA by Divya
Building forms for an engaging UX in VueJS Divya Sasidharan
Divya Sasidharan Developer Advocate @Netlify @shortdiv
We’re hiring! https://boards.greenhouse.io/netlify
Cash Rules Everything Around Me
Forms Cash Rule Everything Around Me
“ Forms Suck. We should design accordingly. “ – Luke Wroblewski How Users Read on the Web
What makes a form “good”?
A Good Form is Simple A Good Form is Doing Stuff A Good Form is Talkative A Good Form is Unbreakable A Good Form is Fun A Good Form is Concise A Good Form is Well Formatted A Good Form is Easy to Scan A Good Form is Easy to Navigate
A Good Form is Simple A Good Form is Doing Stuff A Good Form is Talkative A Good Form is Unbreakable A Good Form is Fun A Good Form is Concise A Good Form is Well Formatted A Good Form is Easy to Scan A Good Form is Easy to Navigate
A Good Form is Doing Stuff
Tea Fog Earl Grey Matcha Rooibos Yerba Mate
Tea Fog Earl Grey Matcha Rooibos Yerba Mate
<input type="radio" name="teaType" :value="" :checked="" @change="" >
<input type="radio" name="teaType" :value="" :checked="" @change="" >
<input type="radio" name="teaType" :value="" :checked="" @change="" >
<input type="radio" name="teaType" :value="" :checked="" @change="" >
<template> <div> <form name=“vue-forms” method=“post” @submit.prevent=“handleSubmit"> <label v-for="tea in teaTypes"> <input type="radio" name="tea" :value="tea" :checked=“chosenTea ""=== tea" @input="ev "=> chosenTea = ev.target.value"> <span>{{ tea }}"</span> "</label> "</form> "</div> "</template> <script> export default { name: "TeaFog", data () { return { teaTypes: [“Earl Grey", "Matcha", “Rooibos”, “Yerba Mate"], chosenTea: “Earl Grey” }; } } "</script>
<template> <div> <form name=“vue-forms” method=“post” @submit.prevent=“handleSubmit"> <label v-for="tea in teaTypes"> > <input type="radio" name="tea" :value="tea" :checked=“chosenTea ""=== tea" @input="ev "=> chosenTea = ev.target.value"> > <span> <span>{{ tea }}"</span> "</span> "</label> "</form> "</div> "</template> <script> export default { name: "TeaFog", data () { return { teaTypes: [“Earl Grey", "Matcha", “Rooibos”, “Yerba Mate"], chosenTea: “Earl Grey” }; } } "</script>
<template> <div> <form name=“vue-forms” method=“post” @submit.prevent=“handleSubmit"> <label v-for="tea in teaTypes"> <input type="radio" name="tea" :value="tea" :checked=“chosenTea ""=== tea" @input="ev "=> chosenTea = ev.target.value"> > <span>{{ tea }}"</span> "</label> "</form> "</div> "</template> <script> export default { name: "TeaFog", data () { return { teaTypes: [ “Earl Grey", "Matcha", “Rooibos”, “Yerba Mate” ] ], chosenTea: “Earl Grey” }; } } "</script>
<template> <div> <form name=“vue-forms” method=“post” @submit.prevent=“handleSubmit"> <label v-for="tea in teaTypes"> <input type="radio" name="tea" :value="tea" :checked=“chosenTea ""=== tea" @input="ev "=> chosenTea = ev.target.value"> <span>{{ tea }}"</span> "</label> "</form> "</div> "</template> <script> export default { name: "TeaFog", data () { return { teaTypes: [ “Earl Grey", "Matcha", “Rooibos”, “Yerba Mate” ], chosenTea: “Earl chosenClip: “EarlGrey” Grey” }; } } "</script>
<template> <div> <form name=“vue-forms” method=“post” @submit.prevent=“handleSubmit"> <label v-for="tea in teaTypes"> <input type="radio" name="tea" :value="tea" :checked=“chosenTea ""=== tea" tea” @input="ev "=> chosenTea = ev.target.value"> <span>{{ tea }}"</span> "</label> "</form> "</div> "</template> <script> export default { name: "TeaFog", data () { return { teaTypes: [ “Earl Grey", "Matcha", “Rooibos”, “Yerba Mate” ], chosenTea: “Earl Grey” }; } } "</script>
<template> <div> <form name=“vue-forms” method=“post” @submit.prevent=“handleSubmit"> <label v-for="tea in teaTypes"> <input type="radio" name="tea" :value="tea" :checked="chosenTea ""=== tea" @input="ev "=> chosenTea = ev.target.value"> <span>{{ tea }}"</span> "</label> "</form> "</div> "</template> <script> export default { name: "TeaFog", data () { return { teaTypes: [“Earl Grey", "Matcha", “Rooibos”, “Yerba Mate"], chosenTea: "Earl Grey" }; } } "</script>
<template> <div> <form name=“vue-forms” method=“post” @submit.prevent=“handleSubmit"> <label v-for="tea in teaTypes"> <input type="radio" name="tea" :value="tea" v-model="chosenTea" > <span>{{ tea }}"</span> "</label> "</form> "</div> "</template> <script> export default { name: "TeaFog", data () { return { teaTypes: [“Earl Grey", "Matcha", “Rooibos”, “Yerba Mate"], chosenTea: "Earl Grey" }; } } "</script>
Demo
A Good Form is Talkative
Tea x Earl Grey 1 Manchester Fog Irish Breakfast Coming Right Up! Chai Matcha Milk Whole Milk Soy Milk Order Up!
<template> <div class=“notification” v-if="hasNotifications"> … "</div> "</template> <script> export default { name: "notification", props: { status: { type: Object }, }, data () { return { hasNotification: false, countdown: null, } }, watch: { status () { this.hasNotifications = true; this.countdown = setTimeout(this.removeNotification, 3000) } }, methods: { removeNotification () { clearTimeout(this.countdown) this.hasNotification = false } } } "</script>
<template> <div class=“notification” v-if="hasNotifications"> > … "</div> "</template> <script> export default { name: "notification", props: { status: { type: Object }, }, data () { return { hasNotification: false, countdown: null, } }, watch: { status () { this.hasNotifications = true; this.countdown = setTimeout(this.removeNotification, 3000) } } methods: { removeNotification () { clearTimeout(this.countdown) this.hasNotification = false } } } "</script>
<template> <div class=“notification” v-if=“hasNotifications"> … "</div> "</template> <script> export default { name: "notification", “notification", props: { status: { type: Object }, }, data () { return { hasNotification: false, countdown: null, } }, watch: { status () { this.hasNotifications = true; this.countdown = setTimeout(this.removeNotification, 3000) } } methods: { removeNotification () { clearTimeout(this.countdown) this.hasNotification = false } } } "</script> } "</script>
<template> <div class=“notification” class=“notification”>v-if="hasNotifications"> … "</div> "</template> <script> export default { name: "notification", props: { status: { type: Object }, }, data () { return { hasNotification: false, countdown: null, } }, watch: { status () { this.hasNotifications = true; this.countdown = setTimeout(this.removeNotification, 3000) } } }, methods: { removeNotification () { clearTimeout(this.countdown) this.hasNotification = false } } } "</script>
<template> <div class=“notification” class=“notification”>v-if="hasNotifications"> … "</div> "</template> <script> export default { name: "notification", props: { status: { type: Object }, }, data () { return { hasNotification: false, countdown: null, } }, watch: { status () { this.hasNotifications = true; this.countdown = setTimeout(this.removeNotification, 3000) } } }, methods: { removeNotification () { clearTimeout(this.countdown) this.hasNotification = false } } } "</script>
Demo
Milk Anyone who has used that comforting phrase ‘a nice cup of tea’ invariably means Indian tea. Whole Milk Soy Milk
Demo
A Good Form is Unbreakable
<template> … <label v-for="milk in milkNames"> <input name="milk" type="radio" :disabled=“isMilkDisabled(milk)" …> <span>{{ milk }}"</span> "</label> "</template> <script> export default { name: "TeaFog", data () { return { milkNames: [‘Whole Milk’, ‘Soy Milk’] } }, methods: { isMilkDisabled (milk) { "// check that there is the available milk options based on a selected Tea } }, } "</script>
<template> … <label v-for="milk in milkNames"> <input name="milk" type="radio" :disabled=“isMilkDisabled(milk)" …> <span>{{ milk }}"</span> <span> "</span> "</label> "</template> <script> export default { name: "TeaFog", data () { return { milkNames: [‘Whole Milk’, ‘Soy Milk’] } }, methods: { isMilkDisabled (milk) { "// check that there is the available milk options based on a selected Tea } }, } } "</script> "</script>
<template> … <label v-for="milk in milkNames"> <input name="milk" type="radio" type=“radio" :disabled=“isMilkDisabled(milk)" …> <span>{{ milk }}"</span> "</label> "</template> <script> export default { name: "TeaFog", data () { return { milkNames: [‘Whole Milk’, ‘Soy Milk’] } }, methods: { isMilkDisabled (milk) { "// check that there is the available milk options based on a selected Tea } }, } } "</script> "</script>
<template> … <label v-for="milk in milkNames"> <input name="milk" type="radio" type=“radio" :disabled=“isMilkDisabled(milk)" :disabled=“isMilkDisabled(milk)” …> <span>{{ milk }}"</span> "</label> "</template> <script> export default { name: "TeaFog", data () { return { milkNames: [‘Whole Milk’, ‘Soy Milk’] } }, methods: { isMilkDisabled (milk) { "// check that there is the available milk options based on a selected Tea } }, } } "</script> "</script>
teaTypes: { "Earl Grey": ['London', 'Manchester', 'Seattle'], "Irish Breakfast": ['Dublin'], “Chai":['Bombay'], "Rose": ['Atlantic City'], "Matcha": ['Tokyo'], "Rooibos": ['Cape Town'], "Yerba Mate":['Montreal'], "Green": ['Oregon Mist'] }, milkTypes: { 'Whole Milk':['London', 'Oregon Mist', 'Dublin', …], 'Soy Milk’: ['Manchester', 'Seattle'], }
teaTypes: { "Earl Grey": ['London', 'Manchester', 'Seattle'], "Irish Breakfast": ['Dublin'], “Chai":['Bombay'], "Rose": ['Atlantic City'], "Matcha": ['Tokyo'], "Rooibos": ['Cape Town'], "Yerba Mate":['Montreal'], "Green": ['Oregon Mist'] }, milkTypes: { 'Whole Milk':['London', 'Oregon Mist', 'Dublin', …], 'Soy Milk’: ['Manchester', 'Seattle'], }
teaTypes: { "Earl Grey": ['London', 'Manchester', 'Seattle'], "Irish Breakfast": ['Dublin'], “Chai":['Bombay'], "Rose": ['Atlantic City'], "Matcha": ['Tokyo'], "Rooibos": ['Cape Town'], "Yerba Mate":['Montreal'], "Green": ['Oregon Mist'] }, milkTypes: { 'Whole Milk':['London', 'Oregon Mist', 'Dublin', …], 'Soy Milk’: ['Manchester', 'Seattle'], }
teaTypes: { "Earl Grey": ['London', 'Manchester', 'Seattle'], "Irish Breakfast": ['Dublin'], “Chai":['Bombay'], "Rose": ['Atlantic City'], "Matcha": ['Tokyo'], "Rooibos": ['Cape Town'], "Yerba Mate":['Montreal'], "Green": ['Oregon Mist'] }, milkTypes: { 'Whole Milk':['London', 'Oregon Mist', 'Dublin', …], 'Soy Milk’: ['Manchester', 'Seattle'], }
Demo
<template> <div> <label> <input type=“tel" :value="formatNumber" @change="handleChange" @keyup="handleChange" > "</label> "</div> "</template> "</script> import { NorthAmericanize } from ""../utils/lib.js"; export default { name: "PhoneField", data() { return { formatNumber: "" }; }, methods: { … handleChange(e) { if (this.isNotValidNumChar(e.which) "&& this.isNotDelete(e.which)) { e.preventDefault(); return; } this.number = NorthAmericanize(e, e.target.value); } } }; "</script>
<template> <div> <label> <input type=“tel" :value="formatNumber" @change="handleChange" @keyup="handleChange" > "</label> "</div> "</template> "</script> import { NorthAmericanize } from ""../utils/lib.js"; export default { name: "PhoneField", data() { return { formatNumber: "" }; }, methods: { … handleChange(e) { if (this.isNotValidNumChar(e.which) "&& this.isNotDelete(e.which)) { e.preventDefault(); return; } this.number = NorthAmericanize(e, e.target.value); } } }; "</script>
<template> <div> <label> <input type=“tel" :value="formatNumber" @change="handleChange" @keyup="handleChange" > "</label> "</div> "</template> "</script> import { NorthAmericanize } from ""../utils/lib.js"; export default { name: "PhoneField", data() { return { formatNumber: "" }; }, methods: { … handleChange(e) { if (this.isNotValidNumChar(e.which) "&& this.isNotDelete(e.which)) { e.preventDefault(); return; } this.number = NorthAmericanize(e, e.target.value); } } }; "</script>
<template> <div> <label> <input type=“tel" :value="formatNumber" @change="handleChange" @keyup="handleChange" > "</label> "</div> "</template> "</script> import { NorthAmericanize } from ""../utils/lib.js"; export default { name: "PhoneField", data() { return { formatNumber: "" }; }, methods: { … handleChange(e) { if (this.isNotValidNumChar(e.which) "&& this.isNotDelete(e.which)) { e.preventDefault(); return; } this.number = NorthAmericanize(e, e.target.value); } } }; "</script>
Demo
A Good Form is Fun to Complete
Demo
A Good Form is Fun
Demo
!-> Netlify !-> firebase Actions Component Mutations State
A Good Form is Fun to Work On
<template> <form name=“vue-tea-form" method=“post" data-netlify=“true” data-netlify-honeypot=“bot-field" @submit.prevent="handleSubmit"> <input type=“hidden" name=“form-name" value=“vue-tea-full-form“ "/> … "</form> "</template> <script> export default { name: "Form", methods: { handleSubmit() {…} }, } "</script>
<template> <form name=“vue-tea-form" method=“post" data-netlify=“true” data-netlify-honeypot=“bot-field" @submit.prevent="handleSubmit"> <input type=“hidden" name=“form-name" value=“vue-tea-full-form“ "/> … "</form> "</template> <script> export default { name: "Form", methods: { handleSubmit() {…} }, } "</script>
<template> <form name=“vue-tea-form" method=“post" data-netlify=“true” data-netlify-honeypot=“bot-field" @submit.prevent="handleSubmit"> <input type=“hidden" name=“form-name" value=“vue-tea-full-form“ "/> … "</form> "</template> <script> export default { name: "Form", methods: { handleSubmit() {…} }, } "</script>
<template> <form name=“vue-tea-form" method=“post" data-netlify=“true” data-netlify-honeypot=“bot-field" @submit.prevent="handleSubmit"> <input type=“hidden" name=“form-name" value=“vue-tea-full-form“ "/> … "</form> "</template> <script> export default { name: "Form", methods: { handleSubmit() {…} }, } "</script>
<template> <form name=“vue-tea-form" method=“post" data-netlify=“true” data-netlify-honeypot=“bot-field" @submit.prevent="handleSubmit"> <input type=“hidden" name=“form-name" value=“vue-tea-full-form“ "/> … "</form> "</template> <script> export default { name: "Form", methods: { handleSubmit() {…} }, } "</script>
<template> <form name=“vue-tea-form" method=“post" data-netlify=“true” data-netlify-honeypot=“bot-field" @submit.prevent="handleSubmit"> <input type=“hidden" name=“form-name" value="ask-team-vue" "/> value=“vue-tea-full-form“ "/> … "</form> "</template> <script> export default { name: "Form", methods: { handleSubmit() {…} }, } "</script>
<template> <form name=“vue-tea-form" method=“post" data-netlify=“true” data-netlify-honeypot=“bot-field" @submit.prevent="handleSubmit"> <input type=“hidden" name=“form-name" value=“vue-tea-full-form“ "/> … "</form> "</template> <script> export default { name: "Form", methods: { handleSubmit() {…} }, } "</script>
<template> <form … "</form> "</template> <script> export default { name: "Form", methods: { handleSubmit () { fetch('/', { method: 'POST', headers: { "Content-Type": “application/x-""www-form-urlencoded" }, body: this.encode({ 'form-name': ‘vue-tea-full-form’, 'ask-team-vue', ""...this.form }) }) } } } "</script>
<template> <form … "</form> "</template> <script> export default { name: "Form", methods: { handleSubmit () { fetch('/', { method: 'POST', headers: { "Content-Type": “application/x-""www-form-urlencoded" }, body: this.encode({ 'form-name': ‘vue-tea-full-form’, 'ask-team-vue', ""...this.form }) }) } } } "</script>
<template> <form … "</form> "</template> <script> export default { name: "Form", methods: { handleSubmit () { fetch('/', { … }) .then(() "=> { this.$router.push('thanks') this.$router.push(‘thanks') }) .catch(err "=> { this.$router.push('404') }); } } } "</script>
import import import import import Vue from 'vue'; Router from 'vue-router'; Form from '"../components/Form'; SubmissionSuccess from '"../components/SubmissionSuccess' SubmissionFail from '"../components/SubmissionFail' Vue.use(Router); const router = new Router({ routes: [ { path: '/', name: 'Form', component: QAForm }, { path: '/thanks', name: 'success', component: SubmissionSuccess }, { path: '/404', name: 'fail', component: SubmissionFail } ] }) export default router;
const path = require('path') const PrerenderSPAPlugin = require('prerender-spa-plugin') module.exports = { configureWebpack: () "=> { if (process.env.NODE_ENV ""!== 'production') return; return { plugins: [ new PrerenderSPAPlugin( "// Absolute path to compiled SPA path.resolve("__dirname, 'dist'), "// List of routes to prerender [ '/'], { "// options } ), ] } } } vue.config.js
. ├── functions ├── submission-created.js ├── ├── ├── └── src public ""... package.json
exports.handler = function(event, context, callback) { "// your server-side magic ✨✨✨🧙✨✨✨ "// }
A Good Form is Simple A Good Form is Doing Stuff A Good Form is Talkative A Good Form is Unbreakable A Good Form is Fun A Good Form is Concise A Good Form is Well Formatted A Good Form is Easy to Scan A Good Form is Easy to Navigate
Thanks! @shortdiv https://codepen.io/collection/XgWrxd/ https://github.com/shortdiv/yanny-ya-hear-laurel