v7.3.0

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.

When adding data to a Store, you do normally not need to create records manually. The store does that for you when you add a data object to it

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'] });

Modifying items in the array will not flag the field as updated, since the array itself does not change. For it to register a change, you must assign it a new array (could be a copy of the old one). For more info, see ArrayDataField

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

  • Store - Data container that holds a collection of records
  • DataField - Base class for field type definitions
  • TreeNode - Tree node API mixed into Model
No results

Fields

Fields belong to a Model class and define the Model data structure
  • expanded : Boolean
    READONLY

    Start expanded or not (only valid for tree data)

Properties

Properties are getters/setters or publicly accessible variables on this class
  • isModel : Booleantrue
    READONLY
    static
    ADVANCED
    Identifies an object as an instance of Model class, or subclass thereof.
  • allFields : DataField[]
    READONLY
    static

    An array containing all the defined fields for this Model class. This will include all superclass's defined fields.

  • fieldMap : Object<String, DataField>
    READONLY
    static

    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.

  • idField : String
    static

    The data source for the id field which provides the ID of instances of this Model.

  • 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.

  • isRemoved : Boolean
    internal
    READONLY

    Returns true if this record is not part of any store.

  • isModel : Booleantrue
    READONLY
    ADVANCED
    Identifies an object as an instance of Model class, or subclass thereof.
  • copyOf : Model
    READONLY

    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.

  • isCommitting : Boolean
    READONLY

    True if this models changes are currently being committed.

  • isModified : Boolean
    READONLY

    True if this model has any uncommitted changes.

  • isValid : Boolean
    READONLY

    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.

  • persistableData : Object
    internal
    READONLY

    Returns data for allpersistable fields in form of an object, using dataSource if present.

  • rawModificationData : Object
    internal
    READONLY

    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.

  • fieldMap : Object<String, DataField>
    READONLY

    Same as fieldMap.

  • fields : DataField[]
    READONLY

    Convenience getter to get field definitions from class.

  • When called on a group header row returns list of records in that group. Returns undefined otherwise.

  • Returns true for a group header record

  • internalId : Number
    READONLY

    Gets the records internalId. It is assigned during creation, guaranteed to be globally unique among models.

  • isPhantom : Boolean
    READONLY

    Returns true if the record is new and has not been persisted (and received a proper id).

  • firstStore : Store
    READONLY

    Get the first store that this model is assigned to.

  • indexPath : Number[]
    private
    READONLY

    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
  • exposeRelations( )
    internal
    static

    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.

  • 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.

Type definitions

Source path

Core/data/Model.js

Contents