diff options
author | Clement Ho <ClemMakesApps@gmail.com> | 2018-05-08 10:49:30 -0500 |
---|---|---|
committer | Clement Ho <ClemMakesApps@gmail.com> | 2018-05-08 10:49:30 -0500 |
commit | 5955caed321e242bfebe52a4b47346a01a50e4f6 (patch) | |
tree | d9710c0732ce21801b4a79a281bec0bd39582a95 /doc/development/fe_guide | |
parent | f9e2b4730f58ba630344c9554eb907ab003abbd5 (diff) | |
parent | 533593e95cd3a922a2ec2ea43b345862361dfd67 (diff) | |
download | gitlab-ce-5955caed321e242bfebe52a4b47346a01a50e4f6.tar.gz |
Merge branch 'master' into bootstrap4
Diffstat (limited to 'doc/development/fe_guide')
-rw-r--r-- | doc/development/fe_guide/icons.md | 2 | ||||
-rw-r--r-- | doc/development/fe_guide/index.md | 6 | ||||
-rw-r--r-- | doc/development/fe_guide/style_guide_js.md | 8 | ||||
-rw-r--r-- | doc/development/fe_guide/vue.md | 278 | ||||
-rw-r--r-- | doc/development/fe_guide/vuex.md | 358 |
5 files changed, 400 insertions, 252 deletions
diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md index b288ee95722..b469a9c6aef 100644 --- a/doc/development/fe_guide/icons.md +++ b/doc/development/fe_guide/icons.md @@ -49,7 +49,7 @@ Please use the following function inside JS to render an icon : All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency. -To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. The updated files should be tracked in Git as those are referenced. +To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs`. # SVG Illustrations diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index 3b4dfd50761..6d3796e7560 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -1,5 +1,8 @@ # Frontend Development Guidelines +> **Notice:** +We are currently in the process of re-writing our development guide to make it easier to find information. The new guide is still WIP but viewable in [development/new_fe_guide](../new_fe_guide/index.md) + This document describes various guidelines to ensure consistency and quality across GitLab's frontend team. @@ -45,6 +48,9 @@ Common JavaScript design patterns in GitLab's codebase. ## [Vue.js Best Practices](vue.md) Vue specific design patterns and practices. +## [Vuex](vuex.md) +Vuex specific design patterns and practices. + ## [Axios](axios.md) Axios specific practices and gotchas. diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md index ce35ca98750..284b4b53334 100644 --- a/doc/development/fe_guide/style_guide_js.md +++ b/doc/development/fe_guide/style_guide_js.md @@ -310,7 +310,7 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. })); ``` -1. Don not use a singleton for the service or the store +1. Do not use a singleton for the service or the store ```javascript // bad class Store { @@ -328,9 +328,11 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. } } ``` +1. Use `.vue` for Vue templates. Do not use `%template` in HAML. #### Naming -1. **Extensions**: Use `.vue` extension for Vue components. + +1. **Extensions**: Use `.vue` extension for Vue components. Do not use `.js` as file extension ([#34371]). 1. **Reference Naming**: Use PascalCase for their instances: ```javascript // bad @@ -364,6 +366,8 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. <component my-prop="prop" /> ``` +[#34371]: https://gitlab.com/gitlab-org/gitlab-ce/issues/34371 + #### Alignment 1. Follow these alignment styles for the template method: 1. With more than one attribute, all attributes should be on a new line: diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 9c4b0e86351..f971d8b7388 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -1,29 +1,7 @@ # Vue -For more complex frontend features, we recommend using Vue.js. It shares -some ideas with React.js as well as Angular. - To get started with Vue, read through [their documentation][vue-docs]. -## When to use Vue.js - -We recommend using Vue for more complex features. Here are some guidelines for when to use Vue.js: - -- If you are starting a new feature or refactoring an old one that highly interacts with the DOM; -- For real time data updates; -- If you are creating a component that will be reused elsewhere; - -## When not to use Vue.js - -We don't want to refactor all GitLab frontend code into Vue.js, here are some guidelines for -when not to use Vue.js: - -- Adding or changing static information; -- Features that highly depend on jQuery will be hard to work with Vue.js; -- Features without reactive data; - -As always, the Frontend Architectural Experts are available to help with any Vue or JavaScript questions. - ## Vue architecture All new features built with Vue.js must follow a [Flux architecture][flux]. @@ -57,15 +35,15 @@ new_feature │ └── ... ├── stores │ └── new_feature_store.js -├── services +├── services # only when not using vuex │ └── new_feature_service.js -├── new_feature_bundle.js +├── index.js ``` _For consistency purposes, we recommend you to follow the same structure._ Let's look into each of them: -### A `*_bundle.js` file +### A `index.js` file This is the index file of your new feature. This is where the root Vue instance of the new feature should be. @@ -144,30 +122,30 @@ in one table would not be a good use of this pattern. You can read more about components in Vue.js site, [Component System][component-system] #### Components Gotchas -1. Using SVGs in components: To use an SVG in a template we need to make it a property we can access through the component. -A `prop` and a property returned by the `data` functions require `vue` to set a `getter` and a `setter` for each of them. -The SVG should be a computed property in order to improve performance, note that computed properties are cached based on their dependencies. - -```javascript -// bad -import svg from 'svg.svg'; -data() { - return { - myIcon: svg, - }; -}; - -// good -import svg from 'svg.svg'; -computed: { - myIcon() { - return svg; - } -} -``` +1. Using SVGs icons in components: To use an SVG icon in a template use the `icon.vue` +1. Using SVGs illustrations in components: To use an SVG illustrations in a template provide the path as a prop and display it through a standard img tag. + ```javascript + <script> + export default { + props: { + svgIllustrationPath: { + type: String, + required: true, + }, + }, + }; + <script> + <template> + <img :src="svgIllustrationPath" /> + </template> + ``` ### A folder for the Store +#### Vuex +Check this [page](vuex.md) for more details. + +#### Flux like state management The Store is a class that allows us to manage the state in a single source of truth. It is not aware of the service or the components. @@ -176,6 +154,8 @@ itself, please read this guide: [State Management][state-management] ### A folder for the Service +**If you are using Vuex you won't need this step** + The Service is a class used only to communicate with the server. It does not store or manipulate any data. It is not aware of the store or the components. We use [axios][axios] to communicate with the server. @@ -273,6 +253,9 @@ import Store from 'store'; import Service from 'service'; import TodoComponent from 'todoComponent'; export default { + components: { + todo: TodoComponent, + }, /** * Although most data belongs in the store, each component it's own state. * We want to show a loading spinner while we are fetching the todos, this state belong @@ -291,10 +274,6 @@ export default { }; }, - components: { - todo: TodoComponent, - }, - created() { this.service = new Service('todos'); @@ -476,201 +455,6 @@ need to test the rendered output. [Vue][vue-test] guide's to unit test show us e Refer to [mock axios](axios.md#mock-axios-response-on-tests) -## Vuex -To manage the state of an application you may use [Vuex][vuex-docs]. - -_Note:_ All of the below is explained in more detail in the official [Vuex documentation][vuex-docs]. - -### Separation of concerns -Vuex is composed of State, Getters, Mutations, Actions and Modules. - -When a user clicks on an action, we need to `dispatch` it. This action will `commit` a mutation that will change the state. -_Note:_ The action itself will not update the state, only a mutation should update the state. - -#### File structure -When using Vuex at GitLab, separate this concerns into different files to improve readability. If you can, separate the Mutation Types as well: - -``` -└── store - ├── index.js # where we assemble modules and export the store - ├── actions.js # actions - ├── mutations.js # mutations - ├── getters.js # getters - └── mutation_types.js # mutation types -``` -The following examples show an application that lists and adds users to the state. - -##### `index.js` -This is the entry point for our store. You can use the following as a guide: - -```javascript -import Vue from 'vue'; -import Vuex from 'vuex'; -import * as actions from './actions'; -import * as getters from './getters'; -import mutations from './mutations'; - -Vue.use(Vuex); - -export default new Vuex.Store({ - actions, - getters, - mutations, - state: { - users: [], - }, -}); -``` -_Note:_ If the state of the application is too complex, an individual file for the state may be better. - -##### `actions.js` -An action commits a mutation. In this file, we will write the actions that will commit the respective mutation: - -```javascript - import * as types from './mutation_types'; - - export const addUser = ({ commit }, user) => { - commit(types.ADD_USER, user); - }; -``` - -To dispatch an action from a component, use the `mapActions` helper: -```javascript -import { mapActions } from 'vuex'; - -{ - methods: { - ...mapActions([ - 'addUser', - ]), - onClickUser(user) { - this.addUser(user); - }, - }, -}; -``` - -##### `getters.js` -Sometimes we may need to get derived state based on store state, like filtering for a specific prop. This can be done through the `getters`: - -```javascript -// get all the users with pets -export getUsersWithPets = (state, getters) => { - return state.users.filter(user => user.pet !== undefined); -}; -``` - -To access a getter from a component, use the `mapGetters` helper: -```javascript -import { mapGetters } from 'vuex'; - -{ - computed: { - ...mapGetters([ - 'getUsersWithPets', - ]), - }, -}; -``` - -##### `mutations.js` -The only way to actually change state in a Vuex store is by committing a mutation. - -```javascript - import * as types from './mutation_types'; - - export default { - [types.ADD_USER](state, user) { - state.users.push(user); - }, - }; -``` - -##### `mutations_types.js` -From [vuex mutations docs][vuex-mutations]: -> It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application. - -```javascript -export const ADD_USER = 'ADD_USER'; -``` - -### How to include the store in your application -The store should be included in the main component of your application: -```javascript - // app.vue - import store from 'store'; // it will include the index.js file - - export default { - name: 'application', - store, - ... - }; -``` - -### Vuex Gotchas -1. Avoid calling a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency through out the application. From Vuex docs: - - > why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action. - - ```javascript - // component.vue - - // bad - created() { - this.$store.commit('mutation'); - } - - // good - created() { - this.$store.dispatch('action'); - } - ``` -1. When possible, use mutation types instead of hardcoding strings. It will be less error prone. -1. The State will be accessible in all components descending from the use where the store is instantiated. - -### Testing Vuex -#### Testing Vuex concerns -Refer to [vuex docs][vuex-testing] regarding testing Actions, Getters and Mutations. - -#### Testing components that need a store -Smaller components might use `store` properties to access the data. -In order to write unit tests for those components, we need to include the store and provide the correct state: - -```javascript -//component_spec.js -import Vue from 'vue'; -import store from './store'; -import component from './component.vue' - -describe('component', () => { - let vm; - let Component; - - beforeEach(() => { - Component = Vue.extend(issueActions); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should show a user', () => { - const user = { - name: 'Foo', - age: '30', - }; - - // populate the store - store.dispatch('addUser', user); - - vm = new Component({ - store, - propsData: props, - }).$mount(); - }); -}); -``` - [vue-docs]: http://vuejs.org/guide/index.html [issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards [environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments @@ -681,9 +465,5 @@ describe('component', () => { [vue-test]: https://vuejs.org/v2/guide/unit-testing.html [issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 [flux]: https://facebook.github.io/flux -[vuex-docs]: https://vuex.vuejs.org -[vuex-structure]: https://vuex.vuejs.org/en/structure.html -[vuex-mutations]: https://vuex.vuejs.org/en/mutations.html -[vuex-testing]: https://vuex.vuejs.org/en/testing.html [axios]: https://github.com/axios/axios [axios-interceptors]: https://github.com/axios/axios#interceptors diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md new file mode 100644 index 00000000000..6a89bfc7721 --- /dev/null +++ b/doc/development/fe_guide/vuex.md @@ -0,0 +1,358 @@ +# Vuex +To manage the state of an application you should use [Vuex][vuex-docs]. + +_Note:_ All of the below is explained in more detail in the official [Vuex documentation][vuex-docs]. + +## Separation of concerns +Vuex is composed of State, Getters, Mutations, Actions and Modules. + +When a user clicks on an action, we need to `dispatch` it. This action will `commit` a mutation that will change the state. +_Note:_ The action itself will not update the state, only a mutation should update the state. + +## File structure +When using Vuex at GitLab, separate this concerns into different files to improve readability: + +``` +└── store + ├── index.js # where we assemble modules and export the store + ├── actions.js # actions + ├── mutations.js # mutations + ├── getters.js # getters + ├── state.js # state + └── mutation_types.js # mutation types +``` +The following example shows an application that lists and adds users to the state. +(For a more complex example implementation take a look at the security applications store in [here](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/ee/app/assets/javascripts/vue_shared/security_reports/store)) + +### `index.js` +This is the entry point for our store. You can use the following as a guide: + +```javascript +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; +import state from './state'; + +Vue.use(Vuex); + +export default new Vuex.Store({ + actions, + getters, + mutations, + state, +}); +``` + +### `state.js` +The first thing you should do before writing any code is to design the state. + +Often we need to provide data from haml to our Vue application. Let's store it in the state for better access. + +```javascript + export default { + endpoint: null, + + isLoading: false, + error: null, + + isAddingUser: false, + errorAddingUser: false, + + users: [], + }; +``` + +#### Access `state` properties +You can use `mapState` to access state properties in the components. + +### `actions.js` +An action is a playload of information to send data from our application to our store. + +An action is usually composed by a `type` and a `payload` and they describe what happened. +Enforcing that every change is described as an action lets us have a clear understanting of what is going on in the app. + +In this file, we will write the actions that will call the respective mutations: + +```javascript + import * as types from './mutation_types'; + import axios from '~/lib/utils/axios-utils'; + import createFlash from '~/flash'; + + export const requestUsers = ({ commit }) => commit(types.REQUEST_USERS); + export const receiveUsersSuccess = ({ commit }, data) => commit(types.RECEIVE_USERS_SUCCESS, data); + export const receiveUsersError = ({ commit }, error) => commit(types.REQUEST_USERS_ERROR, error); + + export const fetchUsers = ({ state, dispatch }) => { + dispatch('requestUsers'); + + axios.get(state.endoint) + .then(({ data }) => dispatch('receiveUsersSuccess', data)) + .catch((error) => { + dispatch('receiveUsersError', error) + createFlash('There was an error') + }); + } + + export const requestAddUser = ({ commit }) => commit(types.REQUEST_ADD_USER); + export const receiveAddUserSuccess = ({ commit }, data) => commit(types.RECEIVE_ADD_USER_SUCCESS, data); + export const receiveAddUserError = ({ commit }, error) => commit(types.REQUEST_ADD_USER_ERROR, error); + + export const addUser = ({ state, dispatch }, user) => { + dispatch('requestAddUser'); + + axios.post(state.endoint, user) + .then(({ data }) => dispatch('receiveAddUserSuccess', data)) + .catch((error) => dispatch('receiveAddUserError', error)); + } +``` + +#### Actions Pattern: `request` and `receive` namespaces +When a request is made we often want to show a loading state to the user. + +Instead of creating an action to toggle the loading state and dispatch it in the component, +create: +1. An action `requestSomething`, to toggle the loading state +1. An action `receiveSomethingSuccess`, to handle the success callback +1. An action `receiveSomethingError`, to handle the error callback +1. An action `fetchSomething` to make the request. + 1. In case your application does more than a `GET` request you can use these as examples: + 1. `PUT`: `createSomething` + 2. `POST`: `updateSomething` + 3. `DELETE`: `deleteSomething` + +The component MUST only dispatch the `fetchNamespace` action. Actions namespaced with `request` or `receive` should not be called from the component +The `fetch` action will be responsible to dispatch `requestNamespace`, `receiveNamespaceSuccess` and `receiveNamespaceError` + +By following this pattern we guarantee: +1. All aplications follow the same pattern, making it easier for anyone to maintain the code +1. All data in the application follows the same lifecycle pattern +1. Actions are contained and human friendly +1. Unit tests are easier +1. Actions are simple and straightforward + +#### Dispatching actions +To dispatch an action from a component, use the `mapActions` helper: +```javascript +import { mapActions } from 'vuex'; + +{ + methods: { + ...mapActions([ + 'addUser', + ]), + onClickUser(user) { + this.addUser(user); + }, + }, +}; +``` + +#### `mutations.js` +The mutations specify how the application state changes in response to actions sent to the store. +The only way to change state in a Vuex store should be by committing a mutation. + +**It's a good idea to think of the state before writing any code.** + +Remember that actions only describe that something happened, they don't describe how the application state changes. + +**Never commit a mutation directly from a component** + +```javascript + import * as types from './mutation_types'; + + export default { + [types.REQUEST_USERS](state) { + state.isLoading = true; + }, + [types.RECEIVE_USERS_SUCCESS](state, data) { + // Do any needed data transformation to the received payload here + state.users = data; + state.isLoading = false; + }, + [types.REQUEST_USERS_ERROR](state, error) { + state.isLoading = false; + }, + [types.REQUEST_ADD_USER](state, user) { + state.isAddingUser = true; + }, + [types.RECEIVE_ADD_USER_SUCCESS](state, user) { + state.isAddingUser = false; + state.users.push(user); + }, + [types.REQUEST_ADD_USER_ERROR](state, error) { + state.isAddingUser = true; + state.errorAddingUser = error∂; + }, + }; +``` + +#### `getters.js` +Sometimes we may need to get derived state based on store state, like filtering for a specific prop. +Using a getter will also cache the result based on dependencies due to [how computed props work](https://vuejs.org/v2/guide/computed.html#Computed-Caching-vs-Methods) +This can be done through the `getters`: + +```javascript +// get all the users with pets +export const getUsersWithPets = (state, getters) => { + return state.users.filter(user => user.pet !== undefined); +}; +``` + +To access a getter from a component, use the `mapGetters` helper: +```javascript +import { mapGetters } from 'vuex'; + +{ + computed: { + ...mapGetters([ + 'getUsersWithPets', + ]), + }, +}; +``` + +#### `mutations_types.js` +From [vuex mutations docs][vuex-mutations]: +> It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application. + +```javascript +export const ADD_USER = 'ADD_USER'; +``` + +### How to include the store in your application +The store should be included in the main component of your application: +```javascript + // app.vue + import store from 'store'; // it will include the index.js file + + export default { + name: 'application', + store, + ... + }; +``` + +### Communicating with the Store +```javascript +<script> +import { mapActions, mapState, mapGetters } from 'vuex'; +import store from './store'; + +export default { + store, + computed: { + ...mapGetters([ + 'getUsersWithPets' + ]), + ...mapState([ + 'isLoading', + 'users', + 'error', + ]), + }, + methods: { + ...mapActions([ + 'fetchUsers', + 'addUser', + ]), + + onClickAddUser(data) { + this.addUser(data); + } + }, + + created() { + this.fetchUsers() + } +} +</script> +<template> + <ul> + <li v-if="isLoading"> + Loading... + </li> + <li v-else-if="error"> + {{ error }} + </li> + <template v-else> + <li + v-for="user in users" + :key="user.id" + > + {{ user }} + </li> + </template> + </ul> +</template> +``` + +### Vuex Gotchas +1. Do not call a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency through out the application. From Vuex docs: + + > why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action. + + ```javascript + // component.vue + + // bad + created() { + this.$store.commit('mutation'); + } + + // good + created() { + this.$store.dispatch('action'); + } + ``` +1. Use mutation types instead of hardcoding strings. It will be less error prone. +1. The State will be accessible in all components descending from the use where the store is instantiated. + +### Testing Vuex +#### Testing Vuex concerns +Refer to [vuex docs][vuex-testing] regarding testing Actions, Getters and Mutations. + +#### Testing components that need a store +Smaller components might use `store` properties to access the data. +In order to write unit tests for those components, we need to include the store and provide the correct state: + +```javascript +//component_spec.js +import Vue from 'vue'; +import store from './store'; +import component from './component.vue' + +describe('component', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(issueActions); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should show a user', () => { + const user = { + name: 'Foo', + age: '30', + }; + + // populate the store + store.dipatch('addUser', user); + + vm = new Component({ + store, + propsData: props, + }).$mount(); + }); +}); +``` + +[vuex-docs]: https://vuex.vuejs.org +[vuex-structure]: https://vuex.vuejs.org/en/structure.html +[vuex-mutations]: https://vuex.vuejs.org/en/mutations.html +[vuex-testing]: https://vuex.vuejs.org/en/testing.html |