vikunja-frontend/docs/models-services.md

185 lines
5.3 KiB
Markdown
Raw Permalink Normal View History

# Models and services
The architecture of this web app is in general divided in two parts:
Models and services.
Services handle all "raw" requests, models contain data and methods to work with it.
A service takes (in most cases) a model and returns one.
## Table of Contents
* [Services](#services)
* [Requests](#requests)
* [Loading](#loading)
* [Factories](#factories)
* [Before Request](#before-request)
* [After Request?](#after-request-)
* [Models](#models)
* [Default Values](#default-values)
* [Constructor](#constructor)
* [Access Model Data](#access-to-the-data)
## Services
Services are located in `src/services`.
All services must inherit `AbstractService` which holds most of the methods.
A basic service can look like this:
```javascript
import AbstractService from './abstractService'
import ListModel from '../models/list'
export default class ListService extends AbstractService {
constructor() {
super({
getAll: '/lists',
get: '/lists/{id}',
create: '/namespaces/{namespaceID}/lists',
update: '/lists/{id}',
delete: '/lists/{id}',
})
}
modelFactory(data) {
return new ListModel(data)
}
}
```
The `constructor` calls its parent constructor and provides the paths to make the requests.
The parent constructor will take these and save them in the service.
All paths are optional. Calling a method which doesn't have a path defined will fail.
The placeholder values in the urls are replaced with the contens of variables with the same name in the
corresponding model (the one you pass to the functions).
#### Requests
Several request types are possible:
| Name | HTTP Method |
|------|-------------|
| `get` | `GET` |
| `getAll` | `GET` |
| `create` | `PUT` |
| `update` | `POST` |
| `delete` | `DELETE` |
Each method can take a model and optional url parameters as function parameters.
With the exception of `getAll()`, a model is always mandatory while parameters are not.
Each method returns a promise, so you can access a request result like so:
```javascript
service.getAll().then(result => {
// Do something with result
})
```
The result is a ready-to-use model returned by the model factory.
##### Loading
Each service has a `loading` property, provided by `AbstractModel`.
This property is a `boolean`, it is automatically set to `true` (with a 100ms delay to avoid flickering)
once the request is started and set to `false` once the request is finished.
You can use this to show and hide a loading animation in the frontend.
#### Factories
The `modelFactory` takes data, and returns a model. The result of all requests (with the exception
of the `delete` method) is run through this factory. The factory should return the appropriate model, see
[models](#models) down below on how to handle data in models.
`getAll()` checks if the response is an array, if that's the case, it will run each entry in it through
the `modelFactory`.
It is possible to define a different factory for each request. This is done by implementing a method called
`model{TYPE}Factory(data)` in your service. As a fallback if the specific factory is not defined,
`modelFactory` will be used.
#### Before Request
For each request exists a `before{TYPE}(model)` method. It recieves the model, can alter it and should return
the modified version.
This is useful to make unix timestamps from javascript dates, for example.
#### After Request ?
There is no `after{TYPE}` method which would be called after a request is done.
Processing raw api data should be done in the constructor of the model, see more on that below.
## Models
Models are a bit simpler than services.
They usually consist of a declaration of defaults and an optional constructor.
Models are located in `src/models`.
Each model should extend the `AbstractModel`.
This handles the default value parsing.
A model _does not_ handle any http requests, that's what services are for.
A simple model can look like this:
```javascript
import AbstractModel from './abstractModel'
import TaskModel from './task'
import UserModel from './user'
export default class ListModel extends AbstractModel {
constructor(data) {
// The constructor of AbstractModel handles all the default parsing.
super(data)
// Make all tasks to task models
this.tasks = this.tasks.map(t => {
return new TaskModel(t)
})
this.owner = new UserModel(this.owner)
}
// Default attributes that define the "empty" state.
defaults() {
return {
id: 0,
title: '',
description: '',
owner: UserModel,
tasks: [],
namespaceID: 0,
created: 0,
updated: 0,
}
}
}
```
#### Default values
The `defaults()` functions provides all default values.
The `AbstractModel` constructor will take all the data provided to it, and fill any non-existent,
`undefined` or `null` value with the default provided by the function.
#### Constructor
The `AbstractModel` constructor handles all the default value parsing.
In your model, the constructor can do additional parsing, like making js date object from unix timestamps
or parsing the contents of a child-array into a model.
If the model does nothing like this, you don't need to define a constructor at all.
The parent will handle it all.
#### Access to the data
After initializing a model, it is possible to access all properties via `model.property`.
To make sure the property actually exists, provide it as a default.