explanations
examples
stories
clarifications
what-ifs
code
experiment
one-on-one help
please clone it now
(no need to copy down code examples)
(for the privacy of participants)
<!-- MyComponent.vue -->
<script>
export default {
// ...
}
</script>
<template>
<!-- ... -->
</template>
<style>
/* ... */
</style>
(typically only 1 right way to do something)
(no data, computed, etc and no lifecycle)
<template functional>
<!-- ... -->
</template>
export default {
functional: true,
render (h, context) {
// ...
}
}
or
(especially for components rendered many times, e.g. with a v-for)
boilerplate: src/components/nav-bar-routes.vue
(using either the scoped or module attributes)
<style scoped>
/* ... */
</style>
<style module>
/* ... */
</style>
<style lang="scss">
.my-class {
& > .another-class {
@extend %placeholder-class;
&:hover {
// ...
}
}
}
</style>
Add a <label> to the _base-input-text.vue component, then style that label with a CSS module class.
Refactor the _base-button.vue component to use a render function instead of a template. Then make it a functional component.
<!-- MyComponent.vue -->
<script>
export default {
// ...
}
</script>
<template>
<!-- ... -->
</template>
<style>
/* ... */
</style>
easy to add a <template> or <style> later
Vue Loader provides fallback names that Vue looks for internally
no need to update imports after moving a component
folder scoping leads to lazily named files, because they don't have to be unique
boilerplate: src/components/_base-icon.vue
boilerplate: src/components/_base-input-text.vue
<script>
export default {
inheritAttrs: false,
model: {
event: 'update'
}
}
</script>
<template>
<input
v-bind="$attrs"
@input="$emit('update', $event.target.value)"
v-on="$listeners"
/>
</template>
<template>
<input v-bind="$attrs" />
</template>
import { reactive } from 'vue'
export default function useSearch(getResults) {
const state = reactive({
query: '',
results: [],
run() {
state.results = []
return getResults(state.query).then(results => {
state.results = results
})
}
})
return state
}
<script>
import useSearch from '@use/search'
import axios from 'axios'
export default {
setup() {
const productSearch = useSearch(query =>
axios
.get('/api/products', { params: { query } })
.then(response => response.data.products)
)
return { productSearch }
}
}
</script>
<template>
<input
v-model="productSearch.query"
@keydown.enter="productSearch.run"
/>
<ProductList :products="productSearch.results" />
</template>
Create a new base component, then use that component in another one without explicitly importing it
Create a <BaseSelect> component that acts as a transparent wrapper for a <select> element.
Add a prefix (e.g. "base-") to a component, then create a _globals.js file to import all the .vue files with that prefix
Refactor a component that wraps an element to be fully transparent
Refactor a single-instance component to use the "the-" prefix.
mapping paths to view components
markup shared between pages (header, navbar, sidebar, etc)
definitions for page-level components
beforeRouteEnter, beforeRouteUpdate, beforeRouteLeave
(often better to use route-level guards instead)
(other components can also access $route, but usually shouldn't)
can often use <keep-alive> with include if it's a problem
<router-view :key="$route.fullPath"/>
no more beforeRouteUpdate!
Page <title>, <meta name="description">, etc
export default {
metaInfo() {
return {
title: this.user.name,
meta: [
{
name: 'description',
content: `The user profile for ${this.user.name}.`,
},
],
}
},
// ...
}
<script>
import NavBar from '@components/nav-bar'
export default {
components: { NavBar },
}
</script>
<template>
<div :class="$style.container">
<NavBar/>
<slot/>
</div>
</template>
<style lang="scss" module>
@import '~@design';
.container {
min-width: $size-content-width-min;
max-width: $size-content-width-max;
margin: 0 auto;
}
</style>
View component renders here
<script>
import Layout from '@layouts/main'
export default {
components: { Layout },
}
</script>
<template>
<Layout>
<h1>Home Page</h1>
<img
src="@assets/images/logo.png"
alt="Logo"
>
</Layout>
</template>
Layout wraps the view's template
keep element selectors in App.vue or nested under a scoped class
e.g. for a tabbed interface in a dashboard
import store from '@state/store'
export default [
{
path: '/',
name: 'home',
component: require('@views/home').default,
},
{
path: '/login',
name: 'login',
component: require('@views/login').default,
beforeEnter(routeTo, routeFrom, next) {
// If the user is already logged in
if (store.getters['auth/loggedIn']) {
// Redirect to the home page instead
next({ name: 'home' })
} else {
// Continue to the login page
next()
}
},
},
// ...
}
Inline require
In-route guards keep the views simple
Access the store
(especially when using in-route guards to fetch data)
import NProgress from 'nprogress/nprogress'
const router = new VueRouter({ /* ... */ })
// After navigation is confirmed, but before resolving...
router.beforeEach((routeTo, routeFrom, next) => {
// If this isn't an initial page load...
if (routeFrom.name) {
// Start the route progress bar.
NProgress.start()
}
next()
})
// When each route is finished evaluating...
router.afterEach((routeTo, routeFrom) => {
// Complete the animation of the route progress bar.
NProgress.done()
})
Note: branded is better!
<script>
import Layout from '@layouts/main'
export default {
components: { Layout },
props: {
resource: {
type: String,
default: '',
},
},
}
</script>
<template>
<Layout>
<h1>
404
<span v-if="resource">
{{ resource }}
</span>
Not Found
</h1>
</Layout>
</template>
{
path: '/404',
name: '404',
component: require('@views/404').default,
props: true,
},
{
path: '*',
redirect: '404',
},
next({ name: '404', params: { resource: 'User' } })
1
2
3
{
path: '/admin',
name: 'admin-dashboard',
component: () => import('@views/admin'),
}
{
path: '/admin',
name: 'admin-dashboard',
component: require('@views/admin').default,
}
Add a new layout and use it inside an existing view component.
Add an /about page, with a path in routes.js pointing to a new about.vue view.
Make one route lazy.
Integrate Vue Router into your app, with a single “main” layout component and a single “home” view.
Use <router-view :key="$route.fullPath"> to simplify a view component.
Add vue-meta and specify metaInfo for a view component.
~== data properties
~== computed properties
synchronous functions
that update state
store.commit('COMMIT_NAME')
~== methods
store.dispatch('actionName')
const store = new Vuex.Store({
modules: {
todos: {
state: {
all: [],
},
getters: {
completedTodos(state) {
return state.all.filter(todo =>
todo.isComplete
)
},
},
mutations: {
PUSH_TODO(state, newTodo) {
state.all.push(newTodo)
},
},
actions: {
addTodo({ commit }, newTodo) {
commit('PUSH_TODO', newTodo)
},
},
},
},
})
store.getters.completedTodos
store.state.todos.all
store.commit('PUSH_TODO')
store.dispatch('addTodo')
const store = new Vuex.Store({
modules: {
todos: {
namespaced: true,
state: {
all: [],
},
getters: {
completed(state) {
return state.all.filter(todo =>
todo.isComplete
)
},
},
mutations: {
PUSH(state, newTodo) {
state.all.push(newTodo)
},
},
actions: {
add({ commit }, newTodo) {
commit('PUSH', newTodo)
},
},
},
},
})
store.getters['todos/completed']
store.state.todos.all
store.commit('todos/PUSH')
store.dispatch('todos/add')
export const namespaced = true
export const state = {
all: [],
}
export const getters = {
completed(state) {
return state.all.filter(todo => todo.isComplete)
},
}
export const mutations = {
PUSH(state, newTodo) {
state.all.push(newTodo)
},
}
export const actions = {
add({ commit }, newTodo) {
commit('PUSH', newTodo)
},
}
allows flexible organization
import Vue from 'vue'
import Vuex from 'vuex'
import modules from './modules'
Vue.use(Vuex)
const store = new Vuex.Store({ modules })
boilerplate: src/state/modules/index.js
const store = new Vuex.Store({ modules })
// Automatically run the `init` action for
// every module, if one exists.
for (const moduleName of Object.keys(modules)) {
if (modules[moduleName].actions.init) {
store.dispatch(`${moduleName}/init`)
}
}
modules[modulePath.pop()] = {
// Modules are namespaced by default
namespaced: true,
...requireModule(fileName),
}
boilerplate:
boilerplate:
import { mapState, mapGetters, mapActions } from 'vuex'
export const authComputed = {
...mapState('auth', {
currentUser: state => state.currentUser,
}),
...mapGetters('auth', ['loggedIn']),
}
export const authMethods = mapActions('auth',
['logIn', 'logOut']
)
boilerplate: src/state/helpers.js
import {
authComputed,
authActions
} from '@state/helpers'
export default {
computed: {
...authComputed,
},
methods: {
...authActions,
},
}
Create a new shoppingList module to store shopping list items.
Create helpers for the shoppingList module in src/state/helpers.js.
In src/router/views/home.vue, import some state helpers to display, add, and remove items from the shopping list.
Add Vuex to your app with a single shoppingList module, as described to the left.
Automatically register all your Vuex modules.
Refactor a module to be namespaced, updating all references to that module.
Automatically dispatch an init action for your modules.
we stop writing and running tests when they're NOT
Everyone knows when
tests are failing
Tests don't fail intermittently
Tests run quickly
When a test fails,
it's easy to learn why
they should be able to run with it.only
manually manipulate your app instead
think from the user's perspective, or select elements by intent
yarn dev:e2e in the boilerplate
Write a new e2e test for one of the features you wrote in a previous practice round.
Develop a small new feature using test-driven development with yarn dev:e2e. That means:
First write the test
Then develop the feature
Follow these instructions to integrate Cypress into your app.
Refactor a test targeting classes to use content or data-testid instead.
Set up Circle CI for your app, including e2e tests.
(currently in beta)
to full test a component's interface
including shallow rendering, to avoid testing subcomponents
uses JSDOM to simulate the browser
CSS only experimentally supported
aliases, dynamic imports, etc require additional config
e.g. asserting that a data, computed, etc property exists
it's easier to add a new test when the file already exists
big components often scare people, so they go untested
otherwise, a problem in a common component can breaks many tests
run unit tests against staged files on a pre-commit hook
make sure you know when tests are failing
create helpers and custom matchers to simplify complex assertions
(e.g. hello.vue and hello.unit.js)
Refactor an existing component test to use Vue Test Utils (try the shallow render).
Delete a test that confirms Vue works.
Refactor your unit tests to be side-by-side with your source files.
great tooling can not only catch typos and potential errors, but also teach new features as developers need them
Faster reviews, more learning, more motivating
when developers feel cared for by their tooling, they take more pride in their work and write better code
people get excited when they see a great architecture laid out
when trainees review doc PRs, training is made reusable
module.exports = {
'*.{js,jsx}': [
'eslint --fix',
'prettier --write',
'git add',
'jest --bail --findRelatedTests',
],
'*.json': ['prettier --write', 'git add'],
'*.vue': [
'eslint --fix',
'stylelint --fix',
'prettier --write',
'git add',
'jest --no-cache --bail --findRelatedTests',
],
'*.scss': ['stylelint --fix', 'prettier --write', 'git add'],
'*.md': ['markdownlint', 'prettier --write', 'git add'],
'*.{png,jpeg,jpg,gif,svg}': ['imagemin-lint-staged', 'git add'],
}
speed up development and simplify refactors
e.g. automatically generate a unit test with each module
if (process.env.NODE_ENV !== 'production') {
Vue.mixin({
// Use lifecycle hooks to scan components for
// patterns that should be avoided.
})
}
Add one linter to your app, run with a lint command in package.json.
Use lint-staged to either lint or run unit tests against staged files (or both).
Add a meta validation to a component that your team has previously misused.
we stop writing and running tests when they're NOT
Everyone knows when
tests are failing
Tests don't fail intermittently
Tests run quickly
When a test fails,
it's easy to learn why
they should be able to run with it.only
manually manipulate your app instead
think from the user's perspective, or select elements by intent
yarn dev:e2e in the boilerplate
Develop a small new feature using test-driven development with yarn dev:e2e. That means:
First write the test
Then develop the feature
Follow these instructions to integrate Cypress into your app.
Refactor a test targeting classes to use content or data-testid instead.
Set up Circle CI for your app, including e2e tests.
(currently in beta)
to full test a component's interface
including shallow rendering, to avoid testing subcomponents
uses JSDOM to simulate the browser
aliases, dynamic imports, etc require additional config
e.g. asserting that a data, computed, etc property exists
it's easier to add a new test when the file already exists
big components often scare people, so they go untested
otherwise, a problem in a common component can breaks many tests
run unit tests against staged files on a pre-commit hook
make sure you know when tests are failing
create helpers and custom matchers to simplify complex assertions
(e.g. hello.vue and hello.unit.js)
Refactor an existing component test to use Vue Test Utils (try the shallow render).
Delete a test that confirms Vue works.
Refactor your unit tests to be side-by-side with your source files.