A presentation at Connect.Tech 2018 in 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
Forms are one of the most heavily used forms of interactivity on the web and are an integral part of any web application. When built well, they incentivize users to complete forms quickly without sacrificing inaccuracies. Creating a “good form” that is semantic while also providing the user with a pleasant experience however is a tricky business. It requires a keen understanding of the user experience to build out a good engaging workflow and a willingness to finagle input elements to your bidding. Thankfully, the template syntax in VueJS alongside its state management system lends itself well to creating semantic web forms that also respond seamlessly to user input. In this talk, we will explore various patterns and strategies to building semantic and robust forms in VueJS while also ensuring a delightful and engaging user experience.