Adds vue js example application and documentation
This commit is contained in:
parent
381e3484ed
commit
c7b3ba834b
6 changed files with 408 additions and 16 deletions
BIN
doc/development/fe_guide/img/boards_diagram.png
Normal file
BIN
doc/development/fe_guide/img/boards_diagram.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
doc/development/fe_guide/img/vue_arch.png
Normal file
BIN
doc/development/fe_guide/img/vue_arch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.6 KiB |
|
@ -25,6 +25,59 @@ Working with our frontend assets requires Node (v4.3 or greater) and Yarn
|
|||
|
||||
For our currently-supported browsers, see our [requirements][requirements].
|
||||
|
||||
---
|
||||
|
||||
## Development Process
|
||||
|
||||
When you are assigned an issue please follow the next steps:
|
||||
|
||||
### Divide a big feature into small Merge Requests
|
||||
1. Big Merge Request are painful to review. In order to make this process easier we
|
||||
must break a big feature into smaller ones and create a Merge Request for each step.
|
||||
1. First step is to create a branch from `master`, let's call it `new-feature`. This branch
|
||||
will be the recipient of all the smaller Merge Requests. Only this one will be merged to master.
|
||||
1. Don't do any work on this one, let's keep it synced with master.
|
||||
1. Create a new branch from `new-feature`, let's call it `new-feature-step-1`. We advise you
|
||||
to clearly identify which step the branch represents.
|
||||
1. Do the first part of the modifications in this branch. The target branch of this Merge Request
|
||||
should be `new-feature`.
|
||||
1. Once `new-feature-step-1` gets merged into `new-feature` we can continue our work. Create a new
|
||||
branch from `new-feature`, let's call it `new-feature-step-2` and repeat the process done before.
|
||||
|
||||
```shell
|
||||
* master
|
||||
|\
|
||||
| * new-feature
|
||||
| |\
|
||||
| | * new-feature-step-1
|
||||
| |\
|
||||
| | * new-feature-step-2
|
||||
| |\
|
||||
| | * new-feature-step-3
|
||||
```
|
||||
|
||||
**Tips**
|
||||
- Make sure `new-feature` branch is always synced with `master`: merge master frequently.
|
||||
- Do the same for the feature branch you have opened. This can be accomplished by merging `master` into `new-feature` and `new-feature` into `new-feature-step-*`
|
||||
- Avoid rewriting history.
|
||||
|
||||
### Share your work early
|
||||
1. Before writing code guarantee your vision of the architecture is aligned with
|
||||
GitLab's architecture.
|
||||
1. Add a diagram to the issue and ask a Frontend Architecture about it.
|
||||
|
||||
![Diagram of Issue Boards Architecture](img/boards_diagram.png)
|
||||
|
||||
1. Don't take more than one week between starting work on a feature and
|
||||
sharing a Merge Request with a reviewer or a maintainer.
|
||||
|
||||
### Vue features
|
||||
1. Follow the steps in [Vue.js Best Practices](vue.md)
|
||||
1. Follow the style guide.
|
||||
1. Only a handful of people are allowed to merge Vue related features.
|
||||
Reach out to @jschatz, @iamphill, @fatihacet or @filipa early in this process.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## [Architecture](architecture.md)
|
||||
|
|
|
@ -58,7 +58,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
|
|||
import Bar from './bar';
|
||||
```
|
||||
|
||||
- **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead.
|
||||
- **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead.
|
||||
|
||||
- When declaring multiple globals, always use one `/* global [name] */` line per variable.
|
||||
|
||||
|
@ -183,6 +183,19 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
|
|||
parseInt('10', 10);
|
||||
```
|
||||
|
||||
#### CSS classes used for JavaScript
|
||||
- If the class is being used in Javascript it needs to be prepend with `js-`
|
||||
```html
|
||||
// bad
|
||||
<button class="add-user">
|
||||
Add User
|
||||
</button>
|
||||
|
||||
// good
|
||||
<button class="js-add-user">
|
||||
Add User
|
||||
</button>
|
||||
```
|
||||
|
||||
### Vue.js
|
||||
|
||||
|
@ -200,6 +213,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
|
|||
#### Naming
|
||||
- **Extensions**: Use `.vue` extension for Vue components.
|
||||
- **Reference Naming**: Use PascalCase for Vue components and camelCase for their instances:
|
||||
|
||||
```javascript
|
||||
// bad
|
||||
import cardBoard from 'cardBoard';
|
||||
|
@ -217,6 +231,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
|
|||
cardBoard: CardBoard
|
||||
};
|
||||
```
|
||||
|
||||
- **Props Naming:**
|
||||
- Avoid using DOM component prop names.
|
||||
- Use kebab-case instead of camelCase to provide props in templates.
|
||||
|
@ -243,12 +258,18 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
|
|||
<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>
|
||||
|
||||
// if props fit in one line then keep it on the same line
|
||||
<component bar="bar" />
|
||||
```
|
||||
|
|
|
@ -26,6 +26,10 @@ browser and you will not have access to certain APIs, such as
|
|||
[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
|
||||
which will have to be stubbed.
|
||||
|
||||
### Writing tests
|
||||
### Vue.js unit tests
|
||||
See this [section][vue-test].
|
||||
|
||||
### Running frontend tests
|
||||
|
||||
`rake karma` runs the frontend-only (JavaScript) tests.
|
||||
|
@ -134,3 +138,4 @@ Scenario: Developer can approve merge request
|
|||
[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html
|
||||
[jasmine-jquery]: https://github.com/velesin/jasmine-jquery
|
||||
[karma]: http://karma-runner.github.io/
|
||||
[vue-test]:https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components
|
||||
|
|
|
@ -19,13 +19,31 @@ We don't want to refactor all GitLab frontend code into Vue.js, here are some gu
|
|||
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 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.
|
||||
|
||||
## How to build a new feature with Vue.js
|
||||
## Vue architecture
|
||||
|
||||
**Components, Stores and Services**
|
||||
All new features built with Vue.js must follow a [Flux architecture][flux].
|
||||
The main goal we are trying to achieve is to have only one data flow and only one data entry.
|
||||
In order to achieve this goal, each Vue bundle needs a Store - where we keep all the data -,
|
||||
a Service - that we use to communicate with the server - and a main Vue component.
|
||||
|
||||
Think of the Main Vue Component as the entry point of your application. This is the only smart
|
||||
component that should exist in each Vue feature.
|
||||
This component is responsible for:
|
||||
1. Calling the Service to get data from the server
|
||||
1. Calling the Store to store the data received
|
||||
1. Mounting all the other components
|
||||
|
||||
![Vue Architecture](img/vue_arch.png)
|
||||
|
||||
You can also read about this architecture in vue docs about [state management][state-management]
|
||||
and about [one way data flow][one-way-data-flow].
|
||||
|
||||
### Components, Stores and Services
|
||||
|
||||
In some features implemented with Vue.js, like the [issue board][issue-boards]
|
||||
or [environments table][environments-table]
|
||||
|
@ -46,16 +64,17 @@ _For consistency purposes, we recommend you to follow the same structure._
|
|||
|
||||
Let's look into each of them:
|
||||
|
||||
**A `*_bundle.js` file**
|
||||
### A `*_bundle.js` file
|
||||
|
||||
This is the index file of your new feature. This is where the root Vue instance
|
||||
of the new feature should be.
|
||||
|
||||
The Store and the Service should be imported and initialized in this file and provided as a prop to the main component.
|
||||
The Store and the Service should be imported and initialized in this file and
|
||||
provided as a prop to the main component.
|
||||
|
||||
Don't forget to follow [these steps.][page_specific_javascript]
|
||||
|
||||
**A folder for Components**
|
||||
### A folder for Components
|
||||
|
||||
This folder holds all components that are specific of this new feature.
|
||||
If you need to use or create a component that will probably be used somewhere
|
||||
|
@ -70,29 +89,320 @@ 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]
|
||||
|
||||
**A folder for the Store**
|
||||
### A folder for the Store
|
||||
|
||||
The Store is a class that allows us to manage the state in a single
|
||||
source of truth.
|
||||
source of truth. It is not aware of the service or the components.
|
||||
|
||||
The concept we are trying to follow is better explained by Vue documentation
|
||||
itself, please read this guide: [State Management][state-management]
|
||||
|
||||
**A folder for the Service**
|
||||
### A folder for the Service
|
||||
|
||||
The Service is used only to communicate with the server.
|
||||
It does not store or manipulate any data.
|
||||
We use [vue-resource][vue-resource-repo] to
|
||||
communicate with the server.
|
||||
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 [vue-resource][vue-resource-repo] to communicate with the server.
|
||||
|
||||
The [issue boards service][issue-boards-service]
|
||||
is a good example of this pattern.
|
||||
### End Result
|
||||
|
||||
The following example shows an application:
|
||||
|
||||
```javascript
|
||||
// store.js
|
||||
export default class Store {
|
||||
|
||||
/**
|
||||
* This is where we will iniatialize the state of our data.
|
||||
* Usually in a small SPA you don't need any options when starting the store. In the case you do
|
||||
* need guarantee it's an Object and it's documented.
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
|
||||
// Create a state object to handle all our data in the same place
|
||||
this.todos = []:
|
||||
}
|
||||
|
||||
setTodos(todos = []) {
|
||||
this.todos = todos;
|
||||
}
|
||||
|
||||
addTodo(todo) {
|
||||
this.todos.push(todo);
|
||||
}
|
||||
|
||||
removeTodo(todoID) {
|
||||
const state = this.todos;
|
||||
|
||||
const newState = state.filter((element) => {element.id !== todoID});
|
||||
|
||||
this.todos = newState;
|
||||
}
|
||||
}
|
||||
|
||||
// service.js
|
||||
import Vue from 'vue';
|
||||
import VueResource from 'vue-resource';
|
||||
import 'vue_shared/vue_resource_interceptor';
|
||||
|
||||
Vue.use(VueResource);
|
||||
|
||||
export default class Service {
|
||||
constructor(options) {
|
||||
this.todos = Vue.resource(endpoint.todosEndpoint);
|
||||
}
|
||||
|
||||
getTodos() {
|
||||
return this.todos.get();
|
||||
}
|
||||
|
||||
addTodo(todo) {
|
||||
return this.todos.put(todo);
|
||||
}
|
||||
}
|
||||
// todo_component.vue
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<h1>
|
||||
Title: {{data.title}}
|
||||
</h1>
|
||||
<p>
|
||||
{{data.text}}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
// todos_main_component.vue
|
||||
<script>
|
||||
import Store from 'store';
|
||||
import Service from 'service';
|
||||
import TodoComponent from 'todoComponent';
|
||||
export default {
|
||||
/**
|
||||
* 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
|
||||
* in the component.
|
||||
*
|
||||
* We need to access the store methods through all methods of our component.
|
||||
* We need to access the state of our store.
|
||||
*/
|
||||
data() {
|
||||
const store = new Store();
|
||||
|
||||
return {
|
||||
isLoading: false,
|
||||
store: store,
|
||||
todos: store.todos,
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
todo: TodoComponent,
|
||||
},
|
||||
|
||||
created() {
|
||||
this.service = new Service('todos');
|
||||
|
||||
this.getTodos();
|
||||
},
|
||||
|
||||
methods: {
|
||||
getTodos() {
|
||||
this.isLoading = true;
|
||||
|
||||
this.service.getTodos()
|
||||
.then(response => response.json())
|
||||
.then((response) => {
|
||||
this.store.setTodos(response);
|
||||
this.isLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.isLoading = false;
|
||||
// Show an error
|
||||
});
|
||||
},
|
||||
|
||||
addTodo(todo) {
|
||||
this.service.addTodo(todo)
|
||||
then(response => response.json())
|
||||
.then((response) => {
|
||||
this.store.addTodo(response);
|
||||
})
|
||||
.catch(() => {
|
||||
// Show an error
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="container">
|
||||
<div v-if="isLoading">
|
||||
<i
|
||||
class="fa fa-spin fa-spinner"
|
||||
aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isLoading"
|
||||
class="js-todo-list">
|
||||
<template v-for='todo in todos'>
|
||||
<todo :data="todo" />
|
||||
</template>
|
||||
|
||||
<button
|
||||
@click="addTodo"
|
||||
class="js-add-todo">
|
||||
Add Todo
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
</template>
|
||||
|
||||
// bundle.js
|
||||
import todoComponent from 'todos_main_component.vue';
|
||||
|
||||
new Vue({
|
||||
el: '.js-todo-app',
|
||||
components: {
|
||||
todoComponent,
|
||||
},
|
||||
render: createElement => createElement('todo-component' {
|
||||
props: {
|
||||
someProp: [],
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
The [issue boards service][issue-boards-service] is a good example of this pattern.
|
||||
|
||||
## Style guide
|
||||
|
||||
Please refer to the Vue section of our [style guide](style_guide_js.md#vuejs)
|
||||
for best practices while writing your Vue components and templates.
|
||||
|
||||
## Testing Vue Components
|
||||
|
||||
Each Vue component has a unique output. This output is always present in the render function.
|
||||
|
||||
Although we can test each method of a Vue component individually, our goal must be to test the output
|
||||
of the render/template function, which represents the state at all times.
|
||||
|
||||
Make use of Vue Resource Interceptors to mock data returned by the service.
|
||||
|
||||
Here's how we would test the Todo App above:
|
||||
|
||||
```javascript
|
||||
import component from 'todos_main_component';
|
||||
|
||||
describe('Todos App', () => {
|
||||
it('should render the loading state while the request is being made', () => {
|
||||
const Component = Vue.extend(component);
|
||||
|
||||
const vm = new Component().$mount();
|
||||
|
||||
expect(vm.$el.querySelector('i.fa-spin')).toBeDefined();
|
||||
});
|
||||
|
||||
describe('with data', () => {
|
||||
// Mock the service to return data
|
||||
const interceptor = (request, next) => {
|
||||
next(request.respondWith(JSON.stringify([{
|
||||
title: 'This is a todo',
|
||||
body: 'This is the text'
|
||||
}]), {
|
||||
status: 200,
|
||||
}));
|
||||
};
|
||||
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
Vue.http.interceptors.push(interceptor);
|
||||
|
||||
const Component = Vue.extend(component);
|
||||
|
||||
vm = new Component().$mount();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
|
||||
});
|
||||
|
||||
|
||||
it('should render todos', (done) => {
|
||||
setTimeout(() => {
|
||||
expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(1);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add todo', () => {
|
||||
let vm;
|
||||
beforeEach(() => {
|
||||
const Component = Vue.extend(component);
|
||||
vm = new Component().$mount();
|
||||
});
|
||||
it('should add a todos', (done) => {
|
||||
setTimeout(() => {
|
||||
vm.$el.querySelector('.js-add-todo').click();
|
||||
|
||||
// Add a new interceptor to mock the add Todo request
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(2);
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Stubbing API responses
|
||||
[Vue Resource Interceptors][vue-resource-interceptor] allow us to add a interceptor with
|
||||
the response we need:
|
||||
|
||||
```javascript
|
||||
// Mock the service to return data
|
||||
const interceptor = (request, next) => {
|
||||
next(request.respondWith(JSON.stringify([{
|
||||
title: 'This is a todo',
|
||||
body: 'This is the text'
|
||||
}]), {
|
||||
status: 200,
|
||||
}));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Vue.http.interceptors.push(interceptor);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
|
||||
});
|
||||
|
||||
it('should do something', (done) => {
|
||||
setTimeout(() => {
|
||||
// Test received data
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
[vue-docs]: http://vuejs.org/guide/index.html
|
||||
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
|
||||
|
@ -100,5 +410,8 @@ for best practices while writing your Vue components and templates.
|
|||
[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript
|
||||
[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
|
||||
[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
|
||||
[one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow
|
||||
[vue-resource-repo]: https://github.com/pagekit/vue-resource
|
||||
[vue-resource-interceptor]: https://github.com/pagekit/vue-resource/blob/develop/docs/http.md#interceptors
|
||||
[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
|
||||
|
|
Loading…
Reference in a new issue