Ember Data model find by query deferred? - javascript

If I have a query that defines the model for my route:
App.MyRoute = Ember.Route.extend({
model: function (params) {
return this.store.find('MyModel', {myProperty: myValue}).then(function(results) {
return results.get('firstObject');
});
}
});
How do I handle the case where the store has not yet been populated with the results of the above query?
I want the query to update the model with the results of the above query whenever a value is inserted into the store that satisfies the query.
Is this possible?
NOTE: I'm using the FixtureAdapter as I'm not using a REST back end and I don't need the persistence layer. As such, I have implemented the findQuery method myself as follows:
App.ApplicationAdapter = DS.FixtureAdapter.extend({
queryFixtures: function(records, query, type) {
return records.filter(function(record) {
for(var key in query) {
if (!query.hasOwnProperty(key)) { continue; }
var value = query[key];
if (record[key] != value) { return false; }
}
return true;
});
}
});
I am absolutely open to changing this up if it will help me reach this end.

Use a filter, it is a live record array that updates as new records are injected into the store. As a side note, filter doesn't make a call to the server for records, so if you still want that to happen you'll want to still make the find call, and then just return the filter.
App.MyRoute = Ember.Route.extend({
model: function (params) {
// trigger a find
this.store.find('MyModel', {myProperty: myValue});
// return a live filter (updates when the store updates)
return this.store.filter('MyModel', function(record){
return record.get('myProperty') == myValue;
});
}
});

Related

Asynchronous data retrieving and rendering in ExtJS with Ajax

so I have this situation:
renderer: function(value, grid, record) {
var testAjax = function(callback) {
Ext.Ajax.request({
url: appConfig.baseUrl + '/api/users/' + record.getData().id + '/jobRoles',
method: 'GET',
success: function(result) {
callback(result)
};
});
};
return testAjax(function(result) {
try {
result = JSON.parse(result.responseText);
} catch(e) {
return '';
}
result = result.data;
var roles = _.map(result, function(jRole) {
console.log(jRole);
return jRole.name;
}).join(',');
console.log("Roles: ", roles);
return roles;
});
}
What I wanted to achieve is that when I have to render a particular field, I make a call to my Loopback endpoint, retrieve some data about a relation, map it using a "," character and return the joined string in order to view it.
However, I think I have a few problem with callbacks here as I don't see the result at all, as if the function is returning before the callback is called (thus showing nothing instead of what I retrieved from the server).
I tried to look here and there, and this is the best I came up with.
How can I return to the parent function the "roles" variable? How do I properly set up my callbacks?
Regards
You cannot and should not use the renderer with load operations and asynchonous callbacks. The renderer can be called dozens of times for the same record, if you filter or sort or just refresh the grid view. What you want to do is get all the information required for display in the grid in a single call. Data you cannot get in the single call should not be shown in the grid. You don't want to call the endpoint 1000 times for 1000 records, because even if each call needs only 60ms, that's a full minute.
That said, if you really have to, because you cannot change the endpoints and the roles have to be displayed, you can do as follows:
dataIndex: 'MyTempRoles',
renderer: function(value, grid, record) {
if(value) return value; // show the loaded value if available
else { // no value loaded -> load value
Ext.Ajax.request({
url: appConfig.baseUrl + '/api/users/' + record.getData().id + '/jobRoles',
method: 'GET',
success: function(result) {
try {
result = JSON.parse(result.responseText);
result = result.data;
var roles = _.map(result, function(jRole) {
console.log(jRole);
return jRole.name;
}).join(',');
record.set("MyTempRoles", roles || " "); // put the loaded value into the record. This will cause a grid row refresh, thus a call to the renderer again.
} catch(e) {
}
}
});
}
}
This will call the backend in the first call to the renderer, and asynchronously fill the displayed record's temp variable. When the temp variable is filled, the renderer will then display the value from the temp variable automatically.

Maintaining a resource collection with ngResource, socket.io and $q

I am trying to create an AngularJS factory that maintains a collection of resources automatically by retrieving the initial items from the API and then listening for socket updates to keep the collection current.
angular.module("myApp").factory("myRESTFactory", function (Resource, Socket, ErrorHandler, Confirm, $mdToast, $q, $rootScope) {
var Factory = {};
// Resource is the ngResource that fetches from the API
// Factory.collection is where we'll store the items
Factory.collection = Resource.query();
// manually add something to the collection
Factory.push = function(item) {
Factory.collection.push(item);
};
// search the collection for matching objects
Factory.find = function(opts) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
resolve(_.where(Factory.collection, opts || {}));
});
});
};
// search the collection for a matching object
Factory.findOne = function(opts) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
var item = _.findWhere(collection, opts || {});
idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
resolve(Factory.collection[idx]);
});
});
};
// create a new item; save to API & collection
Factory.create = function(opts) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
Resource.save(opts).$promise.then(function(item){
Factory.collection.push(item);
resolve(item);
});
});
});
};
Factory.update = function(item) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
Resource.update({_id: item._id}, item).$promise.then(function(item) {
var idx = _.findIndex(collection, function(u) {
return u._id === item._id;
});
Factory.collection[idx] = item;
resolve(item);
});
});
});
};
Factory.delete = function(item) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
Resource.delete({_id: item._id}, item).$promise.then(function(item) {
var idx = _.findIndex(collection, function(u) {
return u._id === item._id;
});
Factory.collection.splice(idx, 1);
resolve(item);
});
});
});
};
// new items received from the wire
Socket.on('new', function(item){
idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
if(idx===-1) Factory.collection.push(item);
// this doesn't help
$rootScope.$apply();
});
Socket.on('update', function(item) {
idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
Factory.collection[idx] = item;
// this doesn't help
$rootScope.$apply();
});
Socket.on('delete', function(item) {
idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
if(idx!==-1) Factory.collection.splice(idx, 1);
});
return Factory;
});
My backend is solid and the socket messages come through correctly. However, the controllers don't respond to updates to the collection if any of the Factory methods are used.
i.e.
This works (responds to socket updates to the collection):
$scope.users = User.collection;
This does not work (it loads the user initially but is not aware of updates to the collection):
User.findOne({ _id: $routeParams.user_id }).then(function(user){
$scope.user = user;
});
How can I get my controllers to respond to update to changes to the collection?
Update:
I was able to implement a workaround in the controller by changing this:
if($routeParams.user_id) {
User.findOne({ _id: $routeParams.user_id }).then(function(user){
$scope.user = user;
});
}
To this:
$scope.$watchCollection('users', function() {
if($routeParams.user_id) {
User.findOne({ _id: $routeParams.user_id }).then(function(user){
$scope.user = user;
});
}
});
However, nobody likes workarounds, especially when it involves redundant code in your controllers. I am adding a bounty to the question for the person who can solve this inside the Factory.
The solution is for the factory methods to return an empty object/array to be populated later (similar to the way ngResource works). Then attach the socket listeners to both those return objects/arrays and the main Factory.collection array.
angular.module("myApp").factory("myRESTFactory",
function (Resource, Socket, ErrorHandler, Confirm, $mdToast, $q) {
var Factory = {};
// Resource is the ngResource that fetches from the API
// Factory.collection is where we'll store the items
Factory.collection = Resource.query();
// This function attaches socket listeners to given array
// or object and automatically updates it based on updates
// from the websocket
var socketify = function(thing, opts){
// if attaching to array
// i.e. myRESTFactory.find({name: "John"})
// was used, returning an array
if(angular.isArray(thing)) {
Socket.on('new', function(item){
// push the object to the array only if it
// matches the query object
var matches = $filter('find')([item], opts);
if(matches.length){
var idx = _.findIndex(thing, function(u) {
return u._id === item._id;
});
if(idx===-1) thing.push(item);
}
});
Socket.on('update', function(item) {
var idx = _.findIndex(thing, function(u) {
return u._id === item._id;
});
var matches = $filter('find')([item], opts);
// if the object matches the query obj,
if(matches.length){
// and is already in the array
if(idx > -1){
// then update it
thing[idx] = item;
// otherwise
} else {
// add it to the array
thing.push(item);
}
// if the object doesn't match the query
// object anymore,
} else {
// and is currently in the array
if(idx > -1){
// then splice it out
thing.splice(idx, 1);
}
}
});
Socket.on('delete', function(item) {
...
});
// if attaching to object
// i.e. myRESTFactory.findOne({name: "John"})
// was used, returning an object
} else if (angular.isObject(thing)) {
Socket.on('update', function(item) {
...
});
Socket.on('delete', function(item) {
...
});
}
// attach the socket listeners to the factory
// collection so it is automatically maintained
// by updates from socket.io
socketify(Factory.collection);
// return an array of results that match
// the query object, opts
Factory.find = function(opts) {
// an empty array to hold matching results
var results = [];
// once the API responds,
Factory.collection.$promise.then(function(){
// see which items match
var matches = $filter('find')(Factory.collection, opts);
// and add them to the results array
for(var i = matches.length - 1; i >= 0; i--) {
results.push(matches[i]);
}
});
// attach socket listeners to the results
// array so that it is automatically maintained
socketify(results, opts);
// return results now. initially it is empty, but
// it will be populated with the matches once
// the api responds, as well as pushed, spliced,
// and updated since we socketified it
return results;
};
Factory.findOne = function(opts) {
var result = {};
Factory.collection.$promise.then(function(){
result = _.extend(result, $filter('findOne')(Factory.collection, opts));
});
socketify(result);
return result;
};
...
return Factory;
};
The reason this is so so great is that your controllers can be ridiculously simple yet powerful at the same time. For example,
$scope.users = User.find();
This returns an array of ALL users that you can use in your view; in an ng-repeat or something else. It will automatically be updated/spliced/pushed by updates from the socket and you don't need to do anything extra to get that. But wait, there's more.
$scope.users = User.find({status: "active"});
This will return an array of all active users. That array will also automatically be managed and filtered by our socketify function. So if a user is updated from "active" to "inactive", he is automatically spliced from the array. The inverse is also true; a user that gets updated from "inactive" to "active" is automatically added to the array.
The same is true for the other methods as well.
$scope.user = User.findOne({firstname: "Jon"});
If Jon's email changes, the object in the controller is updated. If his firstname changes to "Jonathan", $scope.user becomes an empty object. Better UX would be to do soft-delete or just mark the user deleted somehow, but that can be added later.
No $watch, $watchCollection, $digest, $broadcast, required--it just works.
Don't expose the collection property on Factory, keep it as a local variable.
Create a new exposed getter/setter on the Factory that proxies to and from the local variable.
Use the getter/setter Object internally in your find methods.
Something like this:
// internal variable
var collection = Resource.query();
// exposed 'proxy' object
Object.defineProperty(Factory, 'collection', {
get: function () {
return collection;
},
set: function (item) {
// If we got a finite Integer.
if (_.isFinite(item)) {
collection.splice(item, 1);
}
// Check if the given item is already in the collection.
var idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
if (idx) {
// Update the item in the collection.
collection[idx] = item;
} else {
// Push the new item to the collection.
collection.push(item);
}
// Trigger the $digest cycle as a last step after modifying the collection.
// Can safely be moved to Socket listeners so as to not trigger unnecessary $digests from an angular function.
$rootScope.$digest();
}
});
/**
* Change all calls from 'Factory.collection.push(item)' to
* 'Factory.collection = item;'
*
* Change all calls from 'Factory.collection[idx] = item' to
* 'Factory.collection = item;'
*
* Change all calls from 'Factory.collection.splice(idx, 1) to
* 'Factory.collection = idx;'
*
*/
Now, seeing as how the non angular parties modify your collection (namely Sockets in this case), you will need to trigger a $digest cycle to reflect the new state of the collection.
If you are only ever interested in keeping the collection in sync in a single $scope (or multiple ones, but not cross-scope) I would attach said $scope's to the factory, and run the $digest there instead of $rootScope. That will save you a little bit of performance down the line.
here's a jsbin showcasing how the usage of an Object.getter will keep your collection in sync and allow you to find items recently added to the collection.
I've opted for setTimeout in the jsbin so as to not trigger automatic $digests through the usage of $interval.
Obviously the jsbin is very barebones; There's no promises being shuffled around, no socket connections. I only wanted to showcase how you can keep things in sync.
I will admit that Factory.collection = value looks whack, but you could hide that away with the help of wrapping functions to make it more pretty / read better.

Array of JavaScript objects: change value and datatype of one key if condition met

I'm pulling some data from a MongoDB collection using Mongoose. I'm getting back my array of objects with just the fields I've selected. All good. Here's my code:
var fieldsToReturn = 'username password';
User.find({username: {$exists: true}}, fieldsToReturn, {sort: {'created.when': 1}}, function(err, data){
if (err) {
return err;
} else {
// clever code needed here to manipulate the data!
return res.json(data);
}
});
What I want to do is iterate through the array of JavaScript objects returned in data and if there is a password (it'll be a text string) replace password with a boolean true, but if it's null then return a boolean false.
I've tried a bit of underscore.js magic:
_.each(data, function(element, index, list){
if (element.password !== null) {element.password = true};
});
But what I get in the JSON that's returned is "password":"true" and not "password":true. Also tried element.password = new Boolean(true). Any suggestions?
You could use map for this.
data = data.map(function(element) {
if (element.password) {
element.password = true;
}
return element;
});
var data = [
{
password: 'secret'
},
{
password: ''
},
{
password: 'swordfish'
}
];
data = data.map(function(element) {
if (element.password) {
element.password = true;
}
return element;
});
document.querySelector('pre').innerText = JSON.stringify(data);
<pre></pre>
Try using asyncjs lybrary. It will call the callback function when finished all the executions for the array objects. Please, also read carefully about asyncronous flow in javascript (nodejs) .
Hope it helps!!
//this should work if placed on your clever code and installed asyncjs
async.map(data, checkPassword, function(err, results){
//when arrived here, all the array has been checked and modified
console.log(results);
}
//Remember This function must be placed outside of the find() method.
function checkPassword(obj, callback)
{
if (obj.password) {
obj.password = true;
}
return callback(null, obj);
}
It's Mongoose. Something is happening before the data is returned which makes it look like straight JSON but if you try and manipulate the element in a _.each loop then the object you're working on (element) isn't a simple object mapping to the document in MongoDB. Any Mongoose experts out there that can shed light on this?
What I did to fix this is use LoDash's _.mapValues:
function renderUserKeys(obj){
var newObj = _.mapValues(obj, function(n){return n});
if (newObj.password !== null) {newObj.password = true};
return newObj;
};
And then in the code:
...
} else {
var newArray = [];
_.each(data, function(element, index, list){
newArray.push(renderUserKeys(element._doc)); // note the ._doc
});
return res.json(newArray);
};
Maybe there is a better function that _.mapValues (maybe _.merge?) to perform the copy, I'll need to look into this, but for now this works and my Mongoose middleware is still running normally (as I'm not using lean()).
Lesson learned: Mongoose !== Duck. As in, it may look like an object but when you try and manipulate it, it doesn't behave as you expect it to!

In my service-factory I lookup up a large dataset - I want to persist it and check for its existence to avoid calling it again

My service (factory) makes an API call and assigns response data to a variable:
.factory('MedicationMatchingNamesFactory',
['$http', '$q', 'MedicationDisplayNamesFactory',
function MedicationMatchingNamesFactory($http, $q, MedicationDisplayNamesFactory){
return {
getMatchingNames: function(inputValue){
var matches = [];
// I thought this may be set as null so I'm able to
// differentiate it from the below array form (when exists)
var allNames = null;
MedicationDisplayNamesFactory.getDisplayNames().then(
function(response){
// this is the large dataset - now as array
var allNames = response.data.displayTermsList.term;
angular.forEach(allNames, function(value, key){
if(value.indexOf( inputValue ) !== -1){
matches.push(value);
}
});
}
);
return matches;
}
};
return MedicationMatchingNamesFactory;
}])
I'm using Angular-UI's "ui-select" directive to search within this large dataset based on entered string.
In my controller:
$scope.$on('inputStarted', function(event, value){
if(value.length > 3){
$scope.meds.displayNames = MedicationMatchingNamesFactory.getMatchingNames(value);
console.log($scope.meds.displayNames);
}
});
Now, to avoid querying the API (actually call another service containing the call to API) every time the number of input characters is greater than 3, I think it would be great if I'm able to check whether allNames is null (empty, do call API) or it's an array (skip the call, just use that).
I tried moving the angular.forEach part outside the call and promise but then, obviously, nothing happens because it's not resolved yet on the first run.
Is there a way to have this allNames dataset checked before I do the API call?
You can move the declaration of allNames outside the return statement of your service, and then check if it's null before querying the API. That way allNames serves as a cache for the results of the inner API call.
Also, note that it would be better if you return a promise from getMatchingNames, otherwise your service always returns a blank array right away, which then gets filled later, after the inner API call is completed. If you do return a promise, you would need to change the way you are setting displayNames in your inputStarted event handler:
MedicationMatchingNamesFactory.getMatchingNames.then(function(matches) {
$scope.meds.displayNames = matches;
});
and your service could look like this:
.factory('MedicationMatchingNamesFactory',
['$http', '$q', 'MedicationDisplayNamesFactory',
function MedicationMatchingNamesFactory($http, $q, MedicationDisplayNamesFactory) {
var allNames = null;
function getMatches(inputValue, list){
var matches = [];
angular.forEach(list, function(value, key){
if(value.indexOf(inputValue) !== -1){
matches.push(value);
}
});
return matches;
}
return {
getMatchingNames: function(inputValue){
var deferred = $q.defer();
if (allNames !== null) {
deferred.resolve(getMatches(inputValue, allNames));
} else {
MedicationDisplayNamesFactory.getDisplayNames().then(
function(response){
// this is the large dataset - now as array
allNames = response.data.displayTermsList.term;
deferred.resolve(getMatches(inputValue, allNames));
},
function(reason){
deferred.reject(reason);
}
);
}
return deferred.promise;
}
};
}])
Actually the solution to this problem is very simple: I just went with the sessionStorage which seems to fit perfectly my needs in this case as I need the big dataset persisted just for this session and it doesn't matter if it's lost on the next one. The goal is to prevent fetching it more than once, that is, more than necessary, as the values inside are not likely to be changed during that session.

Multi-Property lookup with Backbone and Underscore

I have a method on my collection called activeCall that takes an id as a parameter. This method does a _find with underscore and returns the first match. What I want to do is pass multiple parameters to the method, then have the .get() function use those params.
Here's what I currently have:
var Activities = Backbone.Collection.extend({
model: Activity,
url: "/activity",
activeCall: function (Activity_id) {
return _.find(this.models, function (item) {
return item.get("Activity_id") === Activity_id;
});
}
});
Here is what I'd like to do:
var Activities = Backbone.Collection.extend({
model: Activity,
url: "/activity",
activeCall: function (Activity_id, CallType_id) {
return _.find(this.models, function (item) {
//How do I use both Activity_id & CallType_id with the call to item.get()?
return item.get("Activity_id") === Activity_id && item.get("CallType_id") === CallType_id;
});
}
});
Well, turns out return item.get("Activity_id") === Activity_id && item.get("CallType_id") === CallType_id; actually works. I tried it just for kicks earlier and it did not work. I must have been doing something wrong because it's working now.

Categories

Resources