Building forms for an engaging UX in VueJS

A presentation at Connect.Tech 2018 in October 2018 in Atlanta, GA, USA by Divya

Slide 1

Slide 1

Building forms for an engaging UX in VueJS Divya Sasidharan

Slide 2

Slide 2

Divya Sasidharan Developer Advocate @Netlify @shortdiv

Slide 3

Slide 3

We’re hiring! https://boards.greenhouse.io/netlify

Slide 4

Slide 4

Slide 5

Slide 5

Slide 6

Slide 6

Slide 7

Slide 7

Cash Rules Everything Around Me

Slide 8

Slide 8

Forms Cash Rule Everything Around Me

Slide 9

Slide 9

“ Forms Suck. We should design accordingly. “ – Luke Wroblewski How Users Read on the Web

Slide 10

Slide 10

Slide 11

Slide 11

What makes a form “good”?

Slide 12

Slide 12

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

Slide 13

Slide 13

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

Slide 14

Slide 14

A Good Form is Doing Stuff

Slide 15

Slide 15

Tea Fog Earl Grey Matcha Rooibos Yerba Mate

Slide 16

Slide 16

Tea Fog Earl Grey Matcha Rooibos Yerba Mate

Slide 17

Slide 17

<input type="radio" name="teaType" :value="" :checked="" @change="" >

Slide 18

Slide 18

<input type="radio" name="teaType" :value="" :checked="" @change="" >

Slide 19

Slide 19

<input type="radio" name="teaType" :value="" :checked="" @change="" >

Slide 20

Slide 20

<input type="radio" name="teaType" :value="" :checked="" @change="" >

Slide 21

Slide 21

<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>

Slide 22

Slide 22

<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>

Slide 23

Slide 23

<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>

Slide 24

Slide 24

<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>

Slide 25

Slide 25

<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>

Slide 26

Slide 26

<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>

Slide 27

Slide 27

<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>

Slide 28

Slide 28

Demo

Slide 29

Slide 29

A Good Form is Talkative

Slide 30

Slide 30

Slide 31

Slide 31

Tea x Earl Grey 1 Manchester Fog Irish Breakfast Coming Right Up! Chai Matcha Milk Whole Milk Soy Milk Order Up!

Slide 32

Slide 32

<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>

Slide 33

Slide 33

<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>

Slide 34

Slide 34

<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>

Slide 35

Slide 35

<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>

Slide 36

Slide 36

<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>

Slide 37

Slide 37

Demo

Slide 38

Slide 38

Slide 39

Slide 39

Milk Anyone who has used that comforting phrase ‘a nice cup of tea’ invariably means Indian tea. Whole Milk Soy Milk

Slide 40

Slide 40

Demo

Slide 41

Slide 41

A Good Form is Unbreakable

Slide 42

Slide 42

Slide 43

Slide 43

<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>

Slide 44

Slide 44

<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>

Slide 45

Slide 45

<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>

Slide 46

Slide 46

<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>

Slide 47

Slide 47

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'], }

Slide 48

Slide 48

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'], }

Slide 49

Slide 49

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'], }

Slide 50

Slide 50

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'], }

Slide 51

Slide 51

Demo

Slide 52

Slide 52

Slide 53

Slide 53

<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>

Slide 54

Slide 54

<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>

Slide 55

Slide 55

<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>

Slide 56

Slide 56

<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>

Slide 57

Slide 57

Demo

Slide 58

Slide 58

A Good Form is Fun to Complete

Slide 59

Slide 59

Slide 60

Slide 60

Demo

Slide 61

Slide 61

A Good Form is Fun

Slide 62

Slide 62

Demo

Slide 63

Slide 63

!-> Netlify !-> firebase Actions Component Mutations State

Slide 64

Slide 64

A Good Form is Fun to Work On

Slide 65

Slide 65

Slide 66

Slide 66

<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>

Slide 67

Slide 67

<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>

Slide 68

Slide 68

<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>

Slide 69

Slide 69

<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>

Slide 70

Slide 70

<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>

Slide 71

Slide 71

<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>

Slide 72

Slide 72

<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>

Slide 73

Slide 73

<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>

Slide 74

Slide 74

<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>

Slide 75

Slide 75

<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>

Slide 76

Slide 76

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;

Slide 77

Slide 77

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

Slide 78

Slide 78

. ├── functions ├── submission-created.js ├── ├── ├── └── src public ""... package.json

Slide 79

Slide 79

exports.handler = function(event, context, callback) { "// your server-side magic ✨✨✨🧙✨✨✨ "// }

Slide 80

Slide 80

Slide 81

Slide 81

Slide 82

Slide 82

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

Slide 83

Slide 83

Thanks! @shortdiv https://codepen.io/collection/XgWrxd/ https://github.com/shortdiv/yanny-ya-hear-laurel