Models represent the individual Domain objects of your application. Normally they will be used to represent a single resource from the API.
A simple use case for a model would that of a user. Below is an example of a JSON:API representation of a user.
{
"data": {
"id": "1",
"type": "users",
"attributes": {
"title": "Mr",
"first_name": "Nick",
"last_name": "Ryall",
"email": "[email protected]",
"phone": "021552497"
},
"relationships": {
"role": {
"data": {
"type": "roles",
"id": "1"
}
},
"company": {
"data": {
"type": "companies",
"id": "1"
}
}
},
"links": {
"self": "http://example.com/jsonapi/user/1"
}
}
}
extend
You extend the Model Class with your domain-specific properties and methods. Model provides a base set of functionality for managing changes.
import { Model } from 'mobx-jsonapi';
class UserModel extends Model {
// My properties and methods
}
type
Often you will define a type property for the model which will reflect the type member of the JSON resource. This will prevent having to manually specify the type in the payload when creating a new model and saving it to the server.
import { Model } from 'mobx-jsonapi';
class UserModel extends Model {
type = 'users';
}
url()
More often than not, a model will belong to a collection and will derive it's URL from it. However in some cases your application may only have one instance of a model which needs to be persisted to the server. An example would be a 'Me' model that represents the currently authenticated user. In this case you can define a url action which will define the endpoint to use for all CRUD operations.
import { Model } from 'mobx-jsonapi';
class UserModel extends Model {
type = 'users';
}
class MeModel extends UserModel {
url() {
return 'jsonapi/me';
}
}
urlRoot
Specify a urlRoot if you're using a model outside of a collection, to enable the default url action to generate URLs based on the model id.
import { Model } from 'mobx-jsonapi';
class MeModel extends Model {
type = 'users';
urlRoot = '/jsonapi/users';
}
const me = new MeModel({
"id": "1"
});
// Saves to 'jsonapi/users/1'
me.save();
Normally, you won't need to define this.
Constructor / Initialize
When creating an instance of a model, you can pass in some initial state as the first argument and an options object as the second argument.
import User from '../models/User';
import Companies from '../collections/Companies';
const companies = new Companies();
const user = new User({
"data": {
"id": "1",
"type": "users",
"attributes": {
"title": "Mr",
"first_name": "Nick",
"last_name": "Ryall",
"email": "[email protected]",
"phone": "021552497"
},
"relationships": {
"role": {
"data": {
"type": "roles",
"id": "1"
}
},
"company": {
"data": {
"type": "companies",
"id": "1"
}
}
},
"links": {
"self": "http://example.com/jsonapi/user/1"
}
}
}, {
related: {
companies
}
});
console.log(user.getAttribute('first_name')) // Nick
options
Name | Type | Description |
---|---|---|
related | object | Specify references to other model and collection instances. The reverse relationship is also set up automatically. |
attributes
The model will parse the state and create an observable map from the attributes object which represents the resource’s primary data. The Model class provides a number of methods for working with this data directly.
Note: Most of these methods use Mobx's observable map methods. When creating your own methods it can be helpful to refer to https://mobx.js.org/refguide/map.html
getAttribute(key)
Get the current value of an attribute from the model.
setAttribute(key, value)
Set the current value of an attribute on the model. The provided key will be added to the attributes if it didn't exist yet.
setAttributes(data = {})
Copies all entries from the provided object into the attributes map ( via merge ).
clearAttributes()
Removes all entries from the attributes map.
Computed Values
Because attributes is a Mobx observable map, you can define your own computed getters which will update when their referenced attributes change.
class User extends Model {
@computed get fullName() {
return `${this.getAttribute('first_name') ${this.getAttribute('last_name')}`;
}
};
const user = new User({
"data": {
"id": "1",
"type": "users",
"attributes": {
"title": "Mr",
"first_name": "Nick",
"last_name": "Ryall",
"email": "[email protected]",
"phone": "021552497"
},
"relationships": {
"role": {
"data": {
"type": "roles",
"id": "1"
}
},
"company": {
"data": {
"type": "companies",
"id": "1"
}
}
},
"links": {
"self": "http://example.com/jsonapi/user/1"
}
}
});
console.log(user.fullName) // Nick Ryall
user.setAttribute('firstName', 'Nicholas');
console.log(user.fullName) // Nicholas Ryall
relationships
The model will parse the state and create an observable map from the relationships object which represents references to other domain models that are linked to the resource in some way. The Model class provides a number of methods for working with this data directly.
getRelationship(key)
Get the relationship object with the given key.
setRelationships(data = {})
Copies all entries from the provided object into the relationships map ( via merge ).
clearRelationships()
Removes all entries from the relationships map.
Working with the Relationships Map
More often than not, the relationships map will be used to define computed getters that reference another model elsewhere in the application.
An example of this would be using the 'role' relationship of the user to find and save a reference to a model which exists in a 'roles' collection.
import { Model, Collection } from 'mobx-jsonapi';
import { computed } from 'mobx';
class User extends Model {
type = 'users';
// Set up a getter to find a user's role
@computed get role() {
return this.roles.find((model) => {
model.id === this.getRelationship('role').data.id;
});
}
// Full name
@computed get fullName() {
return `${this.getAttribute('firstName')} ${this.getAttribute('lastName')}`;
}
// Full title with Role
@computed get titleAndRole() {
return `${this.getAttribute('title'} ${this.fullName} - ${this.role.getAttribute('name')}`
}
};
class Roles extends Collection {};
// Create a new roles collection instance
const roles = new Roles({
"data": [
{
"id": "1",
"type": "roles",
"attributes": {
"name": "Manager"
},
"links": {
"self": "http://example.com/jsonapi/roles/1"
}
},
{
"id": "2",
"type": "roles",
"attributes": {
"name": "Employee"
},
"links": {
"self": "http://example.com/jsonapi/roles/2"
}
}
]
});
const user = new User({
"data": {
"id": "1",
"type": "users",
"attributes": {
"title": "Mr",
"first_name": "Nick",
"last_name": "Ryall",
"email": "[email protected]",
"phone": "021552497"
},
"relationships": {
"role": {
"data": {
"type": "roles",
"id": "1"
}
},
"company": {
"data": {
"type": "companies",
"id": "1"
}
}
},
"links": {
"self": "http://example.com/jsonapi/user/1"
}
}
}, {
related: {
roles
}
});
console.log(user.role.getAttribute('name')) // Admin
console.log(user.titleAndRole) // Mr Nick Ryall - Manager
CRUD Actions
All CRUD related methods below return a promise. You can use then and catch to respond to successful and unsuccessful API calls or to combine calls using e.g. Promise.all().
Each method has a related observable property that can be used for checking the status of the request. This can be useful for showing UI elements such as a loading icon.
fetch(options = { url: string, params: object })
Fetches the latest model state from the server via a GET request. Merges the model's attributes and relationships with the response data.
options
Name | Type | Default | Description |
---|---|---|---|
url | String | undefined | Override the URL for the request. Normally this isn't as fetch makes use of the Models URL by default. |
params | String | undefined | Specify querystring parameters to use in the request. |
The fetching
observable property can be used to view the status of the request.
class Me extends Model {
url() {
return '/jsonapi/me';
}
};
const me = new Me();
// Makes a GET request to '/jsonapi/me'
me.fetch().then((model, response) => {
console.log(me.fetching) // false
// Do something with the updated model or response
}).catch((error) => {
console.log(me.fetching) // false
// Do something with JSON:API error object
});
console.log(me.fetching) // true
save(data = {}, options = { wait: bool })
Saves the model to the server via a PATCH request. Merges the model's attributes and relationships with the response data.
If the model is new (does not have an ID) it will delegate to the create()
method.
If the data argument is NULL it will send all the existing attributes and relationships in the request. Otherwise it will only send those specified.
If the wait option is false it will optimistically update the attributes and relationships passed in the data argument.
Note the 'saving' observable property can be used to view the status of the request.
class Me extends Model {
url() {
return '/jsonapi/me';
}
};
const me = new Me({
"data": {
"id": "1",
"type": "users",
"attributes": {
"title": "Mr",
"first_name": "Nick",
"last_name": "Ryall",
"email": "[email protected]",
"phone": "021552497"
},
"relationships": {
"role": {
"data": {
"type": "roles",
"id": "1"
}
},
"company": {
"data": {
"type": "companies",
"id": "1"
}
}
},
"links": {
"self": "http://example.com/jsonapi/user/1"
}
}
});
// Will send all attributes and relationships in a PATCH request and wait for the response before updating the model
me.save(null, { wait: true }).then((model, response) => {
console.log(me.saving) // false
// Do something
}).then((error) => {
console.log(me.saving) // false
// Do something
});
console.log(me.saving) // true
// Will send only specified attributes and relationships in PATCH and update the values immediately
me.save({
attributes: {
first_name: 'Nicholas'
},
relationships: {
company: {
data: {
type: 'companies',
id: '2'
}
}
}
}, { wait: false });
console.log(me.getAttribute('name')) // Nicholas
// Note if the request was to fail, the attributes will be rolled back to previous state.
saveAttributes(attributes = {}, options = { wait: bool })
Saves a set of attributes to the server without the need to provide the full nested structure. Delegates to the save()
method and supports the same options.
// Will send only specified attributes in PATCH and update the attributes immediately
me.saveAttributes({
firstName: 'Nicholas'
}, { wait: false });
console.log(me.getAttribute('name')) // Nicholas
create(data ={}, options = { wait: bool })
Note: Not normally called directly. This action is used by a parent collection to save a new model to the server before adding it to it's observable array of models.
Saves the new model to the server via a POSTrequest.
If the `wait` option is false it will optimistically update the attributes and relationships passed in the data argument.
destroy(options ={ wait: bool })
Sends a DELETE requests to the models URL.
Optimistically removes the model from its collection, if it belongs to one. If `wait` option is true, it will wait for the server to respond before removal.
Note the 'deleting' observable property can be used to view the status of the request.
Parsing Related Data
One of the nicer features of the JSON:API spec is the ability to reduce requests by including resources related to the primary resource being fetched. See http://jsonapi.org/format/#fetching-includes for the official spec.
setIncluded(included = [])
The setIncluded action can be overriden at the class level. Here you can specify how any included data should be handled. It will be called internally by the model when fetching new data from the server.
The example below shows how a user's role data can be parsed into the related roles collection.
import { Model, Collection } from 'mobx-jsonapi';
import { computed } from 'mobx';
class Me extends Model {
type = 'users';
url() {
return 'jsonapi/me';
}
// Set up a getter to find a user's role
@computed get role() {
return this.roles.find((model) => {
model.id === this.getRelationship('role').data.id;
});
}
// Set included role data into the roles collection
@action setIncluded(included) {
const roleData = included.filter((model) => model.type === 'roles');
// Populate the roles collection
if (this.roles && roleData.length) {
this.roles.set({
data: rolesData
}, { add: true, remove: false, merge: false });
}
}
};
class Roles extends Collection {};
// Create a new roles collection instance
const roles = new Roles();
// Create a new me model instance
// with the roles collection specified as a relationship
const me = new Me(null, {
related: {
roles
}
});
// Fetches the user with the included role data `jsonapi/me?include[users]=role`
me.fetch({
params: {
'include[users]': 'role'
}
});
// Returned from server
{
"data": {
"id": "1",
"type": "users",
"attributes": {
"title": "Mr",
"first_name": "Nick",
"last_name": "Ryall",
"email": "[email protected]",
"phone": "021552497"
},
"relationships": {
"role": {
"data": {
"type": "roles",
"id": "1"
}
},
"company": {
"data": {
"type": "companies",
"id": "1"
}
}
},
"links": {
"self": "http://example.com/jsonapi/user/1"
}
},
"included": [
{
"id" : "1",
"type": "roles",
"attributes": {
"name" : "Manager"
}
}
]
}
// Both me and roles are now up to date.
console.log(me.role.getAttribute('name')); // Manager