Fundamental Component Design Patterns

A presentation at Connect.Tech in October 2019 in Atlanta, GA, USA by Ben Hong

Slide 1

Slide 1

Fundamental Component Design Patterns 2019 @bencodezen

Slide 2

Slide 2

BEN HONG Senior Frontend Engineer @ GitLab Vue.js Community Partner @bencodezen

Slide 3

Slide 3

Slide 4

Slide 4

Slide 5

Slide 5

Before we get started…

Slide 6

Slide 6

Before we get started… All resources will be available online https://www.twitter.com/bencodezen For the social media folks @bencodezen - #ConnectTech

Slide 7

Slide 7

Slide 8

Slide 8

Slide 9

Slide 9

Slide 10

Slide 10

Slide 11

Slide 11

Slide 12

Slide 12

Component Basics

Slide 13

Slide 13

Navbar.vue <template> <ul> <li class=”nav-item”> <a href=”/Home”>Home</a> </li> <li class=”nav-item”> <a href=”/About”>About</a> </li> <li class=”nav-item”> <a href=”/Contact”>Contact</a> </li> </ul> </template>

Slide 14

Slide 14

Navbar.vue <template> <ul> <li class=”nav-item”> <a href=”/Home”>Home</a> </li> <li class=”nav-item”> <a href=”/About”>About</a> </li> <li class=”nav-item”> <a href=”/Contact”>Contact</a> </li> </ul> </template> NavItem.vue <template> <li class=”nav-item”> <a :href=”`/${label}`”> {{ label }} </a> </li> </template>

Slide 15

Slide 15

Navbar.vue <template> <ul> <li class=”nav-item”> <a href=”/Home”>Home</a> </li> <li class=”nav-item”> <a href=”/About”>About</a> </li> <li class=”nav-item”> <a href=”/Contact”>Contact</a> </li> </ul> </template>

Slide 16

Slide 16

Navbar.vue <template> <ul> <NavItem label=”Home”></NavItem> <NavItem label=”About”></NavItem> <NavItem label=”Contact”></NavItem> </ul> </template>

Slide 17

Slide 17

Why Components?

Slide 18

Slide 18

Build things faster

Slide 19

Slide 19

No more repetitive code

Slide 20

Slide 20

Less bugs means you can relax

Slide 21

Slide 21

TECHNIQUE Props

Slide 22

Slide 22

Navbar.vue <script> export default { props: [‘label’] } </script> <template> <li class=”nav-item”> <a :href=”`/${label}`”> {{ label }} </a> </li> </template>

Slide 23

Slide 23

Navbar.vue <script> export default { props: [‘label’] } </script> <template> <li class=”nav-item”> <a :href=”`/${label}`”> {{ label }} </a> </li> </template> NavItem.vue <template> <ul> <NavItem label=”Home” /> <NavItem label=”About” /> <NavItem label=”Contact” /> </ul> </template>

Slide 24

Slide 24

Navbar.vue <script> export default { props: [‘label’] } </script> <template> <li class=”nav-item”> <a :href=”`/${label}`”> {{ label }} </a> </li> </template> NavItem.vue <template> <ul> <NavItem label=”Home” /> <NavItem label=”About” /> <NavItem label=”Contact” /> </ul> </template>

Slide 25

Slide 25

Navbar.vue <script> export default { props: { label: { type: String, required: true, default: ‘Home’ } } } </script>

Slide 26

Slide 26

Navbar.vue <script> export default { props: { label: { type: String, default: ‘Home’ } } } </script>

Slide 27

Slide 27

Let’s do a little thought experiment… Hat tip to Damian Dulisz

Slide 28

Slide 28

Task 1 Create a button component that can display text specified in the parent component

Slide 29

Slide 29

Task 2 Allow the button to display an icon of choice on the right side of the text

Slide 30

Slide 30

Task 3 Make it possible to have icons on either side or even both sides

Slide 31

Slide 31

Task 4 Make it possible to replace everything with a loading spinner

Slide 32

Slide 32

Task 5 Make it possible to replace an icon with a loading spinner

Slide 33

Slide 33

<template> <button type=”button” class=”nice-button”> {{ text }} </button> </template> <script> export default { props: [‘text’] } </script>

Slide 34

Slide 34

<template> <button type=”button” class=”nice-button”> <SpinnerIcon v-if=”isLoading” color=”#fff” size=”12px”> <template v-else> <template v-if=”iconLeftName”> <SpinnerIcon v-if=”isLoadingLeft” color=”#fff” size=”6px”> <AppIcon v-else :icon=”iconLeftName”/> </template> {{ text }} <template v-if=”iconRightName”> <SpinnerIcon v-if=”isLoadingRight” color=”#fff” size=”6px”> <AppIcon v-else :icon=”iconRightName”/> </template> </template> </button> </template> <script> export default { props: [‘text’, ‘iconLeftName’, ‘iconRightName’, ‘isLoading’, ‘isLoadingLeft’, ‘isLoadingRight’] } </script>

Slide 35

Slide 35

<template> <button type=”button” class=”nice-button”> <PulseLoader v-if=”isLoading” color=”#fff” size=”12px”> <template v-else> <template v-if=”iconLeftName”> <PulseLoader v-if=”isLoadingLeft” color=”#fff” size=”6px”> <AppIcon v-else :icon=”iconLeftName”/> </template> {{ text }} <template v-if=”iconRightName”> <PulseLoader v-if=”isLoadingRight” color=”#fff” size=”6px”> <AppIcon v-else :icon=”iconRightName”/> </template> </template> </button> </template> <script> export default { props: [‘text’, ‘iconLeftName’, ‘iconRightName’, ‘isLoading’, ‘isLoadingLeft’, ‘isLoadingRight’] } </script>

Slide 36

Slide 36

<template> <button type=”button” class=”nice-button”> {{ text }} </button> </template> <script> export default { props: [‘text’] } </script> OMG PROPS EVERYWHERE!

Slide 37

Slide 37

<template> <button type=”button” class=”nice-button”> {{ text }} </button> </template> Let’s call it the props-based solution <script> export default { props: [‘text’] } </script>

Slide 38

Slide 38

props-based solution Is it wrong?

Slide 39

Slide 39

props-based solution Is it wrong? No. It does the job.

Slide 40

Slide 40

props-based solution Is it good, then?

Slide 41

Slide 41

props-based solution Is it good, then? Not exactly.

Slide 42

Slide 42

props-based solution Problems

Slide 43

Slide 43

props-based solution Problems • New requirements increase complexity • Multiple responsibilities • Lots of conditionals in the template • Low flexibility • Hard to maintain

Slide 44

Slide 44

Is there a better another alternative?

Slide 45

Slide 45

Is there a better another alternative?

Slide 46

Slide 46

TECHNIQUE Slots

Slide 47

Slide 47

Recommended solution

Slide 48

Slide 48

Recommended solution <template> <button type=“button” class=“nice-button“> <slot /> </button> </template>

Slide 49

Slide 49

<template> <button type=“button” class=“nice-button“> <slot/> </button> </template> Usage: <AppButton> Submit <PulseLoader v-if=”isLoading” color=”#fff” size=”6px”/> <AppIcon v-else icon=“arrow-right”/> </AppButton>

Slide 50

Slide 50

Default Slot // navigation-link.vue <a v-bind:href=”url” class=”nav-link” > <slot></slot> </a> <navigation-link url=”/profile”> <span class=”fa fa-user”/> Your Profile </navigation-link>

Slide 51

Slide 51

Named Slots // base-layout.vue <div class=”container”> <header> <slot name=”header”></slot> </header> <main> <slot></slot> </main> <footer> <slot name=”footer”></slot> </footer> </div> <base-layout> <template slot=”header”> <h1>Here might be a page title</h1> </template> <p>A paragraph for the main content.</p> <p>And another one.</p> <p slot=“footer”> Here’s some contact info </p> </base-layout>

Slide 52

Slide 52

Scoped Slots // todo-list.vue <ul> <li v-for=”todo in todos” :key=“todo.id” > <slot :todo=”todo”> <!— Fallback content —> {{ todo.text }} </slot> </li> </ul> <todo-list :todos=”todos”> <template slot-scope=”scope”> <AppIcon v-if=”scope.todo.completed” icon=”checked” /> {{ scope.todo.text }} </template> </todo-list>

Slide 53

Slide 53

Scoped slots // todo-list.vue <ul> <li v-for=”todo in todos” :key=“todo.id” > <slot :todo=”todo”> <!— Fallback content —> {{ todo.text }} </slot> </li> </ul> <todo-list :todos=”todos”> <template slot-scope=”scope”> <AppIcon v-if=”scope.todo.completed” icon=”checked” /> {{ scope.todo.text }} </template> </todo-list>

Slide 54

Slide 54

Scoped slots // todo-list.vue <ul> <li v-for=”todo in todos” :key=“todo.id” > <slot :todo=”todo”> <!— Fallback content —> {{ todo.text }} </slot> </li> </ul> <todo-list :todos=”todos”> <template slot-scope=”scope”> <AppIcon v-if=”scope.todo.completed” icon=”checked” /> {{ scope.todo.text }} </template> </todo-list>

Slide 55

Slide 55

Destructuring slot-scope <todo-list :todos=”todos”> <template slot-scope=”scope”> <AppIcon v-if=”scope.todo.completed” icon=”checked” /> {{ scope.todo.text }} </template> </todo-list> <todo-list :todos=”todos”> <template slot-scope=“{ todo }“> <AppIcon v-if=”todo.completed” icon=”checked” /> {{ todo.text }} </template> </todo-list>

Slide 56

Slide 56

Use slots for: • Content distribution (like layouts) • Creating larger components by combining smaller components • Default content in Multi-page Apps • Providing a wrapper for other components • Replace default component fragments

Slide 57

Slide 57

Use scoped slots for: • Applying custom formatting/template to fragments of a component • Creating wrapper components • Exposing its own data and methods to child components

Slide 58

Slide 58

Slots changes in Vue v2.6

Slide 59

Slide 59

What’s new in v2.6 Unified v-slot directive <base-layout> <template slot=”header”> <h1>Here might be a page title</h1> </template> <p>Main content.</p> <p>And another one.</p> <template slot=”footer”> <p>Here’s some contact info</p> </template> </base-layout> <base-layout> <template v-slot:header> <h1>Here might be a page title</h1> </template> <p>Main content.</p> <p>And another one.</p> <template v-slot:footer> <p>Here’s some contact info</p> </template> </base-layout>

Slide 60

Slide 60

What’s new in v2.6 Unified v-slot directive <base-layout> <template slot=”header”> <h1>Here might be a page title</h1> </template> <p>Main content.</p> <p>And another one.</p> <template slot=”footer”> <p>Here’s some contact info</p> </template> </base-layout> <base-layout> <template v-slot:header> <h1>Here might be a page title</h1> </template> <p>Main content.</p> <p>And another one.</p> <template v-slot:footer> <p>Here’s some contact info</p> </template> </base-layout>

Slide 61

Slide 61

What’s new in v2.6 Unified v-slot directive <todo-list :todos=”todos”> <template slot=”todo” slot-scope=”{ todo }”> {{ todo.text }} </template> </todo-list> <todo-list :todos=”todos”> <template v-slot:todo=”{ todo }”> {{ todo.text }} </template> </todo-list>

Slide 62

Slide 62

What’s new in v2.6 Unified v-slot directive <todo-list :todos=”todos”> <template slot=”todo” slot-scope=”{ todo }”> {{ todo.text }} </template> </todo-list> <todo-list :todos=”todos”> <template v-slot:todo=”{ todo }”> {{ todo.text }} </template> </todo-list>

Slide 63

Slide 63

What’s new in v2.6 v-slot directive shorthand <todo-list :todos=”todos”> <template v-slot:todo=”{ todo }”> {{ todo.text }} </template> </todo-list> <todo-list :todos=”todos”> <template #todo=“{ todo }”> {{ todo.text }} </template> </todo-list>

Slide 64

Slide 64

What’s new in v2.6 v-slot directive shorthand <todo-list :todos=”todos”> <template v-slot:todo=”{ todo }”> {{ todo.text }} </template> </todo-list> <todo-list :todos=”todos”> <template #todo=“{ todo }”> {{ todo.text }} </template> </todo-list>

Slide 65

Slide 65

What’s new in v2.6 Dynamic Slot Names <base-layout> <template v-slot:[dynamicSlotName]> … </template> </base-layout>

Slide 66

Slide 66

Slots > Props

Slide 67

Slide 67

Composition With composition, you’re less restricted by what you are building at first

Configuration With configuration, you have to document everything and new requirements means new configuration

Slide 68

Slide 68

DESIGN PATTERN Transparent Components

Slide 69

Slide 69

When passing props, listeners, and attributes… // BaseInput.vue <template> <div> <input type=”text” v-bind=“{ …$attrs, …$props }” v-on=“$listeners” /> </div> </template>

Slide 70

Slide 70

When passing props, listeners, and attributes… // BaseInput.vue <template> <div> <BaseInput <input @input=”filterData” type=”text” label=”Filter results” v-bind=“{ …$attrs, …$pr placeholder=”Type in here…” v-on=“$listeners” /> /> </div> </template>

Slide 71

Slide 71

When passing props, listeners, and attributes… // BaseInput.vue <template> <div> <BaseInput <input @input=”filterData” type=”text” label=”Filter results” v-bind=“{ …$attrs, …$pr placeholder=”Type in here…” v-on=“$listeners” /> /> </div> </template>

Slide 72

Slide 72

When passing props, listeners, and attributes… <template> <div> <input type=”text” v-bind=“{ …$attrs, …$props }” v-on=“$listeners” /> </div> </template> <script> export default { inheritAttrs: false, // … } </script> Both props and attributes, as well as all listeners will be passed to this element instead. Prevent Vue from assigning attributes to top-level element

Slide 73

Slide 73

When passing props, listeners, and attributes… <template> <div> <input type=”text” v-bind=“{ …$attrs, …$props }” v-on=“$listeners” /> </div> </template> <script> export default { inheritAttrs: false, // … } </script>

Slide 74

Slide 74

DESIGN PATTERN Wrap Vendor Components

Slide 75

Slide 75

<template> <p> <FontAwesome <FontAwesome <FontAwesome <FontAwesome </p> </template> <template> <p> <FontAwesome <FontAwesome <FontAwesome <FontAwesome </p> </template> icon=“water” /> icon=”earth” /> icon=“fire” /> icon=”air” /> <template> <p> <FontAwesome <FontAwesome <FontAwesome <FontAwesome </p> </template> 😱 icon=“water” /> icon=”earth” /> icon=“fire” /> icon=”air” /> <template> <p> <FontAwesome <FontAwesome <FontAwesome <FontAwesome </p> </template> icon=“water” /> icon=”earth” /> icon=“fire” /> icon=”air” /> icon=“water” /> icon=”earth” /> icon=“fire” /> icon=”air” /> <template> <p> <FontAwesome <FontAwesome <FontAwesome <FontAwesome </p> </template> icon=“water” /> icon=”earth” /> icon=“fire” /> icon=”air” />

Slide 76

Slide 76

BaseIcon.vue <template> <FontAwesomeIcon v-if=”src === ‘fa’” :icon=”name” /> <span v-else :class=”customIconClass” /> </template> Hat tip to Chris Fritz

Slide 77

Slide 77

<template> <p> <BaseIcon <BaseIcon <BaseIcon <BaseIcon </p> </template> src=“fa” src=“fa” src=“fa” src=“fa” icon=“earth” /> icon=”fire” /> icon=”water” /> icon=”water” />

Slide 78

Slide 78

DESIGN PATTERN Provider Components

Slide 79

Slide 79

Problem What if you only want to expose Data and Methods, but no User Interface?

Slide 80

Slide 80

“Provider” components <ApolloQuery :query=”require(‘../graphql/HelloWorld.gql’)” :variables=”{ name }” > <template slot-scope=”{ result: { loading, error, data } }”> <!— Loading —> <div v-if=”loading” class=”loading apollo”>Loading…</div> <!— Error —> <div v-else-if=”error” class=”error apollo”>An error occured</div> <!— Result —> <div v-else-if=”data” class=”result apollo”>{{ data.hello }}</div> From Vue Apollo by @akryum

Slide 81

Slide 81

<ApolloQuery :query=”require(‘../graphql/HelloWorld.gql’)” :variables=”{ name }” > <template slot-scope=”{ result: { loading, error, data } }”> <!— Loading —> <div v-if=”loading” class=”loading apollo”>Loading…</div> <!— Error —> <div v-else-if=”error” class=”error apollo”>An error occured</div> <!— Result —> <div v-else-if=”data” class=”result apollo”>{{ data.hello }}</div> From Vue Apollo by @akryum

Slide 82

Slide 82

<ApolloQuery :query=”require(‘../graphql/HelloWorld.gql’)” :variables=”{ name }” > <template slot-scope=”{ result: { loading, error, data } }”> <!— Loading —> <div v-if=”loading” class=”loading apollo”>Loading…</div> <!— Error —> <div v-else-if=”error” class=”error apollo”>An error occured</div> <!— Result —> <div v-else-if=”data” class=”result apollo”>{{ data.hello }}</div> From Vue Apollo by @akryum

Slide 83

Slide 83

<ApolloQuery :query=”require(‘../graphql/HelloWorld.gql’)” :variables=”{ name }” > <template slot-scope=”{ result: { loading, error, data } }”> <!— Loading —> <div v-if=”loading” class=”loading apollo”>Loading…</div> <!— Error —> <div v-else-if=”error” class=”error apollo”>An error occured</div> <!— Result —> <div v-else-if=”data” class=”result apollo”>{{ data.hello }}</div> From Vue Apollo by @akryum

Slide 84

Slide 84

// SelectProvider.vue export default { props: [‘value’, ‘options’], data () { isOpen: false }, render () { return this.$scopedSlots.default({ value: this.value, options: this.options, select: this.select, deselect: this.deselect, isOpen: this.isOpen, // and more }) }, methods: { // methods } }

Slide 85

Slide 85

“Renderless” components // SelectProvider.vue export default { props: [‘value’, ‘options’], data () { isOpen: false }, render () { // expose everything return this.$scopedSlots.default(this) }, methods: { // methods } }

Slide 86

Slide 86

// SelectDropdown.vue <SelectProvider v-bind=”$attrs” v-on=”$listeners”> <template slot-scope=”{ value, options, select, deselect, isOpen, open, close }”> <AppButton @click=“open”> {{ value || ‘Pick one’ }} </AppButton> <AppList v-if=”isOpen” :options=”options” @select=”select”/> </template> </SelectProvider>

Slide 87

Slide 87

Popular convention for classifying components Container aka smart components, providers Presentational aka dumb components, presenters

Slide 88

Slide 88

Container Presentational • Application logic • Application UI and styles • Application state • UI-related state only • Use Vuex • Receive data from props • Usually Router views • Emit events to containers • Reusable and composable • Not relying on global state

Slide 89

Slide 89

Container Presentational Examples: Examples: UserProfile, Product, TheShoppingCart, Login AppButton, AppModal, TheSidebar, ProductCard What is it doing? How does it look?

Slide 90

Slide 90

Should I always follow this convention?

Slide 91

Slide 91

Should I always follow this convention? NO

Slide 92

Slide 92

Should I always follow this convention? Not when: • It leads to premature optimisations • It makes simple things unnecessarily complex • It requires you to create strongly coupled components (like feature-aware props in otherwise reusable components) • It forces you to create unnecessary, one-time-use presenter components

Slide 93

Slide 93

Should I always follow this convention? Instead • Focus on keeping things simple (methods, props, template, Vuex modules, everything) • Don’t be afraid to have UI and styles in your containers • Split large, complicated containers into several smaller ones

Slide 94

Slide 94

Premature optimization is the root of all evil (or at least most of it) in programming. - Donald Knuth

Slide 95

Slide 95

Data Driven Refactoring

Slide 96

Slide 96

Signs you need more components • When your components are hard to understand • You feel a fragment of a component could use its own state • Hard to describe what what the component is actually responsible for

Slide 97

Slide 97

Components and how to find them? • Look for similar visual designs • Look for repeating interface fragments • Look for multiple/mixed responsibilities • Look for complicated data paths • Look for v-for loops • Look for large components

Slide 98

Slide 98

Composition API http://bit.ly/compositionapi

Slide 99

Slide 99

Thank you! www.bencodezen.io