2017-03-22 15:30:54 -04:00
# Vue
To get started with Vue, read through [their documentation][vue-docs].
2018-07-12 08:10:53 -04:00
## Examples
2017-03-22 15:30:54 -04:00
2018-07-12 08:10:53 -04:00
What is described in the following sections can be found in these examples:
2017-09-05 03:39:52 -04:00
2018-07-12 08:10:53 -04:00
- web ide: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/ide/stores
- security products: https://gitlab.com/gitlab-org/gitlab-ee/tree/master/ee/app/assets/javascripts/vue_shared/security_reports
- registry: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/registry/stores
2017-04-20 07:24:40 -04:00
2018-07-12 08:10:53 -04:00
## Vue architecture
2017-04-20 07:24:40 -04:00
2018-07-12 08:10:53 -04:00
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 we use [vuex ](#vuex ).
2017-04-20 07:24:40 -04:00
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].
2018-07-12 08:10:53 -04:00
### Components and Store
2017-03-22 15:30:54 -04:00
In some features implemented with Vue.js, like the [issue board][issue-boards]
or [environments table][environments-table]
you can find a clear separation of concerns:
```
new_feature
├── components
2018-03-12 06:08:58 -04:00
│ └── component.vue
2017-03-22 15:30:54 -04:00
│ └── ...
2018-07-12 08:10:53 -04:00
├── store
2018-03-12 06:08:58 -04:00
│ └── new_feature_store.js
2018-03-22 06:25:03 -04:00
├── index.js
2017-03-22 15:30:54 -04:00
```
_For consistency purposes, we recommend you to follow the same structure._
Let's look into each of them:
2018-03-22 06:25:03 -04:00
### A `index.js` file
2017-03-22 15:30:54 -04:00
This is the index file of your new feature. This is where the root Vue instance
of the new feature should be.
2017-04-20 07:24:40 -04:00
The Store and the Service should be imported and initialized in this file and
provided as a prop to the main component.
2017-03-22 15:30:54 -04:00
2018-05-27 15:21:12 -04:00
Don't forget to follow [these steps][page_specific_javascript].
2017-03-22 15:30:54 -04:00
2017-09-05 03:39:52 -04:00
### Bootstrapping Gotchas
2018-05-27 15:21:12 -04:00
#### Providing data from HAML to JavaScript
2017-09-05 03:39:52 -04:00
While mounting a Vue application may be a need to provide data from Rails to JavaScript.
To do that, provide the data through `data` attributes in the HTML element and query them while mounting the application.
2018-05-27 15:21:12 -04:00
_Note:_ You should only do this while initializing the application, because the mounted element will be replaced with Vue-generated DOM.
2017-09-05 03:39:52 -04:00
The advantage of providing data from the DOM to the Vue instance through `props` in the `render` function
instead of querying the DOM inside the main vue component is that makes tests easier by avoiding the need to
create a fixture or an HTML element in the unit test. See the following example:
```javascript
// haml
.js-vue-app{ data: { endpoint: 'foo' }}
2018-05-27 15:21:12 -04:00
// index.js
2017-09-05 03:39:52 -04:00
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '.js-vue-app',
data() {
const dataset = this.$options.el.dataset;
return {
endpoint: dataset.endpoint,
};
},
render(createElement) {
return createElement('my-component', {
props: {
endpoint: this.isLoading,
},
});
},
}));
```
#### Accessing the `gl` object
2018-05-27 15:21:12 -04:00
When we need to query the `gl` object for data that won't change during the application's life cyle, we should do it in the same place where we query the DOM.
2017-09-05 03:39:52 -04:00
By following this practice, we can avoid the need to mock the `gl` object, which will make tests easier.
It should be done while initializing our Vue instance, and the data should be provided as `props` to the main component:
```javascript
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '.js-vue-app',
render(createElement) {
return createElement('my-component', {
props: {
username: gon.current_username,
},
});
},
}));
```
2017-04-20 07:24:40 -04:00
### A folder for Components
2017-03-22 15:30:54 -04:00
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
else, please refer to `vue_shared/components` .
A good thumb rule to know when you should create a component is to think if
it will be reusable elsewhere.
For example, tables are used in a quite amount of places across GitLab, a table
would be a good fit for a component. On the other hand, a table cell used only
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]
2017-04-20 07:24:40 -04:00
### A folder for the Store
2017-03-22 15:30:54 -04:00
2018-03-22 06:25:03 -04:00
#### Vuex
Check this [page ](vuex.md ) for more details.
2017-03-22 15:30:54 -04:00
## Style guide
2017-10-03 15:47:07 -04:00
Please refer to the Vue section of our [style guide ](style_guide_js.md#vue-js )
2017-03-22 15:30:54 -04:00
for best practices while writing your Vue components and templates.
2017-04-20 07:24:40 -04:00
## 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.
2018-05-27 15:21:12 -04:00
Make use of the [axios mock adapter ](axios.md#mock-axios-response-on-tests ) to mock data returned.
2017-04-20 07:24:40 -04:00
Here's how we would test the Todo App above:
```javascript
2018-05-27 15:21:12 -04:00
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
2017-04-20 07:24:40 -04:00
describe('Todos App', () => {
2018-05-27 15:21:12 -04:00
let vm;
let mock;
beforeEach(() => {
// Create a mock adapter for stubbing axios API requests
mock = new MockAdapter(axios);
2017-04-20 07:24:40 -04:00
const Component = Vue.extend(component);
2018-05-27 15:21:12 -04:00
// Mount the Component
vm = new Component().$mount();
});
afterEach(() => {
// Reset the mock adapter
mock.restore();
// Destroy the mounted component
vm.$destroy();
});
2017-04-20 07:24:40 -04:00
2018-05-27 15:21:12 -04:00
it('should render the loading state while the request is being made', () => {
2017-04-20 07:24:40 -04:00
expect(vm.$el.querySelector('i.fa-spin')).toBeDefined();
});
2018-05-27 15:21:12 -04:00
it('should render todos returned by the endpoint', done => {
// Mock the get request on the API endpoint to return data
mock.onGet('/todos').replyOnce(200, [
{
2017-04-20 07:24:40 -04:00
title: 'This is a todo',
2018-05-27 15:21:12 -04:00
text: 'This is the text',
},
]);
2017-04-20 07:24:40 -04:00
2018-05-27 15:21:12 -04:00
Vue.nextTick(() => {
const items = vm.$el.querySelectorAll('.js-todo-list div')
expect(items.length).toBe(1);
expect(items[0].textContent).toContain('This is the text');
done();
2017-04-20 07:24:40 -04:00
});
2018-05-27 15:21:12 -04:00
});
2017-04-20 07:24:40 -04:00
2018-05-27 15:21:12 -04:00
it('should add a todos on button click', (done) => {
2017-04-20 07:24:40 -04:00
2018-05-27 15:21:12 -04:00
// Mock the put request and check that the sent data object is correct
mock.onPut('/todos').replyOnce((req) => {
expect(req.data).toContain('text');
expect(req.data).toContain('title');
2017-04-20 07:24:40 -04:00
2018-05-27 15:21:12 -04:00
return [201, {}];
2017-04-20 07:24:40 -04:00
});
2018-05-27 15:21:12 -04:00
vm.$el.querySelector('.js-add-todo').click();
2017-04-20 07:24:40 -04:00
2018-05-27 15:21:12 -04:00
// Add a new interceptor to mock the add Todo request
Vue.nextTick(() => {
expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(2);
done();
2017-04-20 07:24:40 -04:00
});
});
});
```
2018-05-27 15:21:12 -04:00
### `mountComponent` helper
2018-01-19 05:09:59 -05:00
There is a helper in `spec/javascripts/helpers/vue_mount_component_helper.js` that allows you to mount a component with the given props:
2017-09-05 03:39:52 -04:00
```javascript
import Vue from 'vue';
2018-07-09 16:41:19 -04:00
import mountComponent from 'spec/helpers/vue_mount_component_helper'
2017-09-05 03:39:52 -04:00
import component from 'component.vue'
const Component = Vue.extend(component);
const data = {prop: 'foo'};
const vm = mountComponent(Component, data);
```
2018-05-27 15:21:12 -04:00
### Test the component's output
2017-05-04 09:53:00 -04:00
The main return value of a Vue component is the rendered output. In order to test the component we
need to test the rendered output. [Vue][vue-test] guide's to unit test show us exactly that:
2017-03-22 15:30:54 -04:00
[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
[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
2017-04-20 07:24:40 -04:00
[one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow
2017-05-04 09:53:00 -04:00
[vue-test]: https://vuejs.org/v2/guide/unit-testing.html
2017-04-20 07:24:40 -04:00
[flux]: https://facebook.github.io/flux
2017-11-20 04:57:08 -05:00
[axios]: https://github.com/axios/axios