Fundamental Component Design Patterns 2019 @bencodezen

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

Before we get started…

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

Component Basics

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>

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>

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>

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

Why Components?

Build things faster

No more repetitive code

Less bugs means you can relax

TECHNIQUE Props

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

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>

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>

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

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

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

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

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

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

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

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

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

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

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

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

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

props-based solution Is it wrong?

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

props-based solution Is it good, then?

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

props-based solution Problems

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

Is there a better another alternative?

Is there a better another alternative?

TECHNIQUE Slots

Recommended solution

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

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

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>

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>

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>

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>

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>

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>

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

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

Slots changes in Vue v2.6

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>

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>

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>

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>

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>

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>

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

Slots > Props

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

DESIGN PATTERN Transparent Components

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

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>

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>

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

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>

DESIGN PATTERN Wrap Vendor Components

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

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

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

DESIGN PATTERN Provider Components

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

“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

<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

<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

<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

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

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

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

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

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

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

Should I always follow this convention?

Should I always follow this convention? NO

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

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

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

Data Driven Refactoring

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

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

Composition API http://bit.ly/compositionapi

Thank you! www.bencodezen.io