-
Notifications
You must be signed in to change notification settings - Fork 50
Architecture frontend
The frontend of eCamp v3 is written in Vue.js, using the Vuetify library which contains many pre-built Material Design components. This way we are able to quickly develop new pages, while still sharing a consistent style across the whole application.
All frontend code lies inside the frontend/
directory. This directory is a standard Vue.js project. Here is a summary of the most important directories and files:
-
.jest/
contains settings for the Jest unit tests -
node_modules/
contains all third-party npm libraries, once they are installed during application startup -
public/
contains static files, such asindex.html
which is sent to the browser. We almost never need to make changes to these files. -
src/
contains the actual frontend code-
assets/
contains images and logos -
components/
contains reusable components and sub-pages as .vue files (single file components)-
form/
contains our API form components, which we use rather than plain Vuetify input components if possible
-
-
locales/
contains frontend-specific translation JSON files (see the separate documentation) -
mixins/
andplugins/
contain some reusable code and infrastructure -
scss/
contains some global CSS styles -
views/
contains standalone single file components that are used only in the router -
App.vue
is the main single file component which includes all other view code -
main.js
is the entry script which includes all plugins and renderssrc/App.vue
-
router.js
contains the configuration of all frontend URLs and tells the application what to display under a given URL
-
-
vue.config.js
contains some settings for Vue.js
As a rule of thumb, .vue files should go into src/views/
if they are only ever referenced in the router. Otherwise, they should go into src/components/
. We do not currently have a clear policy about the directory structure inside src/components/
.
Unit tests for individual components or JavaScript files are by convention located next to the script files in __tests__
directories.
We follow the Vue.js style guide and use the Vue standard ESLint config for maintaining a common code style.
We try to minimize repetitive work and duplicated code where possible. At the same time, from a user perspective, the UI should be consistent, so when creating the user interface for a new feature, we try to copy and adapt existing UI first, before creating something from scratch.
Vuetify gives us lots of pre-built components, including form input fields. These are prefixed with V
, for example VTextField
, VDatePicker
and so on. Vuetify also offers a wealth of configuration options for these components. In order to maintain a consistent look across the app, we have created our own eCamp-specific versions of commonly used input components. These are located in src/components/form/base/
, and are prefixed with E
, so for example ETextField
is an eCamp-styled version of VTextField
. Whereever possible, we use these E
-inputs instead of the V
-inputs.
In a lot of places in the application, the form input fields directly correspond to fields from the API, so e.g. there is an input field for the camp name somewhere in the camp settings. These types of inputs should usually have auto-save functionality (except in modal dialogs and similar places). For such API-based fields, we have created yet another type of input components with Api
prefix. These are located in src/components/form/api/
. So the camp name input is actually an ApiTextField
instead of an ETextField
or even VTextField
.
When in doubt how to use these components, try to search for other uses in the rest of the application. In most of them, the props of Vuetify are available, and our additional props should be documented via comments in the code.
For these Components we write tests which have three purposes:
- They are a smoke test that we see when an update of vuetify changes a component we use. For that we make snapshot Test with a deep mount. So we test the vue component underneath, but that is on purpose. Example for snapshot test: https://github.com/ecamp/ecamp3/blob/devel/frontend/src/components/form/base/__tests__/ECheckbox.spec.js#L19
- We test the functionality we need. Example for functionality test: https://github.com/ecamp/ecamp3/blob/devel/frontend/src/components/form/base/__tests__/ETextArea.spec.js#L63
- They are documentation for how to use the component. That's why we mount the component in a template. Now you can see how the component should be used in productive code, and don't have to convert from the naming as attribute (v-model) to the naming in the test (vModel). It's also easier to assert the state of the view model with this approach. https://github.com/ecamp/ecamp3/pull/701/commits/734c08af67eb9a16e9d675187f5c21e31550205b
Many times, it is necessary to read and write data from the backend API without a directly corresponding input field. The API is a HAL JSON API, which means that the different data entities are linked via URIs (see also the API documentation). For example, a camp has links to the people involved in the camp (the so-called camp collaborations), to the activities in the camp and more. Each activity has a link to the camp it belongs to, as well as links to the content pieces of the activity, such as storyboards or safety concepts.
In order to reduce the number of network requests needed to get the data for a given page, we use a Vuex store as a kind of cache for data from the API. The first time that some Vue component needs the data of an activity, a real network request is sent out to the API, and the response is cached in the Vuex store. When the same data is needed elsewhere later, the cached data from Vuex is returned without sending another network request.
Data from the API can be loaded in single file components for example in a computed
property:
props: {
// The camp is given to this component from outside, via a prop
camp: { type: Object, required: true }
},
computed: {
campCreator () {
// This visits the "creator" link on the camp
return this.camp.creator()
},
firstActivity () {
// This reads the "activities" list in the camp and gets the first item
return this.camp.activities().items[0]
}
}
The data and relations can also be used in the HTML template part of single file components. There, we don't need to write this.
:
<div>{{ campCreator.displayName }}</div>
<ul>
<li v-for="activity in camp.activities().items">
{{ activity.title }}
</li>
</ul>
We can also access links on the API root endpoint (located under http://localhost:3001/api when running eCamp locally) as follows:
computed: {
profile () {
// this.api.get() is the API root endpoint. This computed
// visits the "profile" link on that API response.
return this.api.get().profile()
},
campFromId () {
// Some links are "templated", which means they accept parameters.
// This is useful for fetching a specific camp by its id
return this.api.get().camps({ campId: this.campId })
}
}
Be aware that the data might still be loading when you first read it. While a network request is still ongoing, the result of the function call will be a placeholder that is mostly safe to use in your Vue components. But sometimes you need to wait for data to be really available. You can do this via the ._meta.load
Promise, or the ._meta.loading
boolean flag.
data () {
return {
// The following line would not work, because data() is only executed once,
// so the placeholder will never be replaced with the actual data:
// creator: this.camp.creator()
// Instead, set it to null and fill it in mounted():
creator: null
}
},
mounted () {
this.camp.creator()._meta.load.then(creator => {
this.creator = creator
})
}
Once you have data from the API in the frontend, you might need to write some changes back. eCamp v3 currently offers 3 options to change data:
- POST requests to create new entries
- PATCH requests to change existing entries
- DELETE requests to remove entries
Below is an example for all of these changes.
methods: {
createTodo (title) {
this.todos.$post({ title: title })
},
editTodo (todo, newTitle) {
todo.$patch({ title: newTitle })
},
deleteTodo (todo) {
todo.$delete()
}
}
Each of these operations returns a Promise which resolves once the operation has completed. So if you e.g. need to do something after a todo
has finished deleting, you can await this Promise: todo.$delete().then(() => { console.log('todo deleted') })
- Home
- Installation
- Domain Object Model
- API
- Design
- Localization and translations
- Architecture
-
Testing guide
- API testing (TBD)
- Frontend testing
- E2E testing (TBD)
- Deployment
- Debugging