Vuejs
This coding style guide extends the recommendations from Vuejs. If something is not mentionned refers youself to the Vuejs Guideline to make a decision.
It also serves as a reference on how to do certain things.
IDE tools and extensions
Structure of a Vuejs project
The structure is intended to organise logically the code.
├── dist // generated
├── public
├── src
| ├── components
| ├── filters
| ├── helpers
| ├── plugins
| ├── router
| ├── vuex
| ├── App.vue
| └── main.js
└── tests
├── e2e
└── unit
Components
Components organsie the code by functionnality and exist to maximize code reuse.
├── base
└── user
├── Profile.vue
└── ProfileApp.vue
All components suffixed by App
combine other components to create a fully woring page or 'app' that is mapped to a route. A component can import component from an other component, typically headers would come from base
components unless the header is so specific.
Often the ...App.vue
component are purely functional or higher order
components and do not contain design but only a function that renders the elements while applying some logic
import SimpleHeader from "@/components/base/SimpleHeader.vue";
import SiteFooter from "@/components/base/SiteFooter.vue";
import Login from "@/components/authentication/Login.vue";
import UserProfile from "@/components/user/Profile.vue";
import { mapGetters } from "vuex";
export default {
name: "profile-app",
components: { SimpleHeader, Login, UserProfile, SiteFooter },
computed: {
...mapGetters("authentication", ["isAuthenticated"])
},
render(h) {
const tag = this.isAuthenticated ? "user-profile" : "login";
return h(
"div",
{
style: {
width: "100%"
}
},
[h("simple-header"), h(tag), h(SiteFooter)]
);
}
};
TIP
the h()
rendering function is a convention
Vuex
Vuex is the the data store for the application, its the data layer. API calls happen in action files. Vuex is organised in modules. Each modules somewhat reflects the components structure.
├── modules
└── store.js
Module Structure:
└── module-name
├── actions.js
├── getters.js
├── mutation-types.js
├── mutations.js
└── store.js
Each module should be:
- name spaced
- imported in the main store
import moduleName from "./modules/module-name/store";
export default new Vuex.Store({
modules: {
moduleName,
// ...
moduleName
will be the name space of the module. More on this below.
Actions
Actions can be asynchronous. This is typically where API calls are done. Here is an example of an HTTP request that requires authentication.
export const getArticle = async ({ commit, rootState, state }) => {
const type = state.type;
const route = `training-data?type=${type}`;
const { ok, response, error } = await sureThing(
HTTP.get(route, {
headers: {
Authorization: `Bearer ${rootState.authentication.token}`
}
})
);
ok
? commit(types.SET_ARTICLE, response.data)
: commit(types.SET_ERROR, error);
commit(types.SET_TYPE, state.type === "content" ? "title" : "content");
};
Each action needs to be exported in order to be usable elsewhere in the app, notice the async/await
keywords. sureThing()
is a wrapper for HTTP requests catches errors. It allows to clean up functions.
Getters
Getters are simple functions that transforms reactivly the data that is in the store.
export const interestsMethods = state => {
return state.interests.reduce((a, c) => {
if (c.title === "methods") {
a.push(c.values);
}
return a;
}, [])[0];
};
Getters also need to be exported. To be available.
Mutations Types
Mutations Types allow to change the names of mutation and to set them in one place. This also serves as "documentations" as it list all the available mutations in a condensed manner.
export const SET_USER = "SET_USER";
Mutations
Mutations are synchronus. They just commit or save data to the store.
export default {
[types.SET_ERROR](state, data) {
state.error = data;
}
// ...
You can do some transformation and or logic before saving if necessary. For example set a modification timestamp etc.
Store
The store puts together actions, mutations and getters here is an example:
import * as actions from "./actions";
import * as mutations from "./mutations";
import * as getters from "./getters";
export default {
namespaced: true,
actions,
getters,
state: {
error: null,
// ...
},
mutations: mutations.default
};
Notice the namespaced properties set to true, this is mandatory. In the state object define all the variable that your component needs. Rember that in the end this create one store, so you do not need to repeat states, use it from other namespaced store by using the rootState
.
Get a store value in an action:
rootState.<name space>.<property>
Dispatch an action from the same module:
dispatch("actionName", data);
Dispatch an action from an other module:
dispatch("nameSpace/actionName", data, { root: true });
Commit data to the current store:
commit(types.<TYPE>, data),
Commit data to an other store:
commit("nameSpace/TYPE", data, { root: true })
Import States, Getters, Actions in a vue compoenent
Vuex comes with nice short cuts to import methods
import { mapActions, mapGetters, mapState } from "vuex";
export default {
computed: {
...mapState("nameSpace", ["stateName"]),
...mapState("otherNameSpace", ["stateName"]),
...mapGetters("nameSpace", ["getterName"]),
// ...
},
methods: {
...mapActions("nameSpace", ["actionName"])
//...
}
Filters
Filters are very useful as the allow to transform data just before displaying it . The
index.js
file is the main entry point of this module and just import and exports all the filters.
├── index.js
└── filterName.js
They are exported as an object so that we could destructure it while importing.
import { slugify } from "./slugify";
import { upperCase, uperCaseEachWords, uperCaseFirstLetter } from "./toUpper";
export { slugify, upperCase, uperCaseEachWords, uperCaseFirstLetter };
This is important as it allows to import only what is necessary and therefore keep to the minimum the size of a component.
Helpers
Plugins
Router
The router maps routes to components and applyse 'guards' to protect routes and redirect when necessary. If the router gets too big, it can be split in modules.
The paths.js
is used to generate the main navigation. The idea is that this was replaced by an API call and the returned data cached locally. One of the @TODO is to generate routes based on the path.js
├── index.js
└── paths.js
ESLint
Never disable eslint rules unless you have a good reason. You may see a lot of legacy files with /* eslint-disable some-rule, some-other-rule */ at the top, but legacy files are a special case. Any time you develop a new feature or refactor an existing one, you should abide by the eslint rules.
Never Ever EVER disable eslint globally for a file
// bad
/* eslint-disable */
// better
/* eslint-disable some-rule, some-other-rule */
// best
// nothing :)
If you do need to disable a rule for a single violation, try to do it as locally as possible
// bad
/* eslint-disable no-new */
import Foo from 'foo';
new Foo();
// better
import Foo from 'foo';
// eslint-disable-next-line no-new
new Foo();
When they are needed always place ESlint directive comment blocks on the first line of a script, followed by any global declarations, then a blank newline prior to any imports or code.
// bad
/* global Foo */
/* eslint-disable no-new */
import Bar from './bar';
// good
/* eslint-disable no-new */
/* global Foo */
import Bar from './bar';
Never disable the no-undef
rule. Declare globals with /* global Foo */ instead.
## Modules, Imports, and Exports Use ES module syntax to import modules
// bad
const SomeClass = require('some_class');
// good
import SomeClass from 'some_class';
// bad
module.exports = SomeClass;
// good
export default SomeClass;
Relative paths: when importing a module in the same directory, a child directory, or an immediate parent directory prefer relative paths. When importing a module which is two or more levels up, prefer either ~/
or @/
. @/
is an alias for src
In src/components/my-module/subdir:
// bad
import Foo from '~/my-feature/foo';
import Bar from '~/my-feature/subdir/bar';
import Bin from '~/my-feature/subdir/lib/bin';
// good
import Foo from '../foo';
import Bar from './bar';
import Bin from './lib/bin';
In spec/javascripts:
// bad
import Foo from '../../app/assets/javascripts/my-feature/foo';
// good
import Foo from '~/my-feature/foo';
When referencing an EE component:
// bad
import Foo from '../../../../../ee/app/assets/javascripts/my-feature/ee-foo';
// good
import Foo from 'ee/my-feature/foo';
Avoid using IIFE. Although we have a lot of examples of files which wrap their contents in IIFEs (immediately-invoked function expressions). They were used in the past to create scoped module. This is no longer necessary.
Avoid adding to the global namespace.
// bad
window.MyClass = class { /* ... */ };
// good
export default class MyClass { /* ... */ }
Side effects are forbidden in any script which contains exports
// bad
export default class MyClass { /* ... */ }
document.addEventListener("DOMContentLoaded", function(event) {
new MyClass();
}
Naming
Extensions: Use .vue
extension for Vue components. Do not use .js
as file extension.
Reference Naming: Use PascalCase for their instances:
// bad
import cardBoard from 'cardBoard.vue'
components: {
cardBoard,
};
// good
import CardBoard from 'cardBoard.vue'
components: {
CardBoard,
};
Props Naming: Avoid using DOM component prop names.
Props Naming: Use kebab-case instead of camelCase to provide props in templates.
// bad
<component class="btn">
// good
<component css-class="btn">
// bad
<component myProp="prop" />
// good
<component my-prop="prop" />
Alignment
Follow these alignment styles for the template method:
With more than one attribute, all attributes should be on a new line:
// bad
<component v-if="bar"
param="baz" />
<button class="btn">Click me</button>
// good
<component
v-if="bar"
param="baz"
/>
<button class="btn">
Click me
</button>
The tag can be inline if there is only one attribute:
// good
<component bar="bar" />
// good
<component
bar="bar"
/>
// bad
<component
bar="bar" />
Quotes Always use double quotes " inside templates.
// bad
template: <button :class='style'>Button</button>
// good
template: <button :class="style">Button</button>
Props
Props should be declared as an object
// bad
props: ['foo']
// good
props: {
foo: {
type: String,
required: false,
default: 'bar'
}
}
Required key should always be provided when declaring a prop
// bad
props: {
foo: {
type: String,
}
}
// good
props: {
foo: {
type: String,
required: false,
default: 'bar'
}
}
Default key should be provided if the prop is not required. Note: There are some scenarios where we need to check for the existence of the property. On those a default key should not be provided.
// good
props: {
foo: {
type: String,
required: false,
}
}
// good
props: {
foo: {
type: String,
required: false,
default: 'bar'
}
}
// good
props: {
foo: {
type: String,
required: true
}
}
Data
data method should always be a function
// bad
data: {
foo: 'foo'
}
// good
data() {
return {
foo: 'foo'
};
}
Directives
Shorthand @ is preferable over v-on
// bad
<component v-on:click="eventHandler"/>
// good
<component @click="eventHandler"/>
Shorthand : is preferable over v-bind
// bad
<component v-bind:class="btn"/>
// good
<component :class="btn"/>
Closing tags
Prefer self closing component tags
// bad
<component></component>
// good
<component />
Ordering
### Tag order in .vue file
<template>
<!-- ... -->
</template>
<script>
// ...
</script>
// We don't use scoped styles but there are few instances of this
<style>
/* ... */
</style>
Properties in a Vue Component
The linter enforces that. Here is the detail about that.
:key
When using v-for you need to provide a unique :key attribute for each item.
If the elements of the array being iterated have an unique id it is advised to use it:
<div
v-for="item in items"
:key="item.id"
>
<!-- content -->
</div>
When the elements being iterated don't have a unique id, you can use the array index as the :key attribute
<div
v-for="(item, index) in items"
:key="index"
>
<!-- content -->
</div>
When using v-for with template and there is more than one child element, the :key values must be unique. It's advised to use kebab-case namespaces.
<template v-for="(item, index) in items">
<span :key="`span-${index}`"></span>
<button :key="`button-${index}`"></button>
</template>
When dealing with nested v-for use the same guidelines as above.
<div
v-for="item in items"
:key="item.id"
>
<span
v-for="element in array"
:key="element.id"
>
<!-- content -->
</span>
</div>