Model
A Model is the definition of a record which can be added to (or loaded into) a Store. It defines which fields the data contains and exposes an interface to access and manipulate that data. The Model data is populated through simple a JSON object.
By default, a Model stores a shallow copy of its raw json, but for records in stores configured with useRawData: true it stores the supplied json object as is.
Defining fields
A Model can either define its fields explicitly (see fields) or have them created from its data (see autoExposeFields). This snippet shows a model with 4 fields defined explicitly:
class Person extends Model {
static fields = [
'name',
{ name : 'birthday', type : 'date', format : 'YYYY-MM-DD' },
{ name : 'shoeSize', type : 'number', defaultValue : 11 },
{ name : 'age', readOnly : true }
]
}
The first field (name) has an unspecified type, which means the field's value is held as received with no conversion applied. The second field (birthday) is defined to be a date, which will make the model parse any supplied value into an actual date. The parsing is handled by DateHelper.parse() using the specified format, or if no format is specified using DateHelper.defaultFormat.
While defining fields on Model in TypeScript, data fields should be of type ModelFieldConfig instead of DataField, because it is a union type that gives completion based on specified type.
class Person extends Model {
static fields: ModelFieldConfig[] = [
'name',
{ name : 'birthday', type : 'date', format : 'YYYY-MM-DD' },
{ name : 'shoeSize', type : 'number', defaultValue : 11 },
{ name : 'age', readOnly : true }
]
}
The set of standard field types is as follows:
You can also set a defaultValue that will be used if the data does not contain a value for the field:
{ name : 'shoeSize', type : 'number', defaultValue : 11 }
Defining a field with a name containing a . will by default point to a nested object in the data. If you have a data property that actually contains a . in the name, also configure complexMapping : false on the field:
class Organization extends Model {
static fields = [
// Points to { "org" : { "name" : "..." } } in the data
{ name : 'org.name' },
// Points to { "org.address" : "..." } in the data
{ name : 'org.address', complexMapping : false }
]
}
Creating a record
To create a record from a Model, supply data to its constructor:
const guy = new Person({
id : 1,
name : 'Dude',
birthday : '2014-09-01'
});
If no id is specified, a temporary id based on a UUID will be generated. This id is not meant to be serialized, it should instead be replaced by the backend with a proper id from the underlying database (or similar).
Please avoid using reserved names for your fields (such as parent, children and others that are used as Model properties) to avoid possible data collisions and bugs.
Calculated fields
A field value can also be calculated using the other data fields, using the calculate config.
const store = new Store({
fields : [
{ name : 'revenue', type : 'number' },
{ name : 'tax', type : 'number' },
{ name : 'net', calculate : record => record.revenue * (1 - (record.tax / 100)) }
],
data : [
{ id : 1, revenue : 100, tax : 30 }
]
});
const record = store.getById(1).net; // 70
Nested fields
Model supports mapping fields to nested data structures using dot . separated paths as the dataSource. For example given this JSON object:
{
name : 'Borje Salming',
team : {
name : 'Toronto Maple Leafs',
league : 'NHL'
}
}
A field can be mapped to the nested team name by using dataSource : 'team.name':
class Player extends Model {
static fields = [
'name',
// Field mapped to a property on a nested object
{ name : 'teamName', dataSource : 'team.name' }
];
}
Usage:
const player = new Player(json);
console.log(player.teamName); // > Toronto Maple Leafs
player.teamName = 'Minnesota Wild'; // Updates name property of the team object
Alternatively, you can define the top level of the nested object as a field of type object:
class Person extends Model {
static fields = [
'name',
// Nested object
{ name : 'address', type : 'object' }
];
}
You can then access properties of the nested object using dot notation with the get function:
const person = new Person({
name : 'Borje Salming',
address : {
city : 'Toronto'
}
});
person.get('address.city'); // > Toronto
Updating a nested object
Note that directly altering a property of the nested object won't register as an update of the record, record does not track changes deeply. If nested fields (as described above) is not enough for your usecase you can map a field directly to the nested object and then assign a shallow copy of it to the record on changes:
class Player extends Model {
static get fields() {
return [
...,
// Field mapped directly to the nested object
{ name : 'team', type : 'object' }
]
}
}
// "External object" to nest
const team = {
name : 'Brynas',
league : 'SHL'
}
const player = new Player({
name : 'Borje Salming',
team
});
// This will not flag player as dirty
team.league = 'CHL';
// Instead you have to reassign the mapped field
player.team = { ...player.team };
You can also use the set function to update a property of the nested object:
// This will flag player as dirty
player.set('team.name', 'BIF');
Arrays of atomic types
When a field holds an array of atomic types (strings, numbers etc.) we recommend using the array type for the field:
class GroceryList extends Model {
static get fields() {
return [
'name',
{ name : 'items', type : 'array' }
];
}
}
const list = new GroceryList({
name : 'My list',
items : ['Milk', 'Bread', 'Eggs']
});
Arrays of objects
When a field holds an array of objects, we recommend using the store type for the field:
class GroceryList extends Model {
static fields = [
'name',
{ name : 'items', type : 'store', storeClass : Store }
]
}
const list = new GroceryList({
name : 'My list',
items : [
{ name : 'Milk', quantity : 1 },
{ name : 'Bread', quantity : 2 },
{ name : 'Eggs', quantity : 12 }
]
});
The items field on the list above will be a Store instance (because we passed that as storeClass), which can be used to manipulate the items in the list. Doing so will flag the list as modified. For more info, see StoreDataField.
Persisting fields
By default, all fields are persisted. If you don't want particular field to get saved to the server, configure it with persist: false. In this case field will not be among changes which are sent by store.commit(), otherwise its behavior doesn't change.
class Person extends Model {
static get fields() {
return [
'name',
{ name : 'age', persist : false }
];
}
}
The id field
By default Model expects its id field to be stored in a data source named "id". The data source for the id field can be customized by setting dataSource on the id field object configuration.
class Person extends Model {
static fields = [
{ name : 'id', dataSource: 'personId'},
'name',
{ name : 'age', persist : false },
{ name : 'birthday', type : 'date' }
];
}
let girl = new Person({
personId : 2,
name : 'Lady',
birthday : '2011-11-05'
});
Also, it is possible to change the id field data source by setting idField:
class Person extends Model {
// Id drawn from 'id' property by default; use custom field here
static idField = 'personId';
static fields = [
'name',
{ name : 'age', persist : false },
{ name : 'birthday', type : 'date' }
];
}
Getting and setting values
Fields are used to generate getters and setters on the records. Use them to access or modify values (they are reactive):
console.log(guy.name);
girl.birthday = new Date(2011,10,6);
NOTE: In an application with multiple different models you should subclass Model, since the prototype is decorated with getters and setters. Otherwise, you might get unforeseen collisions.
Field data mapping
By default, fields are mapped to data using their name. If you for example have a "name" field it expects data to be { name: 'Some name' }. If you need to map it to some other property, specify dataSource in your field definition:
class Person extends Model {
static fields = [
{ name : 'name', dataSource : 'TheName' }
];
}
// This is now OK:
let dude = new Person({ TheName : 'Manfred' });
console.log(dude.name); // --> Manfred
NOTE: Do not modify fields using dataSource, as it is intended only for reading and writing from the raw data object. Fields should be modified using name as it is the public interface.
Field inheritance
Fields declared in a derived model class are added to those from its superclass. If a field declared by a derived class has also been declared by its super class, the field properties of the super class are merged with those of the derived class.
For example:
class Person extends Model {
static fields = [
'name',
{ name : 'birthday', type : 'date', format : 'YYYY-MM-DD' }
];
}
class User extends Person {
static fields = [
{ name : 'birthday', dataSource : 'dob' },
{ name : 'lastLogin', type : 'date' }
];
}
In the above, the Person model declares the birthday field as a date with a specified format. The User model extends Person and also declares the birthday field. This redeclared field only specifies dataSource, so all the other fields are preserved from Person. The User model also adds a lastLogin field.
Note that later accessing Person.fields will refer to the block of two fields defined above (birthday & lastLogin), not to all fields available (name, birthday, lastLogin). To access all fields, use allFields on the Model class instead, or the fields property on a record (instance).
The User from above could have been declared like so to achieve the same fields:
class User extends Model {
static fields = [
'name',
{ name : 'birthday', type : 'date', format : 'YYYY-MM-DD', dataSource : 'dob' },
{ name : 'lastLogin', type : 'date' }
];
}
Override default values
In case you need to define default value for a specific field, or override an existing default value, you can define a new or re-define an existing field definition in fields static getter:
class Person extends Model {
static fields = [
{ name : 'username', defaultValue : 'New person' },
{ name : 'birthdate', type : 'date' }
];
}
class Bot extends Person {
static fields = [
{ name : 'username', defaultValue : 'Bot' } // default value of 'username' field is overridden
];
}
Changing default field values
Default values for fields can also be changed using applyDefaults. This should be called before data is loaded into stores, to ensure new instances pick up the updated defaults:
// Change the default value of the 'name' field for all new UserModel instances
UserModel.applyDefaults({ name : 'New user' });
const user = new UserModel();
console.log(user.name); // 'New user'
This works alongside config defaults — both field and config defaults can be set in a single call.
Read-only records
Model has a default field called readOnly, which is used to make the record read-only in the UI while still allowing programmatic changes to it. Setting it to true will prevent it from being edited by the built-in editing features (cell editing in Grid, event dragging in Scheduler, task editor in Gantt etc.). Please note that it is not made read-only on the data level, the record can still be manipulated by application code.
// Prevent record from being manipulated by the user
record.readOnly = true;
// Programmatic manipulation is still allowed
record.remove();
Tree API
This class mixes in the TreeNode mixin which provides an API for tree related functionality (only relevant if your store is configured to be a tree).
Useful configs and properties
| Config / Property | Description |
|---|---|
| fields | Field definitions for the model |
| idField | Name of the id field, default 'id' |
| get | Read a field value, supports dot paths |
| set | Update one or more field values |
| isModified | true when record has uncommitted changes |
| relations | Define foreign-key relations between stores |
See also
Fields
Fields belong to a Model class and define the Model data structure-
Start expanded or not (only valid for tree data)
Properties
Properties are getters/setters or publicly accessible variables on this class-
Identifies an object as an instance of Model class, or subclass thereof.
-
An array containing all the defined fields for this Model class. This will include all superclass's defined fields.
-
An object containing all the defined fields for this Model class. This will include all superclass's defined fields through its prototype chain. So be aware that
Object.keysandObject.entrieswill only access this class's defined fields. -
The data source for the id field which provides the ID of instances of this Model.
-
This yields
trueif this record is eligible for syncing with the server. It can yieldfalseif the record is in the middle of a batched update, or if it is a tentative record yet to be confirmed as a new addition. -
Returns true if this record is not part of any store.
-
Identifies an object as an instance of Model class, or subclass thereof.
-
For copied records, this property links to the original model instance from which it was copied.
-
True if this Model is currently batching its changes.
-
True if this models changes are currently being committed.
-
True if this model has any uncommitted changes.
-
Check if record has valid data. Default implementation returns true, override in your model to do actual validation.
-
Get a map of the modified fields in form of an object. The field´s dataSource is used as the property name in the returned object. The record's id is included unless its persist config is
false. -
Get a map of the modified data fields along with any alwaysWrite fields, in form of an object. The field´s dataSource is used as the property name in the returned object. Used internally by AjaxStore / CrudManager when sending updates.
-
Returns data for allpersistable fields in form of an object, using dataSource if present.
-
Returns a map of the modified persistable fields
-
Same as allFields.
-
Returns the string value for display purposes of an instance of this Model class. Needs to be overridden in subclasses.
-
Same as fieldMap.
-
Convenience getter to get field definitions from class.
-
When called on a group header row returns list of records in that group. Returns
undefinedotherwise. -
Returns true for a group header record
-
Gets the records internalId. It is assigned during creation, guaranteed to be globally unique among models.
-
Returns true if the record is new and has not been persisted (and received a proper id).
-
Get the first store that this model is assigned to.
-
Returns index path to this node. This is the index of each node in the node path starting from the topmost parent. (only relevant when its part of a tree store).
Functions
Functions are methods available for calling on the class-
Makes getters and setters for related records. Populates a Model#relation array with the relations, to allow it to be modified later when assigning stores.
-
cancelBatch( )
Cancels current batch operation. Any changes during the batch are discarded.
-
Reverts changes in this back to their original values.
-
initRelations( )private
Initializes model relations. Called from store when adding a record.