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)
-
This is a read-only field provided in server synchronization packets to specify which position the node takes in the parent's ordered children array. This index is set on load and gets updated on reordering nodes in tree. Sorting and filtering have no effect on it.
-
This is a read-only field provided in server synchronization packets to specify which record id is the parent of the record.
-
This is a read-only field provided in server synchronization packets to specify which position the node takes in the parent's children array. This index is set on load and gets updated automatically after row reordering, sorting, etc. To save the order, need to persist the field on the server and when data is fetched to be loaded, need to sort by this field.
-
Deprecated:
This field has been deprecated. Please read the guide to find out if your app needs to use the new isFullyLoaded field.
This field is added to the class at runtime when the Store is configured with lazyLoad. The number specified should reflect the total amount of children of a parent node, including nested descendants.
Properties
Properties are getters/setters or publicly accessible variables on this class-
Identifies an object as an instance of Model class, or subclass thereof.
-
Identifies an object as an instance of ModelLink class, or subclass thereof.
-
Identifies an object as an instance of ModelStm class, or subclass thereof.
-
Identifies an object as an instance of TreeNode class, or subclass thereof.
-
A class property getter for the default values of internal properties for this class.
-
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.
-
An empty array that can be used as a default value.
-
An empty object that can be used as a default value.
-
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).
-
Returns a copy of the full configuration which was used to configure this object.
-
This property is set to
truebefore theconstructorreturns. -
This property is set to
trueon entry to the destroy method. It remains on the objects after returning fromdestroy(). If isDestroyed istrue, this property will also betrue, so there is no need to test for both (for example,comp.isDestroying || comp.isDestroyed). -
Are other records linked to this record?
-
Is this record linked to another record?
-
Get the original record this record is linked to.
-
Get links to this record.
-
Get the first store that this model is assigned to.
-
Reference to STM manager, if used
-
Retrieve all children, excluding filtered out nodes (by traversing sub nodes)
-
Retrieve all children, including filtered out nodes (by traversing sub nodes)
-
Depth in the tree at which this node exists. First visual level of nodes are at level 0, their direct children at level 1 and so on.
-
Count all children (including sub-children) for a node (in its `firstStore´)
-
Get the first child of this node
-
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).
-
Is a leaf node in a tree structure?
-
Returns true for parent nodes with children loaded (there might still be no children)
-
Is a parent node in a tree structure?
-
Returns
trueif this node is the root of the tree -
Get the last child of this node
-
Get the next sibling of this node
-
This is a read-only property providing access to the parent node.
-
Get the previous sibling of this node
-
Returns count of all preceding sibling nodes (including their children).
-
Array of tree nodes without any filter applied. On first filter, will take order from sorted
children, but is not thereafter kept in sorted order, so order should not be relied upon. -
Count visible (expanded) children (including sub-children) for a node (in its
firstStore) -
Returns values of the persistable tree-defining fields: parentId, orderedParentIndex, and parentIndex or sparseIndex. parentIndex is omitted when sparseIndex is used.
Functions
Functions are methods available for calling on the class-
This optional class method is called when a class is mixed in using the mixin() method.
-
Registers this class type with its Factory
-
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.
-
Called from insertChild to notify StateTrackingManager about children insertion. Provides it with all necessary context information collected in beforeInsertChild required to undo/redo the action.
-
Called from removeChild to notify StateTrackingManager about children removing. Provides it with all necessary context information collected in beforeRemoveChild required to undo/redo the action.
-
Called during creation to also turn any children into Models joined to the same stores as this model
-
initRelations( )private
Initializes model relations. Called from store when adding a record.
-
Removes all records from the rootNode