# Properties
Most (if not all) front-end data management libraries require some type of specification for Model properties so that assumptions can be made about how data should be available externally and processed internally. This section will detail how to explicitly declare properties for a model.
The following code shows the data structure for declaring Model properties:
class MyModel extends Model {
...
/**
* Static method for declaring properties of Model.
*/
static props() {
return {
/**
* Placeholder property for example.
*/
myProp: {
/**
* Default value for property.
*/
default: null,
/**
* Default type for the property. If no `mutate` configuration
* is set, the type specified here will be used to mutate the object.
*/
type: String,
/**
* Parse value from server. This is useful for reformatting back-end
* data into a format that is more useful for front-end operations.
* This is different than the `mutate` property because it
* defines how the contract will be parsed **after** data is returned
* from the back-end.
* @param value - Value passed to property after fetching data.
* @returns Parsed value to use for property on front-end.
*/
parse: function(value) {
...
return parsedValue;
},
/**
* Mutation function for processing property before create/update
* actions. This is different than the `parse` property because it
* defines how the contract will be mutated **before** data is sent to
* the backend.
* @param value - Value passed to property on update.
* @returns Mutated value to set as model property.
*/
mutate: function(value) {
...
return newValue;
},
/**
* Validation function for ensuring property is valid.
* @param value - Un-mutated value passed to property on update.
* @returns Boolean describing if value is valid or not.
*/
validate: function(value) {
...
return isValid;
},
/**
* Collapse nested models into a single property for API
* update actions (POST or PUT). This value is only relevant if
* the property is an Object type or a linked Model instance. If
* a boolean `true` is applied here, 'id' will be used as the collapse
* property.
*/
collapse: 'linkedPropId',
/**
* Function for retrieving property data. Properties that
* instrument this property are not saved in the store for models,
* and instead accessed using the function specified below. This
* is primarily useful for adding linked models that aren't nested
* in response data.
* @returns Value accessible via Model.<prop>
*/
get: () => {},
/**
* Shorthand for easily renaming a model property when data is
* received **from** the API. In this case, `originalName` in
* the payload for this model will be cast to `myProp` when the
* model is used and casted back to `originalName` when data are
* sent back to the application API.
*/
from: 'originalName',
/**
* Shorthand for easily renaming a model property when data is sent
* to the API. In this case, the model property will be renamed to
* `apiName` when requests are sent to the application API.
*/
to: 'apiName',
},
};
}
...
}
Note that
props()
must be astatic
method on your class. If it is not declared as a static method, the model will not work according to expectations.
Under the hood, this module automatically creates state properties, mutations, and actions in Vuex. Setting a mutation as a property on a model is equivalent to creating a mutation in the Vuex store for changing a state parameter. You can find more information on this in the Store section of the documentation.
If you don't wish to specify detailed property-specific configuration, you can use the following shorthand for simply setting defaults:
class MyModel extends Model {
static props() {
return {
prop1: null,
prop2: 2,
prop3: 'foo',
};
}
}
# Data Flow
There are several logical processes that happen behind the scenes when a value is set on an Model instance. Take this block of code for example:
const obj = new MyModel({ myProp: null });
obj.myProp = 1;
await obj.commit();
console.log(obj.myProp) // 1
What's really happening during each of these operations is the following:
const obj = new MyModel({ myProp: null });
obj.myProp = 1; // 1. Validate input using validate function
// 2. Mutate data using mutate function
// 3. Store data on instance (not in Vuex store until commit)
await obj.commit(); // Dispatch Vuex store action that will:
// 1. Make axios request to update data, return promise
// 2. Wait for response
// 3. Update new value in Vuex store
console.log(obj.myProp); // Use Vuex getter to pull property from store for instance.
More information about how data are organized in the Vuex store can be found in the Store section of the documentation.
# Mutating Properties
You can also define mutations for properties in your Model definitions via the mutation
parameter within a property definition. The mutation
key takes a callable and should return a value to store as the property state:
// within Model.props()
divWrappedProperty: {
default: ' ',
mutate: value => `<div>${value}</div>`,
},
After setting the mutation
key on a property definition, any attempts to set the property will be automatically processed:
// on instantiation
const obj = MyModel({ divWrappedProperty: 'foo' });
console.log(obj.divWrappedProperty); // '<div>foo</div>'
// after instantiation
obj.divWrappedProperty = 'bar';
console.log(obj.divWrappedProperty); // '<div>bar</div>'
# Data Validation
Validation for model properties can be specified using the validation
parameter within a property definition. The validation
key can take either a callable returning a boolean:
// within Model.props()
numericProperty: {
default: 1,
validate: (value) => {
... // validation logic
return true;
},
},
Or, a dictionary specifying the callable (check
) and a message (message
) to raise if the validation fails:
// within Model.props()
specificValue: {
default: 1,
validate: {
check: value => value < 4,
message: "${value} must be less than 4.",
},
}
Inputs to the validate function will be the pre-mutated value.
After setting the validation
key on a property definition, any attempts to set the property will be automatically processed:
// valid property values, no errors thrown:
const obj = MyModel({ specificValue: 3 });
obj.specificValue = 2;
obj.commit();
// invalid property values, errors thrown with message on commit
const obj = MyModel({ specificValue: 5 });
obj.specificValue = 6;
obj.commit();
See lodash and validator for libraries that can provide validation for different types of data.
# Linked Models
As described in the Overview section of the documentation, you can configure properties to reference other types of models via the type
parameter in Model property declaration.
If the API you're pulling data from produces nested data:
// GET /posts
[
{
id: 1,
title: 'Post 1',
body: 'This is the text for post 1',
author: {
id: 1,
name: 'Jane Doe',
email: 'jane@doe.com'
},
},
{
id: 2,
title: 'Post 2',
body: 'This is the text for post 2',
author: {
id: 1,
name: 'Jane Doe',
email: 'jane@doe.com',
},
},
...,
]
And you have two models configured for the nested data (in this case, Author
and Post
), you can configure the Post
model to reference the internal Author
model for the nested data like so:
class Author extends Model { ... }
class Post extends Model {
static props() {
return {
title: '',
body: '',
author: {
model: Author,
},
};
}
}
The configuration above will enable this library to automatically store Author
objects in the store once they're fetched via the /posts
endpoint. Additionally, Author
models will be directly available from the Post
instances, so you can traverse the model data like so:
const post = await Post.get(1) // get post with id `1` from API
post.author.id // 1
post.author.title // Post 1
By default, on update actions, all of the author information will be sent in the PUT
or POST
payload:
const post = Post.query(1); // get post with id `1` from the store
post.title = 'new title';
post.commit(); // see below for payload
// PUT /posts/:id
{
id: 1,
title: 'new title',
body: 'This is the text for post 1',
author: {
id: 1,
name: 'Jane Doe',
email: 'jane@doe.com'
},
},
However, this often isn't a contract required by the application API. More commonly, the application API will accept a payload in the form:
// PUT /posts/:id
{
id: 1,
title: 'new title',
body: 'This is the text for post 1',
author_id: 1,
},
To automatically collapse the nested Author
data into a single update parameter, use the collapse
parameter when defining the property:
class Post extends Model {
static props() {
return {
title: '',
body: '',
author: {
type: Author,
collapse: true,
to: 'author_id',
},
};
}
}
Similarly, collapse
can take a string that will be used to pull a value from nested objects to include in the payload.
Explicitly declaring how models are related can assume many different types of patterns, and this library was built to accommodate as many as possible. For instance, sometimes related models are declared in the API using a key reference:
// GET /posts/:id
{
id: 1,
title: 'new title',
body: 'This is the text for post 1',
author_id: 1,
},
If your frontend models would more easily be usable by changing the author_id
property into an Author
relationship, you can use from
to automatically map the data in that way:
class Post extends Model {
static props() {
return {
title: '',
body: '',
author: {
type: Author,
from: 'author_id',
},
};
}
}
However, this requires that the Author
data being referenced is already in the Vuex store. When you access the property:
Post.fetch().then(() => {
const post = Post.query(1);
post.author.id
});
This library will use a store getter to try to retrieve an Author
model using the author_id
data from the posts
payload. An Author
model will only be returned if that Author
entry already exists in the store. Otherwise, accessing the author
property will return null
.
Or, you can simply use the from
keyword to rename payload data to your liking (doesn't need to be associated with a linked model).
← API Relationships →