1

I'm looking for the best way to define own custom type in ExtJs. I would like to use it in forms, grids, pivots, etc., it should has own rendering, calculating and sorting behaviour.

Consider a type 'pace'. Pace is defined as amount of time needed to move on unit of distance, for example pace 2:30 means you need two and half minute to do 1 mile or km. Pace could be added (2:30 + 2:35 = 5:05) and be used in other calculations. The smaller pace is faster, that means pace 2:00 is faster (higher) than 2:30.

So far as I know, this is a way to define own custom type and use it in data model as in coding below:

Ext.define('App.fields.Pace', {
    extend: 'Ext.data.field.Field',
    alias: 'data.field.pace'
});

Ext.define( 'Data', {
    extend: 'Ext.data.Model',
    fields: [ 
        {name: 'id'},                           
        {name: 'pace', type: 'pace'}
    ]
});

Such defined type is a dummy one, it doesn't render, sort or calculate correctly.

Is there a way to extend it, so it will work ok in forms, grids, pivots, etc.? What should I do to archive it? Should I define or overwrite some methods? Or perhaps I should take other similar type (for example date) and inherit it or use it as template? I think as minimum the new custom type should provide a method to convert its value to internal type like int and a method to render this internal value as external format but I haven’t find such methods.

Is it possible to define own type which will work correctly in all scenarios where standard type could be used?

Regards, Annie

Annie W.
  • 328
  • 4
  • 22

2 Answers2

0

I don't think that calculating with custom types is supported in ExtJS. You can extend any class with any function you wish, to implement custom calculation functions, but I am not sure you can override operators in JavaScript.

That said, let me give you an example of a custom type array:

Ext.define('Ext.data.field.Array',{
    extend: 'Ext.data.field.Field',
    getType: function() {
        return "array"
    },
    compare: function(o1, o2) {
        if (!o1 && !o2) {
            return 0
        }
        if (!o1) {
            return -1
        }
        if (!o2) {
            return 1
        }
        if (o1.length != o2.length) {
            return o1.length > o2.length ? -1 : 1
        }
        for (var i = 0; i < o1.length; i++) {
            if (o2.indexOf(o1[i]) == -1) {
                return 1;
            }
        }
        return 0
    },
    convert: function(value) {
        if (this.separator && Ext.isString(value)) {
            if (value == "") {
                value = []
            } else {
                value = value.split(this.separator)
            }
        }
        if (Ext.isArray(value)) {
            if (this.map) {
                value = value.map(this.map)
            }
            return value
        }
        return []
    },
    serialize: function(value) {
        if (Ext.isArray(value)) {
            if (!this.allowBlank) {
                value = Ext.Array.filter(value, function(a) {
                    return a !== ""
                })
            }
            if(this.separator) return value.join(this.separator)
            else return value
        }
        return value
    }
});

A typical field definition in a model may then be the following:

{
    name: "Names",
    type: "array",
    allowBlank: false,
    separator: ","
}

which would be able to parse a JSON array or a comma-separated string, and serialize into a comma-separated string for submission, or

{
    name: "Attachments",
    type: "array",
}

which would be able to parse a JSON array and also serialize into an array, or

{
    name: "Categories",
    type: "array",
    separator:',',
    map:function(value) {
        return Ext.create('MyApp.model.Category',{value:value});
    }
}

which does take a comma-separated string and map every part of it into a model, so that it returns an array of models.

This value does sort, serialize and deserialize, but not render. Rendering isn't done by a type, but by the Ext.Component that uses the type, e.g. the gridcolumn or the form field. There's a reason they have a datecolumn and datepickerfield, a numbercolumn and a numberfield (AKA spinner), a checkcolumn and a checkboxfield, just to name a few.

For my array, a suitable field would be the combobox with multi:true, but I need a column with a custom renderer; so let's make an arraycolumn:

Ext.define('MyApp.ux.ArrayColumn',{
    extend:'Ext.grid.column.Column',
    lines: 6,
    renderer:function(value) {
        if (Ext.isString(value)) {
            value = value.split("\n")
        }
        var len = value.length;
        if (this.lines && len > this.lines) {
            value = value.slice(0, lines - 1);
            value.push('and {n} more...'.replace('{n}', len - lines + 1))
        }
        return value.join("<br>");
    }
});

Everything without warranty, of course...

Community
  • 1
  • 1
Alexander
  • 19,906
  • 19
  • 75
  • 162
0

It feels like a bit of an anti-pattern as complex types (or those with rules) are generally modelled separately and such entities can be attached to records using one-to-one associations. With your question in mind however, this is the simplest implementation I could come up with.

» Fiddle

Ideally I'd of liked to normalise and store the values as integers; the lowest denomination of "pace" i.e. the total number of seconds. Unfortunately the ExtJS field types don't provide any formatting hooks that work automatically with other parts of the API so string manipulation it has to be.

The following field class includes a custom getValue function which reduces either a numeric or string input to an integer. The other functions override members of the base class and are called automatically by the framework.

convert is called when data is read into the model and is used here to implicitly validate the integrity of the input. sortType is called whenever you use the various sorters on collections and is used here to reduce the string to an unambiguous / easily comparable value.

Ext.define('App.data.field.Pace', {
    extend: 'Ext.data.field.Field',
    alias: 'data.field.pace',
    isPace: true,

    getValue: function(v){
        var type = typeof v,
            isNan = isNaN(v);
        if(type === 'number' || !isNan)
            return isNan ? 0 : (+v|0);
        if(type === 'string'){
            var match = v.match(/^(\d+):([0-5]?\d)$/);
            if(match)
                return (+match[1]) * 60 + (+match[2]);
        }
        return 0;
    },

    convert: function(v){
        var proto = App.data.field.Pace.prototype,
            value = proto.getValue(v),
            mins = value / 60 | 0,
            secs = value % 60;
        return String(mins) +':'+ (secs<10?'0':'') + String(secs);
    },

    sortType: function(v){
        var proto = App.data.field.Pace.prototype;
        return proto.getValue(v);
    }
});

Note that the all functions are deliberately accessed via the class prototype instead of from the this keyword - while playing around it looked as if the framework - at various points - likes to call field functions independently of any (if any) instantiated class, so it's advisable not to rely on the bound context.

In order to address the addition / math on field values you can utilise the calculate configuration - though as this is specific to each use-case it should reside within the model that's actually using the custom field type. For example:

Ext.define('App.data.Model', {
    extend: 'Ext.data.Model',

    fields: [
        {
            name: 'name',
            type: 'string'
        },
        {
            name: 'paceOne',
            type: 'pace'
        },
        {
            name: 'paceTwo',
            type: 'pace'
        },
        {
            name: 'paceTotal',
            type: 'pace',
            calculate: function(data){
                var proto = App.data.field.Pace.prototype;
                return proto.convert(
                    proto.getValue(data.paceOne) +
                    proto.getValue(data.paceTwo)
                );
            }
        }
    ]
});
Emissary
  • 9,954
  • 8
  • 54
  • 65