Front-end best practices (featuring Vue.js)
Champi
Posted on June 20, 2019
Best practices for Front-end Web Development
CSS
BEM
Block element modifier a concept for writing maintainable css.
Writing css may be a burden for some developers. Complex designs require large/maintainable code that can be easily modified.
That's what BEM is for. BEM stands for block, element, modifier and the concept is to always write our styles thinking about blocks.
E.g.
<button class='button'>
<span class='button__text'>Submit<span>
<img class='button__icon' src='icon.svg' alt='' />
</button>
.button {
display: flex;
align-items: center;
background: lightgreen;
padding: 0 1.6rem;
}
.button__text {
font-size: 2rem;
}
.button__icon {
width: 1.4rem;
height: 1.4rem;
}
A block is a meaningful chunk of markup that has one or more children elements (in the above example both the span and img elements are button's children).
An element is every single block's children.
What about modifiers?
What if the above submit button also needed a disabled
class that changes the button's background color to grey?
E.g. Vuejs
<button :class="{
'button': true,
'button--disabled': disabled
}">
<span class='button__text'>Submit<span>
<img class='button__icon' src='icon.svg' alt='' />
</button>
.button--disabled {
background: lightgrey;
}
Sass
Using colors, fonts, sizes and breakpoints.
Design systems are unified concepts that ensure consistency.
Front-end developers must make the effort of understanding those design systems and look for repetitive patterns before writing any line of code.
map-get
The best way to achieving consistency is having a single source of truth. In sass we have a utility called map-get
that makes that really easy.
E.g variables.scss
$colors: (
blue: #4286f4;
lightblue: #b8d1f9;
);
$font: (
main-family: sans-serif,
weight-thin: 300,
weight-medium: 500,
weight-fat: 700,
base-size: 62.5%,
smaller-size: 50%
);
$breakpoints: (
small: 50em,
medium: 76.8em,
large: 102.4em,
larger: 128em,
extra-large: 144em
);
$sizes: (
header-height: 6rem
);
E.g App.vue
<template>
<div class='app'>
<div class='app__header'>
<span class='header__text'>Hello World</span>
</div>
</div>
</template>
// use the scoped atrr in components
<style lang="scss">
@import "./variables";
html {
font-size: map-get($font, base-size);
font-family: map-get($font, main-family);
font-weight: map-get($font, weight-thin);
box-sizing: border-box;
@media screen and (max-width: map-get($breakpoints, small)) {
font-size: map-get($font, smaller-size);
}
}
.app {
display: flex;
flex-direction: column;
background: map-get($colors, blue);
&__header {
width: 100%;
height: map-get($sizes, header-height);
background: transparent;
display: flex;
justify-content: center;
align-items: center;
.header__text {
color: map-get($colors, lightblue);
}
}
}
</style>
Colors, fonts, sizes and breakpoints must be defined in variables.scss
and used when needed. You should avoid using values (for colors, fonts, sizes and breakpoints) that are not already defined in variables.scss
. If you need to create a new value (maybe the designer added a new color), add it to variables.scss
and then use it with map-get
.
Responsive design
About pixel perfect
Designers and developers share the goal of bringing a product to life, yet they use different tools for achieving it.
When a designer hands off a mockup he/she expects that the developer takes care of converting that mockup to a live web version. Often times what he/she forgets is that the web is a dynamic platform that changes it's width/height according to the user's device and therefore the design must adapt to this reality.
A practical view on pixel perfect web design
Mobile, tablet and desktop. Those are the most common screen resolutions that a website is designed for.
- Developers should strive to get as close to the mockups in those resolutions as possible and make sure that it still looks good in uncommon resolutions (resize the browser both horizontally and vertically, everything should look consistent and in place).
- Designers should help developers fix those situations when an uncommon resolution brakes the design.
Stop using px
, use rem
instead
px
is a fixed unit of measurement. Does fixed sound responsive to you?
No? Why are you still using it?
rem
is a relative unit of measument, it means that its value directly depens on a relative (root) value (commonly set with a font-size
targeting the html
tag). If that root value changes, the value expressed in rem
will change uniformly.
It's a good practice to set the html's font-size
to 10px
. This way it'll be easier to write your css thinking in px
(as you're used to) but using rem
for everything else.
E.g.
html {
font-size: 10px;
}
button {
font-size: 1rem; // equals 10px
}
span {
font-size: 1.6rem; // equals 16px
width: 20rem; // equals 200px
height: 14rem; // equals 140px
}
What's the benefit?
If we now change html's font-size
to 8px
, everything setted with rem
will now reevaluate to a 20%
less and therefore will look smaller.
E.g.
html {
font-size: 8px;
}
button {
font-size: 1rem; // now equals 8px
}
span {
font-size: 1.6rem; // now equals 12.8px
width: 20rem; // now equals 160px
height: 14rem; // now equals 112px
}
When to use %
Percentage units come in handy when you need to set the dimensions of an element's child (width/height).
More often than not you'll find yourself using flexbox
everytime you're setting dimensions with percentages.
E.g. Navbar.vue
<template>
<nav class='navbar'>
<a class='navbar__link' href="#pricing">Pricing</a>
<a class='navbar__link' href="#ourwork">Ourwork</a>
<a class='navbar__link' href="#about">About</a>
<a class='navbar__link' href="#legal">Legal</a>
</nav>
</template>
<style lang="scss" scoped>
// Whe may want to give the first 3 links more importance/space
.navbar {
display: flex;
// Setting the width with percentage will keep the links space distribution as we
intended even when the screen width changes
&__link {
width: 30%;
&:last-child {
width: 10%;
}
}
}
</style>
Writing media queries
Use em
in your media queries
In this link https://zellwk.com/blog/media-query-units/ you'll see an experiment writing media queries using px
, rem
and em
. The only unit that achieved consistency across browsers was the em
unit. Give that link a quick read and then come back.
Write as many media queries as you see fit
We usually have 3 mayor breakpoints (mobile, tablet and desktop) but this does not mean that these are the only breakpoints you should use. Start with those mayor breakpoints but then resize the screen and watch for broken/unconsistent elements, I guarantee that you'll find many resolutions where the adjustments that you already have in place will look just ugly.
Write your media queries inside rules, don't write a single media query for everything. This way you won't end up maintaning 2 code structures and you'll also be able to quickly understand how an element will change upon resolution changes.
E.g.
Code this
.container {
display: flex;
&__block {
width: 80%;
margin: 0 auto;
padding: 0 2.4rem;
@media screen and (max-width: 40em) {
width: 100%;
margin: unset;
}
@media screen and (max-width: 30em) {
padding: 0 1.6rem;
}
.block__text {
font-size: 1.6rem;
@media screen and (max-width: 40em) {
font-size: 1.4rem;
}
@media screen and (max-width: 30em) {
font-size: 1.2rem;
}
}
}
}
Not this
.container {
display: flex;
&__block {
width: 80%;
margin: 0 auto;
padding: 0 2.4rem;
.block__text {
font-size: 1.6rem;
}
}
}
@media screen and (max-width: 40em) {
.container {
&__block {
width: 100%;
margin: unset;
.block__text {
font-size: 1.4rem;
}
}
}
}
@media screen and (max-width: 30em) {
.container {
&__block {
padding: 0 1.6rem;
.block__text {
font-size: 1.2rem;
}
}
}
}
JS
Recommended reading
You don't know js
https://github.com/getify/You-Dont-Know-JS
This is a series of books that will show you the ins and outs of javascript
. If you are serious about js development you must read all of those books.
Clean code (for js)
https://github.com/ryanmcdermott/clean-code-javascript
Taken from Uncle Bob's original Clean code book I present to you the concepts/rules that will make your code more readable (for your future self or your colleagues).
Tooling
Modern Front-end development uses modern tools that make the development experience pretty smooth.
Nowadays almost every font-end project will use a linter, formatter/format-rules and a bundler.
The industry standards as of 2019 are:
- Linter: Eslint
- Formatter: Prettier (though I prefer Standard)
- Bundler: Webpack
If you're using the vue-cli
you don't need to worry about configuring these, just make sure that you manually select these options when creating a project:
- Babel
- CSS Pre-processors --> Sass/SCSS (with node-sass)
- Linter / Formatter --> ESLint + Standard config --> Lint and fix on commit
Vue.js
Style guide
https://vuejs.org/v2/style-guide/ Be sure to read the official Vuejs style guide. Following the advices given there will give consistency to the codebase and with the usage of a formatter it'll look even more standardized.
State management
When an application grows and starts becoming complex we find ourselves in a situation where we need to be careful with the way we organize our code.
The state management pattern aids in giving us clarity about our data flow. Use this pattern when your app has global data that many components may share.
Don't use this pattern for data generated by the ui (state inside your components) but rather use it for managing data that comes from the server.
Vuex
https://vuex.vuejs.org/ Vuex docs are crystal clear and you should take the time to read them and understand how to use it properly.
Here's some advice for organizing the store
folder.
Build a module for every meaningful concept of the data you're handling (tighly coupled with the api services)
Let's imagine that we're building the store
folder for an ecommerce app.
We'll need the following modules:
- auth (for handling user authentication)
- products (for handling the ecommerce products)
- cart (for handling the checkout)
Notes:
Assume that every api call will return
[ res ]
if 'success' and[ null, error ]
otherwise*
I'll expand on this in theApp architecture
section.Actions execute api calls and commit mutations as needed.
Actions return
{ status }
for it's usage in the component that called the action (this is useful for toggling styles upon success/error).
auth.js
import POSTauth from 'apiCalls/POSTauth'
export default {
state: {
token: '',
userName: ''
},
mutations: {
setToken(state, token) {
state.token = token
},
setUserName(state, userName) {
state.userName = userName
}
},
actions: {
async loginUser({ commit }, formData) {
const [res, error] = await POSTauth(formData)
if (error) {
return { status: 'error' }
} else {
commit('setToken', res.token)
commit('setUserName', res.userName)
return { status: 'success' }
}
},
},
getters: {
isAuth(state) {
return Boolean(state.token)
}
}
}
products.js
import GETproducts from 'apiCalls/GETproducts'
export default {
state: {
products: []
},
mutations: {
setProducts(state, products) {
state.products = products
}
},
actions: {
async getProducts({ commit }) {
const [res, error] = await GETproducts()
if (error) {
return { status: 'error' }
} else {
commit('setProducts', res.products)
return { status: 'success' }
}
},
}
}
cart.js
import POSTprocess_payment from 'apiCalls/POSTprocess_payment'
export default {
state: {
productsInBasket: []
},
mutations: {
handleProduct(state, { action, selectedProduct }) {
const addProduct = () =>
[...state.productsInBasket, selectedProduct]
const deleteProduct = () =>
state.productsInBasket.filter(prd => prd.id !== selectedProduct.id)
state.productsInBasket = action === 'add' ? addProduct() : deleteProduct()
}
},
actions: {
async processPayment({ state }) {
const [res, error] = await POSTprocess_payment(state.productsInBasket)
if (error) {
return { status: 'error' }
} else {
return { status: 'success' }
}
},
},
getters: {
totalPayment(state) {
return state.productsInBasket.reduce((a, b) => a.price + b.price)
}
}
}
store.js
import Vue from 'vue'
import Vuex from 'vuex'
import auth from './auth'
import products from './products'
import cart from './cart'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
auth,
products,
cart
}
})
export default store
Complex component state
There are cases where handling state with props or bus events can quickly become cumbersome. I'm talking about components that are complex by design/nature and therefore require careful thought in it's construction.
In case you're still not following let me ask you this question:
Have you ever written a component that passes down many props to a middle component that also passes down those props? (this is known as prop drilling)
If you answered yes keep reading, otherwise you may not need the pattern I'm about to describe.
Provide/Inject
https://vuejs.org/v2/api/#provide-inject This feature is similar to React's Context. It lets you define data in a parent component and make it available to all of its children no matter how deep the component hierarchy is without needing to manually pass it down.
With this feature available in our toolbelt we can now replicate a state management pattern for our complex components (consequently gaining clarity over data flow).
Let's build a signup form with two inputs (email and password) and a submit button.
- Each input should have it's on validation
- If an input is invalid it should apply an error style and also display and error message
- If the form is invalid the submit button should be disabled
App architecture
A key aspect of writing maintable apps is to conceptualize it as many layers, each one with it's own responsability, and together they form the bulk of the app.
Some people have referred to this as separation of concerns but the term hasn't been clarified enough to prevent developers into thinking that having separate files for the html
- css
- js
is separating concerns. Nope, that's not it, that's just separation of files.
separation of concerns is about defining responsability.
pages - components - networkLayer
I've come up with this pattern of having the following folders in my projects:
pages: Here are the files that are responsible for rendering an entire page to the screen (it may be also called views). These ones make calls to the store.
components: Here are the files that represent a unit to be used inside a Page.
-
networkLayer:
This is the layer that connects to the backend and manages the data flow.
- apiCalls: Here are all the apiCalls that can be made by the app. These ones are called within a store module action. These return either
[res]
or[null, error]
. The idea here is to separate the concepts of making a server request and managing the requested data. - store: Here is the state management setup to be used. A config file and a
modules/
folder containing the store modules.
- apiCalls: Here are all the apiCalls that can be made by the app. These ones are called within a store module action. These return either
E.g GETproducts.js
(an api call)
import axios from 'axios'
export const GETproducts = () =>
axios
.get(
'/products/'
)
.then((res) => [ res.data ])
.catch((error) => [ null, error ])
Posted on June 20, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.