Flex: Sort -- Writing a custom compareFunction? - apache-flex

OK, I am sorting an XMLListCollection in alphabetical order. I have one issue though. If the value is "ALL" I want it to be first in the list. In most cases this happens already but values that are numbers are being sorted before "ALL". I want "ALL" to always be the first selection in my dataProvider and then the rest alphabetical.
So I am trying to write my own sort function. Is there a way I can check if one of the values is all, and if not tell it to do the regular compare on the values?
Here is what I have:
function myCompare(a:Object, b:Object, fields:Array = null):int
{
if(String(a).toLowerCase() == 'all')
{
return -1;
}
else
if(String(b).toLowerCase() == 'all')
{
return 1;
}
// NEED to return default comparison results here?
}
//------------------------------
var sort:Sort = new Sort();
sort.compareFunction = myCompare;
Is there a solution for what I am trying to do?

The solution from John Isaacks is awesome, but he forgot about "fields" variable and his example doesn't work for more complicated objects (other than Strings)
Example:
// collection with custom objects. We want to sort them on "value" property
// var item:CustomObject = new CustomObject();
// item.name = 'Test';
// item.value = 'Simple Value';
var collection:ArrayCollection = new ArrayCollection();
var s:Sort = new Sort();
s.fields = [new SortField("value")];
s.compareFunction = myCompare;
collection.sort = s;
collection.refresh();
private function myCompare(a:Object, b:Object, fields:Array = null):int
{
if(String((a as CustomObject).value).toLowerCase() == 'all')
{
return -1;
}
else if(String((b as CustomObject).value).toLowerCase() == 'all')
{
return 1;
}
// NEED to return default comparison results here?
var s:Sort = new Sort();
s.fields = fields;
var f:Function = s.compareFunction;
return f.call(null,a,b,fields);
}

Well I tried something out, and I am really surprised it actually worked, but here is what I did.
The Sort class has a private function called internalCompare. Since it is private you cannot call it. BUT there is a getter function called compareFunction, and if no compare function is defined it returns a reference to the internalCompare function. So what I did was get this reference and then call it.
private function myCompare(a:Object, b:Object, fields:Array = null):int
{
if(String(a).toLowerCase() == 'all')
{
return -1;
}
else if(String(b).toLowerCase() == 'all')
{
return 1;
}
// NEED to return default comparison results here?
var s:Sort = new Sort();
var f:Function = s.compareFunction;
return f.call(null,a,b,fields);
}

Thanks guys, this helped a lot. In our case, we needed all empty rows (in a DataGrid) on the bottom. All non-empty rows should be sorted normally. Our row data is all dynamic Objects (converted from JSON) -- the call to ValidationHelper.hasData() simply checks if the row is empty. For some reason the fields sometimes contain the dataField String value instead of SortFields, hence the check before setting the 'fields' property:
private function compareEmptyAlwaysLast(a:Object, b:Object, fields:Array = null):int {
var result:int;
if (!ValidationHelper.hasData(a)) {
result = 1;
} else if (!ValidationHelper.hasData(b)) {
result = -1;
} else {
if (fields && fields.length > 0 && fields[0] is SortField) {
STATIC_SORT.fields = fields;
}
var f:Function = STATIC_SORT.compareFunction;
result = f.call(null,a,b,fields);
}
return result;
}

I didn't find these approaches to work for my situation, which was to alphabetize a list of Strings and then append a 'Create new...' item at the end of the list.
The way I handled things is a little inelegant, but reliable.
I sorted my ArrayCollection of Strings, called orgNameList, with an alpha sort, like so:
var alphaSort:Sort = new Sort();
alphaSort.fields = [new SortField(null, true)];
orgNameList.sort = alphaSort;
orgNameList.refresh();
Then I copied the elements of the sorted list into a new ArrayCollection, called customerDataList. The result being that the new ArrayCollection of elements are in alphabetical order, but are not under the influence of a Sort object. So, adding a new element will add it to the end of the ArrayCollection. Likewise, adding an item to a particular index in the ArrayCollection will also work as expected.
for each(var org:String in orgNameList)
{
customerDataList.addItem(org);
}
Then I just tacked on the 'Create new...' item, like this:
if(userIsAllowedToCreateNewCustomer)
{
customerDataList.addItem(CREATE_NEW);
customerDataList.refresh();
}

Related

List ItemRenderer duplicated when Sorting List's dataprovider

The title basically tells my problem. I am sorting the dataProvider for a list. I have itemRenderers on the list. 3 items get built, opt1, opt2, opt3 - I then have a drag and drop on the list and when that is finished I wait for my custom newRank variable to be set in the dataProvider of the list. then after sorting the list, my item renderer renders out 5 items, not 3: opt2, opt3, opt1 (first three are sorted properly), opt3, opt3.
I have tried to sort with a basic sort and a customFunction and I cannot get it to sort properly. Any help would be appreciated.
public function sortItemsByNewRank():void{
var srt:Sort = new Sort();
//srt.fields = [new SortField("newRank", true, false, true)];
srt.compareFunction = myCompare;
var ac:ArrayCollection = this.dataProvider as ArrayCollection;
ac.sort = srt;
ac.refresh();
}
private function myCompare(a:Object, b:Object, fields:Array = null):int {
LOG.debug("ExtendedList a and b: "+a.newRank+" "+b.newRank);
if(a.newRank == b.newRank) {
return 0;
} else if(a.newRank > b.newRank) {
return 1;
} else{
return -1;
}
}
Take one [Bindable] variable and give it to your list...
And in sorting function use that variable and call refresh...
eg.
[Bindable]
private var _dataProvider:ArrayCollection
public function sortItemsByNewRank():void{
var srt:Sort = new Sort();
//srt.fields = [new SortField("newRank", true, false, true)];
srt.compareFunction = myCompare;
_dataProvider.sort = srt;
_dataProvider.refresh();
}
<s:List dataProvider="{_dataProvider}"/>
May be this will solve your duplicate problem.....
I was able to fix this. Most of the issue had to do with the DATA_CHANGE event firing over and over while my sort was running. I put a flag in and it fixed my issue. A little hacky, but the invalidateList allowed me to clear and reinitialize the data into the UI.

Get Meteor collection object instance by name

I have seen similar questions but I think my scenario is a bit different. Say I define a collection like this:
MyCol = new Meteor.Collection("myCol"
and I want to get a reference to 'MyCol' using the string 'myCol' - I have created the function below which seems to work:
function GetCollectionObject(name) {
for(var key in window) {
var value = window[key];
if (value instanceof Meteor.Collection) {
if (value._name == name) {
return value;
break;
}
}
}
return null;
}
Is this the only/best/most efficient way to do this?
Why don't you store your collections in a dictionary? It's way more efficient.
Dogs = new Meteor.Collection('dogs');
Cats = new Meteor.Collection('cats');
Alpacas = new Meteor.Collection('alpacas');
MyCollections = {
dogs: Dogs,
cats: Cats,
alpacas: Alpacas,
};
...
MyCollections['dogs'].doSomething();

Flex DatagridColumn LabelFunction Additonal Parameters

I have a datagridcolumn where a labelFunction is defined:
private function myLabelFunction(item:Object, column:DataGridColumn):String
{
var returnVal:String;
var nm:NumericFormatter;
nm.decimalSeparatorTo = ".";
nm.precision = additionalParameter;
returnVal = nmTwoDecimals.format(item[column.dataField]);
if (returnVal == '0.00')
{
returnVal = '';
}
return returnVal;
}
Would it be possible to add an additional parameter so that I could pass the property values for the formatter which I intend to use?
Like for example:
private function myLabelFunction(item:Object, column:DataGridColumn, precisionParam:int):String
{
var returnVal:String;
var nm:NumericFormatter;
nm.decimalSeparatorTo = ".";
nm.precision = precisionParam;
returnVal = nmTwoDecimals.format(item[column.dataField]);
if (returnVal == '0.00')
{
returnVal = '';
}
return returnVal;
}
Thanks.
You would have to extend the DataGridColumn class. After creating your new class simply override the existing itemToLabel function:
public function itemToLabel(data:Object):String
{
if (!data)
return " ";
if (labelFunction != null)
return labelFunction(data, this);
if (owner.labelFunction != null)
return owner.labelFunction(data, this);
if (typeof(data) == "object" || typeof(data) == "xml")
{
try
{
if ( !hasComplexFieldName )
data = data[dataField];
else
data = deriveComplexColumnData( data );
}
catch(e:Error)
{
data = null;
}
}
if (data is String)
return String(data);
try
{
return data.toString();
}
catch(e:Error)
{
}
return " ";
}
The line 'return labelFunction(data, this);' is what calls the labelFunction (will also check the owner datagrid for a labelfunction). 'data' in 'itemToLabel' is your object. You could either include the precision value you want in the object or hard code it in the extended class (or inject, or singleton, class var, whatever you like).
At this point you can go ahead and pass it as a third parameter to your new labelFunction.
In your label function for the datagrid column, you can access the assigned data field by using the dataField property, see the following syntax below:
"supposing your label function is called formatNumbers_LabelFunction"
private function formatNumbers_LabelFunction(item:Object, column:DataGridColumn):String{
//Write any code logic you want to apply on your data field ;)
//In this example, I'm using a number formatter to edit numbers
return myCustomNumberFormatter.format(item[column.dataField]);
}
This way, you can use a generic label function to handle some unified operations on your displayed data
And besides that, you can also access to any data field that is in the data provider by just calling its name like this:
item.YourFieldName
where item is the firs parameter [of type Object] in your label function method
This would work:
<DataGridColumn labelFunction="{function(item:Object, column:DataGridColumn):String { return anotherLabelFunction(item,column,2) }}" />
// Elsewhere ...
function anotherLabelFunction(item:Object,column:DataGridColumn,precision:int):String
{
// Do your business
}

flex select value from Combo

My goal is to create a generic function that selects a value in a combobox accoring to a value.
(My comoBox holds arrayCollection as dataProvider.)
The difficulty is infact to get a propertyname in runtime mode
public function selectComboByLabel(combo:ComboBox , propetryName:String, value:String):void {
var dp:ArrayCollection = combo.dataProvider as ArrayCollection;
for (var i:int=0;i<dp.length;i++) {
if (dp.getItemAt(i).propertyName==value) {
combo.selectedIndex = i;
return;
}
}
}
the line if (dp.getItemAt(i).propertyName==value)
is of course incorrect.
It should be arther something like: dp.getItemAt(i).getPropertyByName(propertyName)
Any clue on how to that ?
Don't use Object Property notation. Do this:
dp.getItemAt(i)[propertyName]
In addition to what Flextras said, you could also redo your for loop to make it easier to read:
for each(var item:Object in dp) {
if(item[propertyName] == value) {
combo.selectedItem = item;
return;
}
}

To check if an object is empty or not

I want to check in my function if a passed argument of type object is empty or not. Sometimes it is empty but still not null thus I can not rely on null condition. Is there some property like 'length'/'size' for flex objects which I can use here.
Please help.
Thanks in advance.
If you mean if an Object has no properties:
var isEmpty:Boolean = true;
for (var n in obj) { isEmpty = false; break; }
This is some serious hack but you can use:
Object.prototype.isEmpty = function():Boolean {
for(var i in this)
if(i != "isEmpty")
return false
return true
}
var p = {};
trace(p.isEmpty()); // true
var p2 = {a:1}
trace(p2.isEmpty()); // false
You can also try:
ObjectUtil.getClassInfo(obj).properties.length > 0
The good thing about it is that getClassInfo gives you much more info about the object, eg. you get the names of all the properties in the object, which might come in handy.
If object containes some 'text' but as3 doesn't recognize it as a String, convert it to string and check if it's empty.
var checkObject:String = myObject;
if(checkObject == '')
{
trace('object is empty');
}
Depends on what your object is, or rather what you expect it to have. For example if your object is supposed to contain some property called name that you are looking for, you might do
if(objSomeItem == null || objSomeItem.name == null || objSomeItem.name.length == 0)
{
trace("object is empty");
}
or if your object is actually supposed to be something else, like an array you could do
var arySomeItems = objSomeItem as Array;
if(objSomeItem == null || arySomeItems == null || arySomeItems.length == 0)
{
trace("object is empty");
}
You could also use other ways through reflection, such as ObjectUtil.getClassInfo, then enumerate through the properties to check for set values.... this class help:
import flash.utils.describeType;
import flash.utils.getDefinitionByName;
public class ReflectionUtils
{
/** Returns an Array of All Properties of the supplied object */
public static function GetVariableNames(objItem:Object):Array
{
var xmlPropsList:XMLList = describeType(objItem)..variable;
var aryVariables:Array = new Array();
if (xmlPropsList != null)
{
for (var i:int; i < xmlPropsList.length(); i++)
{
aryVariables.push(xmlPropsList[i].#name);
}
}
return aryVariables;
}
/** Returns the Strongly Typed class of the specified library item */
public static function GetClassByName($sLinkageName:String):Class
{
var tObject:Class = getDefinitionByName($sLinkageName) as Class;
return tObject;
}
/** Constructs an instance of the speicified library item */
public static function ConstructClassByName($sLinkageName:String):Object
{
var tObject:Class = GetClassByName($sLinkageName);
//trace("Found Class: " + tMCDefinition);
var objItem:* = new tObject();
return objItem;
}
public static function DumpObject(sItemName:String, objItem:Object):void
{
trace("*********** Object Dump: " + sItemName + " ***************");
for (var sKey:String in objItem)
{
trace(" " + sKey +": " + objItem[sKey]);
}
}
//}
}
Another thing to note is you can use a simple for loop to check through an objects properties, thats what this dumpobject function is doing.
You can directly check it as follow,
var obj:Object = new Object();
if(obj == null)
{
//Do something
}
I stole this from a similar question relating to JS. It requires FP 11+ or a JSON.as library.
function isEmptyObject(obj){
return JSON.stringify(obj) === '{}';
}
can use use the hasProperty method to check for length
var i:int = myObject.hasProperty("length") ? myObject.length: 0;

Resources