ExtJS: Fully functional custom type - extjs

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

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

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

Related

Easiest way to create a Hash of Arrays (or equivalent) in Typescript?

I am looking to create a Hash of Arrays (or some equivalent structure) that allows me to collect an unknown set of properties (keyed by name) and have each property store an array of things that claimed they have said property.
const currentProperties = currentObject.getProperties();
// we can assume getProperties correctly returns an array of valid properties
currentProperties.forEach( (v) => {
  HoA[ v ].push( currentObject );
});
I want to be able to do something like the above to populate the Hash of Arrays - but how to I actually properly initialize it/do all of the TypeScript stuff? Currently I've been getting by using an enum to manually specify the possible properties that could show up, but I want to adapt it out to a structure that doesn't need to have a property list ahead of time, and can just take whatever shows up as a key.
As noted above, I understand how to solve a version of this problem if I manually specify the expected types of properties to be seen and use a bunch of
if (currentProperties.includes(Properties.exampleOne)) {
this.exampleGroupOne.push(currentObject);
}
but I want to be able to have this work with no prior knowledge of what values of properties exist.
EDIT: some clarification on what I am asking for -
The goal is to have a bunch of objects that have a getProperties() method that returns an array of zero or more attributes. I want to have a data structure that, for each attribute that exists, ends up with an array of the objects that reported that attribute. That is easy when I know the possible attributes ahead of time, but in this case, I won't. For actually acting on the attributes, I'll need a loop that is the attributes on the outer layer [the hash] and the relevant objects on the inner layer [the array]. (This is why I'm assuming HoA)
EDIT #2:
class Alice {
myProps(): string[] {
return ["Apple"];
}
}
class Bob {
myProps(): string[] {
return ["Banana"];
}
}
class Charlie {
myProps(): string[] {
return ["Apple", "Banana"];
}
}
const FruitBasket:{ [prop: string]: string} = {}
const myAlice = new Alice();
const myBob = new Bob();
const myCharlie = new Charlie();
const Objects = [myAlice, myBob, myCharlie];
for (const currentObject of Objects) {
const fruits = currentObject.myProps();
fruits.forEach( (v) => { FruitBasket[v].push(currentObject);});
}
I think this is almost what I want - I am getting an error that push does not exist on type string, but at this point I think I'm just missing something basic because I've been staring at this too long.
EDIT #3:
abstract class JustSomeGuy {
myProps(): string[] {
return [];
}
myName(): string {
return '';
}
}
class Alice extends JustSomeGuy {
myProps(): string[] {
return ["Apple"];
}
myName(): string {
return 'Alice';
}
}
class Bob extends JustSomeGuy {
myProps(): string[] {
return ["Banana"];
}
myName(): string {
return 'Bob';
}
}
class Charlie extends JustSomeGuy {
myProps(): string[] {
return ["Apple", "Banana"];
}
myName(): string {
return 'Charlie';
}
}
const FruitBasket:{ [prop: string]: JustSomeGuy[]} = {}
const myAlice = new Alice();
const myBob = new Bob();
const myCharlie = new Charlie();
const Objects = [myAlice, myBob, myCharlie];
for (const currentObject of Objects) {
const fruits = currentObject.myProps();
fruits.forEach( (v) => { (FruitBasket[v] ??= []).push(currentObject);});
}
for (let key in FruitBasket){
let value = FruitBasket[key];
for (let i = 0; i < value.length; i++){
console.log("In key: " + key + " the ith element [i = " + i + "] is: " + value[i].myName() );
}
}
I believe that this is what I want. Marking this as resolved.
Let's start with the types of the data structures that you described:
type ObjWithProps = {
getProperties (): string[];
};
type PropertyHolders = {
[key: string]: ObjWithProps[] | undefined;
};
// Could also be written using built-in type utilities like this:
// type PropertyHolders = Partial<Record<string, string[]>>;
The type ObjWithProps has a method which returns an array of string elements.
The type PropertyHolders is an object type that is indexed by string values (keys), and each value type is an array of ObjWithProps (if it exists, or undefined if it doesn't) — no object has a value at every possible key.
Next, let's replicate the data structures you showed in your example:
const HoA: PropertyHolders = {};
const currentObject: ObjWithProps = {
getProperties () {
return ['p1', 'p2', 'p3' /* etc. */];
}
};
const currentProperties = currentObject.getProperties();
In the code above, the currentObject has some arbitrary properties (p1, p2, p3). This is just to have reproducible example data. Your own implementation will likely be different, but the types are the same.
Finally, let's look at the part where you assign the values to the hash map:
currentProperties.forEach((v) => {
HoA[v].push(currentObject); /*
~~~~~~
Object is possibly 'undefined'.(2532) */
});
You can see that there's a compiler error where you try to access the array at the key v. Because you aren't sure that the array exists (no object has a value at every key), trying to invoke a push method on undefined would throw a runtime error. TypeScript is trying to help you prevent that case.
Instead, you can use the nullish coalescing assignment operator (??=) to ensure that the array is created (if it doesn't already exist) before pushing in a new value. This is what that refactor would look like:
currentProperties.forEach((v) => {
(HoA[v] ??= []).push(currentObject); // ok
});
Full code in TS Playground
Utility types references:
Record<Keys, Type>
Partial<Type>

Infinite digest loop inside custom filter of angularJS

I am displaying a list of tables and each row is also expandable. I want to filter the list using the property of expandable table. For example if the main table show the general info of student and expandable table shows the marks of subject for that student. I want to filter the complete table by marks column. So the filtering should display the parent table with matched marks.
var students = [{
name: "neha",
address: "abc"
marks: [{
subject: "english",
marks: 80
}, {
subject: "hindi",
marks: 60
}]
}, {
name: "sneha",
address: "xyz"
marks: [{
subject: "english",
marks: 70
}, {
subject: "math",
marks: 50
}]
}
For this
I am using custom filter to filter the list. Inside custom filter I am using "filterFilter" to filter the marks array.
filterBy is the property which keeps track of property against which the value will be tested.
For example the values can be
1) filterBy = {property: "marks.subject", value: "hindi"} //filter on nested table
2) filterBy = {property: "name": value: "neha"} //filter on parent table
var app = angular.module("MyApp", []);
app.filter("filterTable", function(filterFilter) {
return function(list, filterBy) {
var filteredList = [],
len,
i,
j,
property,
present,
tempList,
listCopy,
properties = [];
//creating a copy as filter will update the list and rest filter will not work properly
listCopy = angular.copy(list);
len = listCopy.length;
if(filterBy.value) {
properties = filterBy.property.split(".");
//if filter on nested table
if(properties.length > 1) {
property = properties[1];
for ( i = 0; i < len; i++) {
tempList = {};
//using filterFilter defined by angularjs
listCopy[i].disks = filterFilter(list[i].disks, { [property] : filterBy.value });
console.log(list[i].disks);
if(listCopy[i].disks.length) {
filteredList.push(listCopy[i]);
}
}
} else {
property = filterBy.property;
for ( i = 0; i < len; i++) {
if(list[i][property].indexOf(filterBy.value) > 0) {
filteredList.push(list[i]);
}
}
}
return filteredList;
} else {
return list;
}
};
});
But this is going into infinite digest cycle. I have spent a long time on this and still not able to resolve. Please help me with it.
Thanks in advance.
This happens because you are deep copy array.
Angular's ngRepeat doesn't realize that those objects are equal because ngRepeat tracks them by identity. New object leads to new identity. This makes Angular think that something has changed since the last check, which means that Angular should run another check (aka digest).
Try update logic of filter without angular.copy()
Update:
Instead of angular.copy use flat copy:
var newArray= oldArray.slice(); // clones the array and returns the reference to the new array.

Is there a better way to iterate over this JSON data structure?

This is the data structure in question:
"EditedArticles" : {
"a3" : {
"versions" : [ {
"moment" : "a3",
"question" : "a3",
"situation" : "a3",
"version" : 1
}, ...
]
}
}
Currently, I am using three for loops to access the properties in EditedArticles.a3.versions
var getArticlesByMoment = function (moment,situation) {
angular.forEach(items, function(article, value) {//items are a1,a2,a3...
angular.forEach(article, function(versions, value) {//versions is the array containing version for each article
var checkfirst=0;
angular.forEach(versions, function(version, value) {//version is the version of an article
checkfirst++;
if (checkfirst == 1 && version.moment == moment && version.situation == situation) {
//do something;
return;
}
})
})
})
}
I want to access the properties inside the versions array for each item (eg. a3) to check if an item has the same moment/situation as a desired one. If there are a lot of items, a1 - a1000, then I think this will take a long running time. There may be many objects in each versions array but I only need to check the first object for a matching moment and situation as the rest of the objects will share the same values for those two properties.
Is there a better/faster way to access these properties? I am using an http GET call to get all the items (eg. a3) in EditedArticles.json
Thank you
You could do this to traverse your json object recursivly:
function traverse(jsonObj) {
if( typeof jsonObj == "object" ) {
$.each(jsonObj, function(k,v) {
// object or array
traverse(v);
});
}
else {
// actual value
}
}

Rally SDK 2 custom sorters

How do I get a more complex sort on a query, I have this query currently:
var store = Ext.create('Rally.data.custom.Store',{
data: changes,
limit: 'Infinity',
pageSize: 5000,
sorters: [
{
property: 'ReleaseScope',
direction: 'ASC'
},
{
property: 'ScheduleState',
direction: 'DESC'
}
]
});
Because the ScheduleState is hydrated I can't sort by the normal numerics, can I define the order using some kind of matcher?
i.e. say I want to show in order [Accepted, Completed, In-Progress, Defined, Backlog]
And, if I wanted to complicate this further and show stories with story points first, something like
All stories with a story point value != 0
Sorted by schedulestate [accepted, completed, in-progress, defined etc..]
stories with no story point value
some other sort here possibly
You can pass a sorterFn rather than a property/direction combo to implement custom sort logic:
sorters: [
{
sorterFn: function(a, b) {
var scheduleStates = ['Accepted', 'Completed', 'In-Progress', 'Defined'],
aState = a.get('ScheduleState'),
aIndex = _.indexOf(scheduleStates, aState),
bState = b.get('ScheduleState'),
bIndex = _.indexOf(scheduleStates, bState);
return a - b;
}
}
]
The above function should sort them based on schedule state descending I think.

How to deal with null values in a Store when using the sum function

I have the requirement that I need to group the value and sum of a stored field value. When summing the value I am getting NaN. I am getting null value from json response. Can anybody tell me how to replace null with 0 in store in extjs? Is there any other way to do avoid Nan while summing the fieldvalue?
clientDistributionStore.group(groupField);
curJANObj= clientDistributionStore.sum('curJAN',true);
I am getting null for some of 'curJAN' values so I am getting Nan error
I got it through record.set functionality to set value 0 for null
Another alternative is to add a new "converted" (I'd prefer to call it "computed") field to your model:
fields: [
{ name: 'curJan', type: 'int' },
{
name: 'curJanNotNull', type: 'int',
convert: function(val, row) {
return row.curJan !== null ? row.curJan : 0;
}
}
]
...
var janSum = clientDistributionStore.sum('curJanNotNull', true);
I had similar issue. I resolved it this way.
clientDistributionStore.group(Ext.create('Ext.util.Grouper', {
property: groupField,
getGroupString: function (instance) {
return instance.get(this.property) || 0;
}
}));
curJANObj= clientDistributionStore.sum('curJAN',true); // return ex. {0: 100, 'John Smith': 1000}
getGroupString function can be implemented other way because now it treats empty string also as 0. You can modify it to your own needs but you can't return null value.
It works for ExtJS 4.2.1

Resources