Fundamental Component Design Patterns 2019 @bencodezen
A presentation at Connect.Tech in October 2019 in Atlanta, GA, USA by Ben Hong
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