I've been trying to implement lazy loading in emberjs by following this answer: https://stackoverflow.com/a/11925918/341358 . But I'm stuck when ember loads the initial dataset. For some reason the first ajax call keeps calling: /newsitems instead of only the first page: /newsitems?page=1. From then on, the loadmore functionality works great, returning me a limited data set for page 2, 3, 4, ...
So my question is: how do I make sure that only the items for the first page are loaded and not all of them at once?
Here's how my route looks like:
App.NewsitemsRoute = Ember.Route.extend({
model: function() {
return App.Newsitem.find();
}
});
Here's my controller:
App.NewsitemsController = Ember.ArrayController.extend({
currentPage: 1,
canLoadMore: function() {
return this.get('currentPage') < 10;
}.property('currentPage'),
loadMore: function() {
if (this.get('canLoadMore')) {
this.set('isLoading', true);
var page = this.incrementProperty('currentPage');
this.get('store').findQuery(App.Newsitem, {page:page});
}
else
{
this.set('isLoading', false);
}
}
});
Can you change your route to include a default page number of 1?
e.g.
App.NewsitemsRoute = Ember.Route.extend({
model: function() {
var controller = this.controllerFor('Newsitems');
return App.Newsitem.find({page: controller.get('currentPage')});
}
});
Edit: What if you get the page number from the controller?
Related
I need to get firstObject of my carousel and set it as active
this is how I am setting carousel property
JSBIN for code
App.ApplicationController = Ember.ArrayController.extend({
content: [],
carouselData: function () {
var categories = this.get('content');
var products = Em.A([]);
categories.forEach(function (category) {
category.get('items').then(function (data) {
data.forEach(function (product) {
products.addObject(product);
});
});
});
console.log(products);
console.log(products.get('firstObject'));
return products;
}.property('content')
});
Update
#ppcano Thanks for the explanation :). I got what you are asking me to do. return model only when hasMany has fulfilled. and then with computed property save them in carouselData property. but may be I am missing something in implementation the cdata.get('firstObject') returns a promise updated jsbin UPDATED JSBIN in App.Caroure
update 2
SOLVED enter link description here
Your problem is that the computed property does not work within async execution.
category.get('items').then(function (data)
The products variable is returned before any data can be pushed into products, because the items must be requested.
You could solve it when you ensure that items are loaded when the property is computed. You could do it in your route model as:
model: function(){
return this.store.find('facture').then(function(factures){
var productPromises = factures.getEach('items');
return Ember.RSVP.all(productPromises).then(function(products) {
return factures;
});
});
}
Then, you could define your CP as:
carouselData: function(){
var result = Em.A([]);
this.get('content').getEach('items').forEach(function(items){
result.pushObjects(items.toArray());
});
return result;
}.property('content.#each.items.#each')
Ember : 1.5.0-beta.2
Ember Data : 1.0.0-beta.7
I have the following router:
App.Router.map(function() {
this.resource('posts', function() {
this.route('new');
});
});
My PostsNewRoute creates a new record in the model hook:
App.PostsNewRoute = Ember.Route.extend({
model: function() {
return this.store.createRecord('post');
}
});
Since I don't want transient record to be visible, I filter them out in my PostsRoute:
App.PostsRoute = Ember.Route.extend({
model: function() {
this.store.find('post');
return this.store.filter('post', function(post) {
return !post.get('isNew');
});
}
});
This works as expected, but every transition to posts.new add a new record to the store, which is something I would like to avoid. So, instead of calling createRecord every time the model hook is called, I filter the store for an empty record and return this if there is one found:
App.PostsNewRoute = Ember.Route.extend({
model: function() {
var route = this;
return this.store.filter('post', function(post) {
return post.get('isNew');
}).then(function(result) {
return result.get('firstObject') || route.store.createRecord('post');
);
});
This gives me at the most one empty record.
My question: is there a better way to avoid my store being populated with (many) empty records ?
UPDATE:
Instead of filtering on the isNew attribute, I can probably use currentModel:
model: function() {
this.get('currentModel') || this.store.createRecord('post');
};
You can use this addon https://github.com/dockyard/ember-data-route to clean up when you leave a /new route. It hooks into the willTransition action hook that gets called on the route whenever a transition occurs.
The source code is a short read: https://github.com/dockyard/ember-data-route/blob/master/addon/mixins/data-route.js.
The alternative would be to not create a new record in the model hook, but according to a comment of yours it doesn't seem to be an option.
I'm running RC-3 and want to setup the content of an arraycontroller without the model hook. This is because I need to add some filtering and don't want to reload the content with every transition.
I found that this.get('content') is sometimes undefined. I'm not sure why this is. Here's the code:
App.StockRoute = Em.Route.extend({
setupController: function(controller) {
if (controller.get('content') === undefined) {
controller.set('content', App.Stock.find());
}
}
});
What is the equivalent code in the setupController for the model hook?
Update
I've included this as a fuller description.
I took the ember guide of the todo app, and built off that. Currently I'm building a screen to mangage/view stock levels. What I'm trying to do is have a screen on which I can toggle all/specials/outofstock items (as per the todo, each has its own route), but then on the screen I need to filter the list eg by name or by tag. To add a challenge, I display the number of items (all, on special and out of stock) on the screen all the time, based on the filter (think name or tag) but not on the toggle (think all/on special/ out of stock)
Since its essentially one screen, I've done the following in the route code
App.StockIndexRoute = Em.Route.extend({
model: function() {
return App.Stock.find();
},
setupController: function(controller) {
// if (controller.get('content') === undefined) {
// controller.set('content', App.Stock.find());
// }
// sync category filter from object outside controller (to match the 3 controllers)
if (controller.get('category') != App.StockFilter.get('category')) {
controller.set('category', App.StockFilter.get('category'));
controller.set('categoryFilter', App.StockFilter.get('category'));
}
// a hack so that I can have the relevant toggle filter in the controller
if (controller.toString().indexOf('StockIndexController') > 0) {
controller.set('toggleFilter', function(stock) { return true; });
}
}
});
App.StockSpecialsRoute = App.StockIndexRoute.extend({
setupController: function(controller) {
this._super(controller);
controller.set('toggleFilter', function(stock) {
if (stock.get('onSpecial')) { return true; }
});
}
});
App.StockOutofstockRoute = App.StockIndexRoute.extend({
setupController: function(controller) {
this._super(controller);
controller.set('toggleFilter', function(stock) {
if (stock.get('quantity') === 0) { return true; }
});
}
});
You'll see that the only difference in the routes is the definition of the toggle filter, which needs to be applied to the model (since stock is different to stock/special or to stock/outofstock)
I haven't yet figured out how to link one controller to multiple routes, so I have the following on the controller side
App.StockIndexController = Em.ArrayController.extend({
categoryFilter: undefined,
specialCount: function() {
return this.get('content').filterProperty('onSpecial', true).get('length');
}.property('#each.onSpecial'),
outofstockCount: function() {
return this.get('content').filterProperty('quantity', 0).get('length');
}.property('#each.quantity'),
totalCount: function() {
return this.get('content').get('length');
}.property('#each'),
// this is a content proxy which holds the items displayed. We need this, since the
// numbering calculated above is based on all filtered tiems before toggles are added
items: function() {
Em.debug("Updating items based on toggled state");
var items = this.get('content');
if (this.get('toggleFilter') !== undefined) {
items = this.get('content').filter(this.get('toggleFilter'));
}
return items;
}.property('toggleFilter', '#each'),
updateContent: function() {
Em.debug("Updating content based on category filter");
if (this.get('content').get('length') < 1) {
return;
}
//TODO add filter
this.set('content', content);
// wrap this in a then to make sure data is loaded
Em.debug("Got all categories, lets filter the items");
}.observes('categoryFilter'),
setCategoryFilter: function() {
this.set('categoryFilter', this.get('category'));
App.StockFilter.set('category', this.get('category'));
}
});
// notice both these controllers inherit the above controller exactly
App.StockSpecialsController = App.StockIndexController.extend({});
App.StockOutofstockController = App.StockIndexController.extend({});
There you have it. Its rather complicated, perhaps because I'm not exactly sure how to do this properly in ember. The fact that I have one url based toggle and a filter that works across those 3 routes is, I think, the part that makes this quite compicated.
Thoughts anybody?
Have you tried to seed your filter with some data?
App.Stock.filter { page: 1 }, (data) -> data
That should grab the materialized models from the store, and prevent making any more calls to the server.
Let's say that I want to create an ember app that displays a matrix and allows
you to click on a cell for more information. My routes look like this:
App.Router.map(function() {
this.resource("data", function() {
this.route("cell", {path: ':row/:col'});
});
});
App.DataRoute = Ember.Route.extend({
model: function(params) {
return App.DataModel.create({
/* information needed for AJAX query */
});
},
});
App.DataCellRoute = Ember.Route.extend({
model: function(params) {
return Ember.Object.create({
row: params.row,
col: params.col
});
},
serialize: function(model) {
return {row: model.row, col: model.col};
},
});
Furthermore, the matrix load is expensive (say, a slow AJAX request), so I
don't want to have to reload the data when I transition to the child route.
Also, assume that the data in the cells can't be serialized, so I can't just
pass the cell data in the URL.
Here's what the controllers looks like:
App.DataController = Ember.ArrayController.extend({
model: null,
content: Ember.A(),
loadData: function() {
self = this;
$.ajax({
/* ... */,
success: function(data) {
App.beginPropertyChanges();
self.clear();
data.forEach(function(row) {
self.pushObject(row);
});
App.endPropertyChanges();
},
}.observes('model'),
});
App.DataCellController = Ember.ObjectController.extend({
needs: 'data',
model: {row: 0, col: 0},
matrixBinding: 'controllers.data.content',
cell: function() {
var xy = this.get('model');
return this.get('matrix')[xy.row][xy.col];
}.property('matrix.#each', 'model'),
});
When you click on a cell, its view tells DataController to send an event that
transitons to data.cell with the appropriate row/column. I expected that
when I transition to the data.cell route, I should have access to
DataController's content. However, all I get is the default empty array.
What am I doing wrong?
EDIT:
To answer the question 'how is DataController.content set', I updated the
question to show a more accurate depicition of DataRoute and
DataController. Basically, DataController has a model that contains
information pertinent to the AJAX request. I have a function, loadData,
which observes the model and loads content.
I have been experimenting with using Ember with a JSON server but without without ember-data. My test app renders a directory of images from a small JSON structure (generated from a little Go server).
Can anyone explain to me why, if I uncomment the App.FileController in the code below, the corresponding File view fails to render?
window.App = Ember.Application.create();
App.Router.map(function() {
this.resource('files',function(){
this.resource('file',{path:':file_id'});
});
});
App.FilesRoute = Ember.Route.extend({
model: function() {
return App.File.findAll();
}
});
App.FileRoute = Ember.Route.extend({
setupController: function(controller, args) {
controller.set('model', App.File.find(args.id));
},
model: function(args) {
return App.File.find(args.file_id);
}
});
App.File = Ember.Object.extend({
urlPath: function(){
return "/pics/" + this.get('id');
}.property('id'),
});
If I uncomment this, things break:
// App.FileController = Ember.Controller.extend({
// });
(namely, the File sub-view no longer renders at all.)
App.File.reopenClass({
find: function(id){
file = App.File.create({'id':id});
return file;
},
findAll: function() {
return $.getJSON("http://localhost:8080/api/").then(
function(response) {
var files = [];
response.Files.forEach(function (filename) {
files.push(App.File.create({'id':filename}));
});
return files;
}
);
},
});
Also, is there something fundamental that I'm doing wrong here?
As noted by Finn MacCool and agmcleod I was trying to use the wrong type of controller. The correct lines should be:
App.FileController = Ember.ObjectController.extend({
});
Not that I need to explicitly set a FileController in this small example. However, should I go on to expand the code I will no doubt need one and will need to use the correct one.