ExtJS 4: How can I configure a store to load models for a specific set of IDs? - extjs

For example, say I have a server API for loading people that handles requests like this: GET /people/?id=101,329,27
I'd like to build a Store (probably a custom class that extends Ext.data.Store) which--assuming it has a list of people IDs--causes the proxy to make a request like the one shown above so that the returned data is only for that subset of persons.
I saw the documentation regarding remote filtering, but my concern is that to use it I would first need to call store.load() which would load all persons, then call filter() to do remote filtering. I'd like to just load the subset of persons the first time.
Thanks for any advice!

Found a solution (although still open to hearing other ideas).
First, you can call a store's load() function with a config object that will be passed to an operation. The API docs for Ext.data.Operation make it clear that one of the config options is for an array of Filter objects, so you can do this:
var idFilter = Ext.create('Ext.util.Filter', {
property: 'id',
value: '100,200,300'
});
myStore.load({
filters: [ idFilter ]
});
This results in a request where the URL querystring contains ?filter=[{"property"%3Aid%2C"value"%3A100,200,300}] (in other words, a URL-encoded version of [{ property: 'id', value: '100,200,300'}]).
You can also just call myStore.filter('id', '100,200,300') without having called .load() first. Assuming you have remoteFilter=true in your store, this will make a request with the same query params shown agove.
Sidenote: you can change the keyword used for 'filter' by configuring the 'filterParam' config option for the proxy. For example, if filterParam=q, then the querystring shown above changes to: ?q=[{"property"%3Aid%2C"value"%3A100,200,300}]
Second, you can control "structure" of the filter in the querystring. In my case, I didn't want something like filter={JSON}, as shown above. I wanted a querystring that looked like this:?id=100,200,300
For this I needed to extend a proxy and override the default getParams() function:
Ext.define('myapp.MyRestProxy', {
extend: 'Ext.data.proxy.Rest',
/**
* Override the default getParams() function inherited from Ext.data.proxy.Server.
*
* Note that the object returned by this function will eventually be used by
* Ext.data.Connection.setOptions() to include these parameters via URL
* querystring (if the request is GET) or via HTTP POST body. In either case,
* the object will be converted into one, big, URL-encoded querystring in
* Ext.data.Connection.setOptions() by a call to Ext.Object.toQueryString.
*
* #param {Ext.data.Operation} operation
* #return {Object}
* where keys are request parameter names mapped to values
*/
getParams: function(operation) {
// First call our parent's getParams() function to get a default array
// of parameters (for more info see http://bit.ly/vq4OOl).
var paramsArr = this.callParent(arguments),
paramName,
length;
// If the operation has filters, we'll customize the params array before
// returning it.
if( operation.filters ) {
// Delete whatever filter param the parent getParams() function made
// so that it won't show up in the request querystring.
delete paramsArr[this.filterParam];
// Iterate over array of Ext.util.Filter instances and add each
// filter name/value pair to the array of request params.
for (var i = 0; i < operation.filters.length; i++) {
queryParamName = operation.filters[i].property;
// If one of the query parameter names (from the filter) conflicts
// with an existing parameter name set by the default getParams()
// function, throw an error; this is unacceptable and could cause
// problems that would be hard to debug, otherwise.
if( paramsArr[ queryParamName ] ) {
throw new Error('The operation already has a parameter named "'+paramName+'"');
}
paramsArr[ queryParamName ] = operation.filters[i].value;
}
}
return paramsArr;
}
});

You can also get your Model object to load a record of itself. From a controller you can do:
this.getRequestModel().load(requestID,{ //load from server (async)
success: function(record, operation) {
.....
}
}
where Request is a model class and requestID is an ID to look up. In this scenario Model object needs to define the proxy too:
proxy: {
type: 'ajax',
reader: {
type:'json',
root: 'data'
},
api: {
create : 'request/create.json', //does not persist
read : 'request/show.json'
}
}

Related

Fetch a backbone collection with only the models with specified value

I have a dictionary of type {name: value}
A = {
name: x,
name: y,
name: z
}
I want to fetch a collection (consisting of models having one of their attribute as 'name'), but to be optimal I want to fetch such that the value of the attribute 'name' exists in my dictionary.
Is there a way to do specific filtering like that?
If you're doing the filtering client-side, overriding the filter method is really NOT the way to go.
Now you no longer have it available, should you need it later. Also, modifying the collection itself from within the filter method is an undesirable sideeffect.
Instead you should be using the parse method, which will automatically be called when fetching the collection.
Now as I understand it, you want to limit the fetched set to models with names matching the keys in your dictionary.
If so, I would do the following:
parse: function(response, options) {
// Do we want to filter the response?
if (options.filterNames) {
// Filter
response = _.filter(response, function(obj) {
// Check if this model name is one of the allowed names
return _.contains(options.filterNames, obj.name);
});
}
// Backbone will use the return value to create the collection
return response;
}
And then call fetch using
someCollection.fetch({filterNames: _.keys(someDictionary)});
If you're certain, you will always be filtering the collection on fetch, you can omit passing the option and just use the dictionary within parse.
Alternatively you could create a fetchFiltered() method on the collection, which would then invoke the line above.
After investigations and trials, here are the two ways this can be resolved:
1. Client side filtering after fetching the collection from the server. This is a less optimal method, especially when the collection is huge. In situations when you really want 5 models out of a 1000 model collection, it can be an overkill. But if the server side has no logic of accepting and using the filtering client side filtering should look something like:
Overload the collection filter code something like:
var filter = {
filter: function() {
var results = _.filter(this.models, function(model) {
// Perform the check on this model, like compare it to your local dict
if (checkPassed) {
return true;
}
return false;
});
results = _.map(results, function(model) {
return model.toJSON();
});
// Reset the existing collection to filtered models
this.reset(results) ;
};
var ExtendedCollection = OriginalCollection.extend(filter);
Pass a filter option in the fetch ajax call to the server, and the server should understand the filter and return the collection based off that.

Returning string as json in extJs

I am working on extJs 3.2.x
I have a simple function which returns a string.
public Object getRevenueCurrency(....) {
return "USD";
}
I have Jackson mapper to map the response type to JSON.
<bean name="jsonView" class="org.springframework.web.servlet.view.json.MappingJacksonJsonView">
<property name="contentType">
<!-- <value>application/json</value> -->
<value>text/html</value>
</property>
</bean>
An attempt to retrieve the data goes like this:
currencyStore = new Ext.data.JsonStore({
id:'currencystore',
url: 'xxxxxxx?action=getcurrency',
root: 'string',
listeners: {load: function(store) {
rev_currency=store.?????;
}
}
});
currencyStore.on('exception',function( store, records, options ){
alert('Exception was called');
},this);
Fiddler shows a response from server as :
{"string": "USD"}
Although I get no server or js exception,the exception alert is called.
1.How do i extract the currency value ?
2.What is a way to extract a meaningful information on the exception in the exception handler above?
Concerning your second question:
2.What is a way to extract a meaningful information on the exception in the exception handler above?
According to the docs for the exception event the signature and parameters are:
exception( store, type, action, options, response, arg )
By inspecting the parameters (e.g. by console.log(...)) you should retrieve some useful information.
I notice you have not configured any fields on your store, so when the reader takes your json respsonse its trying to find a field named 'string' to bind to.
Take a look at the ExtJS3 docs for the JsonStore -> http://docs.sencha.com/extjs/3.4.0/#!/api/Ext.data.JsonStore
The sample config shows you how the fields are defined, so your store should look like:
currencyStore = new Ext.data.JsonStore({
id:'currencystore',
url: 'xxxxxxx?action=getcurrency',
fields: ['string'],
listeners: {load: function(store, records, options) {
rev_currency=store.?????;
}
}
});
Note I have also changed the params to your load function handler, so the new record created from your json can be accessed using records[0]
You have configured the store by setting the root property which means the store's reader is expecting a root node in the json matching 'string'.
So the reader is actually expecting the below form of response.
{"string":[{"propertyname":"USD"}]}
Instead you could just take out the root property on your store config since all that's being returned is a flat object

Cannot set priority on $asObject()'s child keys using Angularfire 0.8

According to the Angularfire docs, when working with an object returned through $asObject(), you can set priority for said object by defining a $priority property on the object and then using $save().
My code works great, but $priority isn't doing anything. Here's some code with complete explanations in the comments:
app.factory('MyService', function($rootScope, $firebase) {
// The complete Firebase url
var ref = *obfuscated*;
// Returning the dataset as an object containing objects
var data = $firebase(ref).$asObject;
// This object is what's returned by MyService
var Data = {
// Method to create a new object within the data set,
// keyed by myId. Attempting to set priority for the
// record via $priority. returnData.uid is a valid string.
create: function(returnData, myId) {
data[myId] = {
myId: myId,
$priority: returnData.uid
};
// No need to explain the stuff between here and the
// $rootScope listener below, just added for context
data.$save().then(function() {
setMyId(myId);
});
},
findByMyId: function(myId) {
if (myId) {
return data[myId];
}
}
};
function setMyId(myId) {
$rootScope.myId = User.findByMyId(myId);
}
// This event listener works fine, fires
// at user login and returns data
$rootScope.$on('$firebaseSimpleLogin:login', function(e, returnData) {
// returnData.uid has the correct value - this
// function should return the object(aka record) with
// a priority matching returnData.uid
var query = $firebase(ref.startAt(returnData.uid).endAt(returnData.uid)).$asObject();
// console shows an object with the normal $firebase
// properties, but no records. If I define query without
// limiting the set ($firebase(ref).$asObject()), it returns
// the full set just fine. The priority on all records is still
// null.
console.log(query);
query.$loaded(function() {
setData(query.myId);
});
});
return Data;
});
Yes, I'm following Thinkster.io's tutorial and I'm in Chapter 7. No, this is not a duplicate of the other questions about that chapter, I already found my way around the pre-Angularfire 0.8 code present in their examples, just can't set $priority, and I've spent about 5 hours so far trying to find a solution through my own efforts and on the web.
Any takers?
When viewed in the light of how JavaScript works with objects (i.e. unordered), how JSON handles objects (i.e. unordered), and in light of the expectation that AngularFire's $asObject() method is intended for storing key/value pairs, and singular records that are not used as a collection, this starts to make some sense.
Internally, the synchronize'd object's $save method calls Firebase's setWithPriority. In set or setWithPriority calls, the child nodes are replaced. Any meta data like priorities on those children are replaced.
In AngularFire, $asArray is intended to handle ordered collections, and provides the ability to set $priority on child nodes (only one level deep, of course, as it treats its children as singular records that are not themselves collections).
Since, in your case, you want to work with fixed keys rather than push ids, you'll probably want to override the $add method using $extendFactory and do something like the following:
angular.module(MY_APP).factory('FixedKeysFactory', function($FirebaseArray, $firebaseUtils) {
return $FirebaseArray.$extendFactory({
$add: function(data) {
this._assertNotDestroyed('$add');
if( angular.isObject(data) && typeof data.$id === 'string' ) {
return this.$inst().$set(data.$id, $firebaseUtils.toJSON(data));
}
else {
return this.$inst().$push($firebaseUtils.toJSON(data));
}
}
});
});
You could then pass this into your $firebase instance in place of the default factory:
var list = $firebase(ref, {arrayFactory: FixedKeysFactory}).$asArray();
A simpler but less awesomatic™ solution would be to manually add your objects to the array, manually giving them a $id, then call $save:
var list = $firebase(ref).$asArray();
var i = list.length;
list.push({ foo: 'bar', $id: 'kato' });
list.$save(i);
Some notes on the future: It will soon be possible to use any field as sort criteria and there will be no need to set priorities (yay!). It will probably be possible to set your own $id before calling $add on a synchronized array in AngularFire as soon as I clear that with the other devs (like the 0.8.3 release).

Make backbone model fetch discard missing fields

Let's say I have a Backbone model that when fetch() is called (for that specific model, not the entire collection) it gets this from the server:
{ a: "val-1", b: "val-2" }
Later, I call fetch() on the model again, and this time the server returns:
{ b: "val-x", c: "val-y" }
At this point, I would like my model to have this state, because that's the latest state provided by the server:
{ b: "val-x", c: "val-y" }
However, my Backbone model has this state instead:
{ a: "val-1", b: "val-x", c: "val-y" }
How do I fetch() a model in Backbone so that the resulting state is exactly what is returned from the server, and doesn't include old obsolete fields?
I discovered that one way to get the desired behavior is by adding this parse function to the model in question:
parse: function(resp) {
for (var key in this.attributes) {
if (resp[key] === undefined) {
resp[key] = undefined;
}
}
return resp;
}
If the response from the server lacks an attribute that's in the local model, it explicitly adds that key to the response with an undefined value. This causes fetch() to behave correctly and remove it from the model.
If you want fetch to discard your local state and take the server data without merging then use the {reset: true} option.
From the docs: http://backbonejs.org/#Collection-fetch
When the model data returns from the server, it uses set to
(intelligently) merge the fetched models, unless you pass {reset:
true}, in which case the collection will be (efficiently) reset.

Sencha - store.each() - Data are not available, they are only deep in data class

I have got:
Ext.define("catcher.view.Login", {
extend: "Ext.form.Panel",
// creating login form, including selectfield
Store "Tournaments" is created in stores (autoload:true), have its model. Everyting is set.
need to dynamicly fill selectfield (still in view.Login class):
initialize: function(){
var store = Ext.getStore("Tournaments");
var options = new Array();
store.each(function(radek){
options[radek.get("tournament_id")] = radek.get("tournament_name");
});
}
I do not want to use store:"Tournaments" config options, because of later form.submit(); does not send correct data from selectfield.
There is the problem: console.log(store.getCount()); returns 0. Using store.add({ ... }) I can add anything and getCount() returns corrent count (0 + add()).
Weird part: console.log(store) returns whole class including data object with all items inside. And next weird part - If I use the same code in controller, everything works, the Store is loaded properly and I can use mystore.each();
Store loading is asynchronous, by the time you're accessing it, it's not loaded. You need to listen to the store load event.
Something like:
store.on('load', function(storeRef, records, successful){
//Loop through records
}, this);
on() documentation
load event documentation

Resources