Ember 1.5.1
Ember-Data 1.0 beta 7
I've tried to modify the DS.ActiveModelAdapter's findMany so it'll get in chunks of 40... this is because I can't use the links feature and it seems to be generating 400 errors because it has too many ids in the URL its creating.
I tried using this adapter, but I keep getting error messages that look like this:
Error: Assertion Failed: Error: no model was found for 'super'
Here's my Adapter:
App.ApplicationAdapter = DS.ActiveModelAdapter.extend({
findMany: function(store, type, ids) {
self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
var idsPerRequest = 40;
var totalIdsLength = ids.length;
var numberOfBins = Math.ceil( totalIdsLength / idsPerRequest ); // number per bin
var bins = [];
ids.forEach( function(someId, index) {
var thisBinIndex = index % numberOfBins;
var thisBin = Ember.A( bins[thisBinIndex] );
thisBin.pushObject(someId);
bins[thisBinIndex] = thisBin;
});
var requestPromises = bins.map(function(binOfIds) {
return self.ajax(self.buildURL(type.typeKey), 'GET', { data: { ids: binOfIds } });
});
Ember.RSVP.all(requestPromises).then(function(resolvedBinRequests) {
var resolvedObjects = Em.A([]);
resolvedBinRequests.forEach(function(resolvedBin) {
resolvedObjects.addObjects(resolvedBin);
});
resolve(resolvedObjects);
}, function(error) {
reject(error);
});
});
}
});
Can anyone help me out with this? It'd be really appreciated. Am I just missing something obvious or have I perhaps done something silly?
Thanks in advance!
[edit] Okay so further to this I've figured out why it's not working, and that's because the response that's coming back is a promise for the JSON payload, but what I'm doing is joining multiples of these into an array and returning that... which obviously won't be right... but what I need to do is merge the arrays inside the objects returned into one, I think (in concept)... I'm not really sure how to do this in actuality, though... I've tried various things, but none of them seem to work well... :(
I'm not sure how much control you have over the back-end, but this seems like a perfect use case for using links instead of returning all of the ids.
App.Foo = DS.Model.extend({
bars: DS.hasMany('bar', {async:true})
});
App.Bar = DS.Model.extend({
name: DS.attr()
});
Then when you query for foo your json returns a link instead of a list of ids
{
foo: {
id:1,
links: {
bars: '/foo/1/bars' // or anything, you could put /bars?start=1&end=9000
}
}
}
Here's an example with 1000 relationship records hitting a simple endpoint:
http://emberjs.jsbin.com/OxIDiVU/579/edit
Okay so I finally worked out how to make this work.
I'll share my answer here for future posterity ;-)
Of interest is that the required response had to be a promise and it had to contain a straight up JS object, so I "munged" all the responses into one JS object and manually built the pluralized camelized type key... I wasn't sure how else to do this. So... sorry it's so hacky, but this actually works and lets me fix my app for now until the "links" feature is working again.
App.ApplicationAdapter = DS.ActiveModelAdapter.extend({
findMany: function(store, type, ids) {
self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
var idsPerRequest = 40;
var totalIdsLength = ids.length;
var numberOfBins = Math.ceil( totalIdsLength / idsPerRequest ); // number per bin
var bins = [];
ids.forEach( function(someId, index) {
var thisBinIndex = index % numberOfBins;
var thisBin = Ember.A( bins[thisBinIndex] );
thisBin.pushObject(someId);
bins[thisBinIndex] = thisBin;
});
// build an array of promises, then resolve using Ember.RSVP.all
var requestPromises = bins.map(function(binOfIds) {
return self.ajax(self.buildURL(type.typeKey), 'GET', { data: { ids: binOfIds } });
});
// build the required return object, which is a promise containing a plain JS object
// note this can't be an Ember object
Ember.RSVP.all(requestPromises).then(function(resolvedBinRequests) {
var pluralizedDecamelizedTypeKey = type.typeKey.decamelize().pluralize();
var resolvedObjects = Em.A([]);
var returnObject = {};
returnObject[pluralizedDecamelizedTypeKey] = resolvedObjects;
resolvedBinRequests.forEach(function(resolvedBin) {
var theArray = resolvedBin[pluralizedDecamelizedTypeKey];
resolvedObjects.addObjects(theArray);
});
var responsePromise = Ember.RSVP.Promise.cast(returnObject);
resolve(responsePromise);
}, function(error) {
reject(error);
});
});
}
});
After some feedback I updated this response to attempt to extract the response payloads in the serializer instead of attempting to mimic the store's logic in the adapter.
http://emberjs.jsbin.com/wegiy/60/edit
App.ApplicationAdapter = DS.ActiveModelAdapter.extend({
findMany: function(store, type, ids) {
// build an array of promises, then resolve using Ember.RSVP.all
var idsPerRequest = 40;
var totalIdsLength = ids.length;
var numberOfBins = Math.ceil( totalIdsLength / idsPerRequest ); // number per bin
var bins = [];
ids.forEach( function(someId, index) {
var thisBinIndex = index % numberOfBins;
var thisBin = Ember.A( bins[thisBinIndex] );
thisBin.pushObject(someId);
bins[thisBinIndex] = thisBin;
});
var requestPromises = bins.map(function(binOfIds) {
return self.ajax(self.buildURL(type.typeKey), 'GET', { data: { ids: binOfIds } });
});
return Ember.RSVP.all(requestPromises);
}
});
App.ApplicationSerializer = DS.ActiveModelSerializer.extend({
extractFindMany: function(store, type, responsePayloads) {
// responsePayloads is the resolved value from the Ember.RSVP.all(requestPromises) promise
var serializer = this;
var extractedResponses = responsePayloads.map(function(payload) {
return serializer.extractArray(store, type, payload);
});
// extractedResponses is an array of arrays. We need to flatten it into 1 array.
return [].concat.apply([], extractedResponses);
}
});
Related
I'm trying to test React with Flux code using Jest. I'm reasonably new to unit testing.
I think I'm doing something wrong with Mocking my dependancies (to be honest the mocking thing kind of confuses me).
Here is what I'm having trouble with:
//LoginStore-test.js
jest.dontMock('../../constants/LoginConstants');
jest.dontMock('jsonwebtoken');
jest.dontMock('underscore');
jest.dontMock('../LoginStore');
describe("login Store", function(){
var LoginConstants = require('../../constants/LoginConstants');
var AppDispatcher;
var LoginStore;
var callback;
var jwt = require('jsonwebtoken');
var _user = {
email: 'test#test.com'
};
//mock actions
var actionLogin = {
actionType: LoginConstants.LOGIN_USER,
'jwt': jwt.sign(_user, 'shhh', { expiresInMinutes: 60*5 })
};
beforeEach(function(){
AppDispatcher = require('../../dispatchers/AppDispatcher');
LoginStore = require('../LoginStore');
callback = AppDispatcher.register.mock.calls[0][0];
});
...
it('should save the user', function(){
callback(actionLogin);
var user = LoginStore.getUser();
expect(user).toEqual(_user);
});
});
});
LoginStore.js file:
var AppDispatcher = require('../dispatchers/AppDispatcher');
var BaseStore = require('./BaseStore');
var LoginConstants = require('../constants/LoginConstants.js');
var _ = require('underscore');
var jwt = require('jsonwebtoken');
//initiate some variables
var _user;
var _jwt;
var LoginStore = _.extend({}, BaseStore, {
getUser: function(){
return _user;
}
});
AppDispatcher.register(function(action){
switch(action.actionType){
case LoginConstants.LOGIN_USER:
//set the user
_user = jwt.decode(action.jwt);
//save the token
_jwt = action.jwt;
break;
//do nothing with the default
default:
return true;
}
LoginStore.emitChange();
return true;
});
module.exports = LoginStore;
The jsonwebtoken functionality doesn't seem to be working at all. If I log actionLogin.jwt it just returns undefined. Any idea what I'm doing wrong here?
Cheers
After a bit of searching around, and actually trying to figure out a different issue I found the answer. just add
"jest": {"modulePathIgnorePatterns": ["/node_modules/"]}
to your package.json file
I have the following code in my route for receiving webocket updates of my models. The problem is that when this line executes
setTimeout(self.stompConnect, 10000);
I no longer have access to the Ember.Route Ember object at the top of the stompConnect method.
var self = this; //no longer pointing to my route
How can I maintain the ember context across the callback in a third pary library like this? This has nothing to do with websockets or the library because I had the same issue with another third party library that had a callback.
I guess I need to use .bind() or something but I don't know the correct syntax.
stompClient : null,
activate : function() {
this.stompConnect();
},
stompConnect : function() {
var self = this;
var connectCallback = function(frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/models/update', function(payload){
var model = JSON.parse(JSON.parse(payload.body));
var modelName = Object.keys(model)[0];
var modelPayload = model[modelName];
self.store.push(modelName, modelPayload);
});
};
var errorCallback = function (error) {
console.log('STOMP: ' + error);
setTimeout(self.stompConnect, 10000); //when stompConnect() is called, the ember context is lost :(
console.log('STOMP: Reconecting in 10 seconds');
};
var url = ... ;
var socket = new SockJS(url);
var stompClient = Stomp.over(socket);
stompClient.connect({}, connectCallback, errorCallback);
this.set('stompClient', stompClient);
},
deactivate : function() {
this.get('stompClient').disconnect();
},
Basically you have a callback inside a callback. So context needs to be passed in both callbacks. self will work in errorCallBack but needs to be set again to work in stompConnect. I would rather suggest using run.later to setTimeOut. So Here goes the code.
stompClient : null,
activate : function() {
this.stompConnect();
},
stompConnect : function() {
var self = this;
var connectCallback = function(frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/models/update', function(payload){
var model = JSON.parse(JSON.parse(payload.body));
var modelName = Object.keys(model)[0];
var modelPayload = model[modelName];
self.store.push(modelName, modelPayload);
});
};
var errorCallback = function (error) {
console.log('STOMP: ' + error);
Ember.run.later(this, this.stompConnect, 1000);
//or you can also use
//setTimeout(this.stompConnect.bind(this), 10000); when stompConnect() is called, the ember context is lost :(
console.log('STOMP: Reconecting in 10 seconds');
};
var url = ... ;
var socket = new SockJS(url);
var stompClient = Stomp.over(socket);
stompClient.connect({}, connectCallback, errorCallback.bind(this));
this.set('stompClient', stompClient);
},
deactivate : function() {
this.get('stompClient').disconnect();
}
I prefer using .bind() rather var self = this;. But it depends.
The problem with this code is that the render code is entered twice, and the buffer is not where I expect it. Even when I get the buffer, the stuff I push in is not rendered to the screen.
App.FilterView = Ember.View.extend({
init: function() {
var filter = this.get('filter');
this.set('content', App.ViewFilter.find(filter));
this._super();
},
render: function(buffer) {
var content = this.get('content');
if(!this.get('content.isLoaded')) { return; }
var keys = Object.keys(content.data);
keys.forEach(function(item) {
this.renderItem(buffer,content.data[item], item);
}, this);
}.observes('content.isLoaded'),
renderItem: function(buffer, item, key) {
buffer.push('<label for="' + key + '"> ' + item + '</label>');
}
});
And the App.ViewFilter.find()
App.ViewFilter = Ember.Object.extend();
App.ViewFilter.reopenClass({
find: function(o) {
var result = Ember.Object.create({
isLoaded: false,
data: ''
});
$.getJSON("http://localhost:3000/filter/" + o, function(response) {
result.set('data', response);
result.set('isLoaded', true);
});
return result;
}
});
I am getting the data I expect and once isLoaded triggers, everything runs, I am just not getting the HTML in my browser.
As it turns out the answer was close to what I had with using jquery then() on the $getJSON call. If you are new to promises, the documentation is not entirely straight forward. Here is what you need to know. You have to create an object outside the promise - that you will return immediately at the end and inside the promise you will have a function that updates that object once the data is returned. Like this:
App.Filter = Ember.Object.extend();
App.Filter.reopenClass({
find: function(o) {
var result = Ember.Object.create({
isLoaded: false,
data: Ember.Object.create()
});
$.getJSON("http://localhost:3000/filter/" + o).then(function(response) {
var controls = Em.A();
var keys = Ember.keys(response);
keys.forEach(function(key) {
controls.pushObject(App.FilterControl.create({
id: key,
label: response[key].label,
op: response[key].op,
content: response[key].content
})
);
});
result.set('data', controls);
result.set('isLoaded', true);
});
return result;
}
});
Whatever the function inside then(), is the callback routine that will be called once the data is returned. It needs to reference the object you created outside the $getJSON call and returned immediately. Then this works inside the view:
didInsertElement: function() {
if (this.get('content.isLoaded')) {
var model = this.get('content.data');
this.createFormView(model);
}
}.observes('content.isLoaded'),
createFormView: function(data) {
var self = this;
var filterController = App.FilterController.create({ model: data});
var filterView = Ember.View.create({
elementId: 'row-filter',
controller: filterController,
templateName: 'filter-form'
});
self.pushObject(filterView);
},
You can see a full app (and bit more complete/complicated) example here
I have a controller that observes a search field like so:
Scrolls.IndexController = Ember.ArrayController.extend({
searchResult: function() {
var that = this;
this.get('model').set('content', this.store.filter('scroll', function(item) {
var searchTerm = that.get('searchCard');
var regExp = new RegExp(searchTerm, 'i');
return regExp.test(item.get('name'));
}));
}.observes('searchCard')
});
Which works great, but once I add a method that overrides arrangedContent to limit the returned items, it stops re-rendering.
Scrolls.IndexController = Ember.ArrayController.extend({
arrangedContent: Ember.computed('content', function() {
var count = 0;
return this.get('content').filter(function() {
count++;
return count <= 3;
});
}),
searchResult: function() {
var that = this;
this.get('model').set('content', this.store.filter('scroll', function(item) {
var searchTerm = that.get('searchCard');
var regExp = new RegExp(searchTerm, 'i');
return regExp.test(item.get('name'));
}));
}.observes('searchCard')
});
How can I get make what I'm doing to behave nicely with each other?
I see a few things here that jump out to me. First one being, in the context of a controller, content and model are the same thing so in the observer, when you do:
this.get('model').set('content'
You're setting a property of 'content' on the model when I think you actually intend to set the content directly on the controller, like this:
this.set('content',
I also kind of wonder whether you really need to override the content and arrangedContent properties (not sure what the calling code looks like). I suspect that might cause some bugs later. Instead, I wonder if you could set it up like this:
Scrolls.IndexController = Ember.ArrayController.extend({
firstThreeSearchResults: function() {
var count = 0;
return this.get('searchResults').filter(function() {
count++;
return count <= 3;
});
}.property('searchResults'),
searchResults: function() {
var searchTerm = this.get('searchCard');
return this.store.filter('scroll', function(item) {
var regExp = new RegExp(searchTerm, 'i');
return regExp.test(item.get('name'));
});
}.property('searchCard')
});
Final possible problem is the use of the filter function called on the store. According to the docs, this function: "returns a live RecordArray that remains up to date as new records are loaded into the store or created locally." The problem being, though the filter might update as new results are added, it might not cause the computed property that looks for the first three results to update. That is, the binding on that computed property might not fire. One way to get around this would be to do something like this:
Scrolls.IndexRoute = Ember.Route.extend({
model: function() {
return this.store.find();
}
});
Scrolls.IndexController = Ember.ArrayController.extend({
firstThreeSearchResults: function() {
var count = 0;
return this.get('searchResults').filter(function() {
count++;
return count <= 3;
});
}.property('searchResults'),
searchResults: function() {
var searchTerm = this.get('searchCard');
return this.get('content').filter(function(item) {
var regExp = new RegExp(searchTerm, 'i');
return regExp.test(item.get('name'));
});
}.property('searchCard', 'content.length')
});
I have a big problem with ember. If i navigate to somewhere, i get the error Cannot set property 'type' of null sometimes and the view will not be rendered. The Error is thrown in jQuery, not in Ember.
I have never set something.type in my Application. I dont know what the error could be. An Example of a Route + Controller + View (dont forget, its not happening every time.)
Routing: this.route("channels", {path: "/channels/:only"});
Route:
App.ChannelsRoute = Ember.Route.extend({
model: function(params) {
var controller = this.controllerFor("channels");
controller.set("only", params.only);
if (!controller.get("hasContent")) {
return controller.updateContent();
}
}
});
Controller:
App.ChannelsController = Ember.ArrayController.extend({
sortProperties: ["position"],
sortAscending: true,
hasContent: function() {
return this.get("content").length > 0;
}.property("#each"),
channels_category: function() {
return this.get("only");
}.property("only"),
filteredContent: function() {
var filterType = "group";
var filterID = 1;
switch(this.get("only")) {
case "primary": filterType = "group"; filterID = 1; break;
case "secondary": filterType = "group"; filterID = 2; break;
case "regional": filterType = "group"; filterID = 3; break;
case "hd": filterType = "hd"; filterID = true; break;
default: filterType = "group"; filterID = 1; break;
}
return this.filterProperty(filterType, filterID);
}.property("#each", "only"),
updateContent: function() {
return $.getJSON(getAPIUrl("channels.json")).then(function(data){
var channels = [];
$.each(data.channels, function() {
var channel = App.Channel.create({
id: this.id,
name: this.name,
group: this.group,
hd: this.hd,
position: this.position,
recordable: this.recordable
});
channels.pushObject(channel);
});
return channels;
});
}
});
Its happening in a lot of controllers. I hope somebody knows a solution.
Can you test this variation of your updateContent function? changed the channels var to the controllers model...
updateContent: function() {
return $.getJSON(getAPIUrl("channels.json")).then(function(data){
var channels = this.get('model');
$.each(data.channels, function() {
var channel = App.Channel.create({
id: this.id,
name: this.name,
group: this.group,
hd: this.hd,
position: this.position,
recordable: this.recordable
});
channels.pushObject(channel);
});
});
}
If it's happening in jQuery, it's occurring either in an AJAX call or some DOM manipulation you're doing with jQuery. Your best start will be to have a fiddle with your AJAX calls, and check any Handlebars helpers you're using (especially if you have any input helpers with types declared).