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
];
}
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).
Properties
66
Properties
66Class hierarchy
Editing
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.
Set this property to true when adding a record on a conditional basis, that is, it is yet
to be confirmed as an addition.
When this is set, the isPersistable value of the record is false, and upon being added to a Store it will not be eligible to be synced with the server as an added record.
Subsequently, clearing this property means this record will become persistable and eligible for syncing as an added record.
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 always included.
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.
Get a map of the modified fields in form of an object. The field names are used as the property names in the returned object, and the property values are the latest field values.
The id field is always included.
Fields
An array containing all the defined fields for this Model class. This will include all superclass's defined fields.
Flag checked from Store when loading data that determines if fields found in first records should be exposed in same way as predefined fields.
The name of the data field which holds children of this Model when used in a tree structure
MyModel.childrenField = 'kids';
const parent = new MyModel({
name : 'Dad',
kids : [
{ name : 'Daughter' },
{ name : 'Son' }
]
});
Returns the string value for display purposes of an instance of this Model class. Needs to be overridden in subclasses.
Template static getter which is supposed to be overridden to define default field values for the Model class.
Overrides defaultValue config specified by the fields getter.
Returns a named object where key is a field name and value is a default value for the field.
NOTE: This is a legacy way of defining default values, we recommend using fields moving forward.
class Person extends Model {
static get fields() {
return [
{ name : 'username', defaultValue : 'New person' }
];
}
}
class Bot extends Person {
static get defaults() {
return {
username : 'Bot' // default value of 'username' field is overridden
};
}
}
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.keys and Object.entries will only
access this class's defined fields.
Get the names of all properties in the data object.
Note that this is not the same as the fields defined for the model, in most cases you probably want to use fields instead.
Array of defined fields for this model class. Subclasses add new fields by implementing this static getter:
// Model defining two fields
class Person extends Model {
static get fields() {
return [
{ name : 'username', defaultValue : 'New person' },
{ name : 'birthdate', type : 'date' }
];
}
}
// Subclass overriding one of the fields
class Bot extends Person {
static get fields() {
return [
// Default value of 'username' field is overridden, any other setting from the parents
// definition is preserved
{ name : 'username', defaultValue : 'Bot' }
];
}
}
Fields in a subclass are merged with those from the parent class, making it easy to override mappings, formats etc.
Note that later accessing Bot.fields will refer to the block with the redefinition of the username
field seen above, not to all fields available (username, birthdate). To access all fields, use either
allFields on the Model class, or the fields property on a
record.
The data source for the id field which provides the ID of instances of this Model.
Grouping
When called on a group header row returns list of records in that group. Returns undefined otherwise.
Returns true for a group header record
Identification
Reports true when the record is a parent record generated by the TreeGroup feature.
Checks if record has a generated id.
New records are assigned a generated id based on a UUID (starting with _generated), which is intended to be
temporary and should be replaced by the backend on commit.
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).
JSON
Get the records data as a json string.
const record = new Model({
title : 'Hello',
children : [
...
]
});
const jsonString = record.json;
//jsonString:
'{"title":"Hello","children":[...]}'
Linked records
Get the original record this record is linked to.
Get links to this record.
Misc
Get the first store that this model is assigned to.
Reference to STM manager, if used
Other
Class name getter. Used when original ES6 class name is minified or mangled during production build. Should be overridden in each class which extends Model or it descendants.
class MyNewClass extends Model {
static $name = 'MyNewClass';
}
This yields true if this record is eligible for syncing with the server.
It can yield false if 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.
Override in a subclass of Model to define relations to records in other stores.
Always defined on the "one" side, not the "many" side.
Expects an object where keys are relation names and values are relation configs.
This snippet will define a relation called team, allowing access to the foreign record via player.team. It
will point to a record in the teamStore (must be available as record.firstStore.teamStore) with an id
matching the players teamId field. The team record in turn, will have a field called players which is a
collection of all players in the team.
class Player extends Model {
static relations = {
// Define a relation between a player and a team
team : {
foreignKey : 'teamId',
foreignStore : 'teamStore',
relatedCollectionName : 'players'
}
}
}
const teamStore = new Store({
data : [
{ id : 1, name : 'Brynas' },
{ id : 2, name : 'Leksand' }
]
});
const playerStore = new Store({
modelClass : Player,
// Matches foreignStore, allowing records of playerStore to find the related store
teamStore,
data : [
// teamId is specified as foreignKey, will be used to match the team
{ id : 1, name : 'Nicklas Backstrom', teamId : 1 },
{ id : 2, name : 'Elias Lindholm', teamId : 1 },
{ id : 3, name : 'Filip Forsberg', teamId : 2 }
],
}
playerStore.first.team.name // > Brynas
playerStore.last.team.name // > Leksand
teamStore.first.players // > [nick, elias]
teamStore.last.players // > [filip]
To access the related record from the many side, use dot notation for the field name. For example in a Grid column:
const grid = new Grid({
store : playerStore,
columns : [
{ field : 'name', text : 'Name' },
{ field : 'team.name', text : 'Team' }
]
});
Parent & children
Functions
56
Functions
56Editing
Begin a batch, which stores changes and commits them when the batch ends. Prevents events from being fired during batch.
record.beginBatch();
record.name = 'Mr Smith';
record.team = 'Golden Knights';
record.endBatch();
Please note that you can also set multiple fields in a single call using set, which in many cases can replace using a batch:
record.set({
name : 'Mr Smith',
team : 'Golden Knights'
});
Cancels current batch operation. Any changes during the batch are discarded.
Clears tracked changes, used on commit. Does not revert changes.
| Parameter | Type | Description |
|---|---|---|
includeDescendants | Boolean | Supply |
Makes a copy of this model, assigning the specified id or a generated id and also allowing you to pass field values to the created copy. A copyOf property is set on the copy to reference the original record.
const record = new Model({ name : 'Super model', hairColor : 'Brown' });
const clone = record.copy({ name : 'Super model clone' });
| Parameter | Type | Description |
|---|---|---|
newIdOrData | Number | String | Object | The id for the copied instance, or any field values to apply (overriding the values from the source record). If no id provided, one will be auto-generated |
deep | Boolean | True to also clone children |
Copy of this model
End a batch, triggering events if data has changed.
| Parameter | Type | Description |
|---|---|---|
silent | Boolean | Specify |
Returns raw data from the encapsulated data object for the passed field name
| Parameter | Type | Description |
|---|---|---|
fieldName | String | The field to get data for. |
The raw data value for the field.
Returns the unmodified value of the field, as in the value it had after the last commit. If the field has not been modified, the current value is returned.
| Parameter | Type | Description |
|---|---|---|
fieldName | String | Field name |
The unmodified value of the field, or its current value if not modified
Returns true if this Model currently has outstanding batched changes for the specified field name.
| Parameter | Type | Description |
|---|---|---|
fieldName | String | The field name to check for batched updates on. |
Returns true if this model has uncommitted changes for the provided field.
| Parameter | Type | Description |
|---|---|---|
fieldName | String | Field name |
true if the field has an uncommitted change
Removes this record from all stores (and in a tree structure, also from its parent if it has one).
| Parameter | Type | Description |
|---|---|---|
silent | Boolean | Specify |
Reverts changes in this back to their original values.
Set value for the specified field. You can also use the generated setters if loading through a Store.
Setting a single field, supplying name and value:
record.set('name', 'Clark');
Setting multiple fields, supplying an object:
record.set({
name : 'Clark',
city : 'Metropolis'
});
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.
| Parameter | Type | Description |
|---|---|---|
field | String | Object | The field to set value for, or an object with multiple values to set in one call |
value | * | Value to set |
silent | Boolean | Set to |
Fields
Add a field definition in addition to those predefined in fields.
| Parameter | Type | Description |
|---|---|---|
fieldDef | String | ModelFieldConfig | A field name or definition |
Get value for specified field name. You can also use the generated getters if loading through a Store. If model is currently in batch operation this will return updated batch values which are not applied to Model until endBatch() is called.
| Parameter | Type | Description |
|---|---|---|
fieldName | String | Field name to get value from |
Fields value
Get the data source used by specified field. Returns the fieldName if no data source specified.
| Parameter | Type | Description |
|---|---|---|
fieldName | String | Field name |
Convenience function to get the definition for a field from class.
| Parameter | Type | Description |
|---|---|---|
fieldName | String | Field name |
Get the definition for a field by name.
| Parameter | Type | Description |
|---|---|---|
fieldName | String | Field name |
Field definition or null if none found
Processes input to a field, converting to expected type.
| Parameter | Type | Description |
|---|---|---|
fieldName | String | Field name |
value | * | Value to process |
Converted value
Remove a field definition by name.
| Parameter | Type | Description |
|---|---|---|
fieldName | String | Field name |
Identification
Gets the id of specified model or model data object, or the value if passed string/number.
| Parameter | Type | Description |
|---|---|---|
model | Model | String | Number |
id
Generates an id for a new record (a phantom id), based on a UUID by default.
This function can be overridden to provide custom id generation logic.
let idCounter = 0;
// Override the default logic with app specific
Model.generateId = () => `id${new Date().getTime()}${idCounter++}`;
| Parameter | Type | Description |
|---|---|---|
text | String | Text used as optional prefix for the id, defaults to the name of the class |
A generated id
Generates an id for a new record (a phantom id), based on a UUID (starting with _generated).
Generated ids are intended to be temporary and should be replaced by the backend on commit.
JSON
Used by JSON.stringify() to correctly convert this record to json.
In most cases no point in calling it directly.
// This will call `toJSON()`
const json = JSON.stringify(record);
If called manually, the resulting object is a clone of record.data + the data of any children:
const record = new Model({
title : 'Hello',
children : [
...
]
});
const jsonObject = record.toJSON();
// jsonObject:
{
title : 'Hello',
children : [
...
]
}
Represent the record as a string, by default as a JSON string. Tries to use an abbreviated version of the object's data, using id + name/title/text/label/description. If no such field exists, the full data is used.
const record = new Model({ id : 1, name : 'Steve Rogers', alias : 'Captain America' });
console.log(record.toString()); // logs { "id" : 1, "name" : "Steve Rogers" }
Lifecycle
Constructs a new record from the supplied data config.
| Parameter | Type | Description |
|---|---|---|
config | Object | Raw model config |
store | Store | Data store |
meta | Object | Meta data |
Misc
Compares this Model instance to the passed instance. If they are of the same type, and all fields
(except, obviously, id) are equal, this returns true.
| Parameter | Type | Description |
|---|---|---|
other | Model | The record to compare this record with. |
true if the other is of the same class and has all fields equal.
Creates a proxy record (using native Proxy) linked to this record (the original). The proxy records shares most
data with the original, except for its id (which is always generated), and ordering fields such as
parentIndex and parentId etc.
Any change to the proxy record will be reflected on the original, and vice versa. A proxy record is not meant to be persisted, only the original record should be persisted. Thus, proxy records are not added to stores change tracking (added, modified and removed records).
Removing the original record removes all proxies.
Creating a proxy record allows a Store to seemingly contain the record multiple times, something that is otherwise not possible. It also allows a record to be used in both a tree store and in a flat store.
Proxy record linked to the original record
Other
Defines if the given event field should be manually editable in UI. You can override this method to provide your own logic.
| Parameter | Type | Description |
|---|---|---|
fieldName | String | Name of the field |
Returns true if the field is editable, false if it is not and undefined if the model has no such field.
Configuration
Events
Parent & children
Typedefs
1
Typedefs
1Defines the properties of a relation between two stores.
Used as the values of a Model's relations definition.
This snippet will define a relation called team, allowing access to the foreign record via player.team. It will
point to a record in the teamStore (must be available as record.firstStore.teamStore) with an id matching the
players teamId field. The team record in turn, will have a field called players which is a collection of all
players in the team.
class Player extends Model {
static relations = {
team : {
foreignKey : 'teamId',
foreignStore : 'teamStore',
relatedCollectionName : 'players'
}
}
}
See relations for a more extensive example.
| Parameter | Type | Description |
|---|---|---|
foreignKey | String | Name of a field on this model which holds the foreign key value. |
foreignStore | String | Store | Name of a property on the model's first store, which holds the foreign store. Or the actual store instance |
relatedCollectionName | String | Optionally, name of a property that will be added to the records of the foreign store, which will hold all records from the model's store related to it. |
propagateRecordChanges | Boolean | Set to |
Fields
3
Fields
3Common
Unique identifier for the record. Might be mapped to another dataSource using idField, but always exposed as record.id. Will get a generated value if none is specified in records data.
Flag the record as read-only on the UI level, preventing the end user from manipulating it using editing features such as cell editing and event dragging.
Does not prevent altering the record programmatically, it can still be manipulated by application code.
For more info, see the "Read-only records" section above.
Tree
Start expanded or not (only valid for tree data)