I have a view where there can be a large number of items for the user to scroll through and I'd like to implement infinite scrolling to enable progressive loading of the content.
It looks like some folks have done pagination but Google doesn't bring up anyone discussing how they've done infinite lists with Ember/Ember Data. Anyone already worked through this and have a blog post/example code to share?
I've implemented an infinite scroll mechanism at the GitHub Dashboard project, I'm currently developing. The feature is added in commit 68d1728.
The basic idea is to have a LoadMoreView which invokes the loadMore method on the controller every time the view is visible on the current viewport. I'm using the jQuery plugin inview for this. It allows you to register for an inview event, which is fired when the element of the specified selector is visible on screen and when it disappears.
The controller also has properties which indicate whether there are more items to load and if there are currently items fetched. These properties are called canLoadMore and isLoading.
The LoadMoreView basically looks like this:
App.LoadMoreView = Ember.View.extend({
templateName: 'loadMore',
didInsertElement: function() {
var view = this;
this.$().bind('inview', function(event, isInView, visiblePartX, visiblePartY) {
if (isInView) Ember.tryInvoke(view.get('controller'), 'loadMore');
});
}
});
where the loadMore template is defined as follows:
{{#if isLoading}}
fetching some more stuff <img width="10" src="img/ajax-loader.gif" >
{{else}}
{{#if canLoadMore}}
<a {{action "loadMore" target="controller" }}>click to load more items</a>
{{else}}
<i>no more items</i>
{{/if}}
{{/if}}
The controller which handles the fetching of more items is then implemented as follows. Note that in the loadMore method a query on the store is performed, which loads a specific page of of entries for a model.
App.EventsController = Ember.ArrayController.extend({
currentPage: 1,
canLoadMore: function() {
// can we load more entries? In this example only 10 pages are possible to fetch ...
return this.get('currentPage') < 10;
}.property('currentPage'),
loadMore: function() {
if (this.get('canLoadMore')) {
this.set('isLoading', true);
var page = this.incrementProperty('currentPage');
// findQuery triggers somehing like /events?page=6 and this
// will load more models of type App.Event into the store
this.get('store').findQuery(App.Event, { page: page });
} else {
this.set('isLoading', false);
}
}
});
The only thing left is to initially set the content of the controller to the result of a filter function, so the content is updated when new models are loaded into the store (which happens due to the findQuery method in the loadMore of the controller). Also, a query hash is added when the filter is invoked. This ensures that an initial query to the server is made.
App.eventsController = App.EventsController.create({
content: []
});
var events = App.store.filter(App.Event, { page: 1 }, function(data) {
// show all events; return false if a specific model - for example a specific
// type of event - shall not be included
return true;
});
Were you aware of the newly released Ember.ListView component?
https://github.com/emberjs/list-view
It was announced at the February San Francisco Ember Meetup. Here's a slidedeck from Erik Bryn, one of the Ember Core developers about using it:
http://talks.erikbryn.com/ember-list-view/
I'm writing an infinite pagination plugin for Ember based on #pangratz's work.
Please fire any issues on there if you have questions or improvements that you'd like.
I would recommend using Ember Infinity addon. It supports Ember 1.10 through to 2.0+. It's relatively easy to setup. You only need to modify your route and template.
Route (Product is example model):
import InfinityRoute from 'ember-infinity/mixins/route';
export default Ember.Route.extend(InfinityRoute, {
model() {
/* Load pages of the Product Model, starting from page 1, in groups of 12. */
return this.infinityModel('product', { perPage: 12, startingPage: 1 });
}
});
Template:
{{#each model as |product|}}
...
{{/each}}
{{infinity-loader infinityModel=model}}
When {{infinity-loader}} component becomes visible it sends an action to your route, so it knows to update model array with new (fetched) records.
First request will be sent to:
/products?per_page=12&page=1
So you also need to prepare your backend API to handle these query params. It's obviously customizable, take a look at Advanced Usage section of Readme.
Note:
Both using ListView (#commadelimited's answer) and views with ArrayController (#pangratz's answer) is deprecated/removed as of Ember 2.0 being stable version.
Related
Before anyone brings up components, I must state that I am aware that Ember is moving away from controllers and views completely and adopting the component structure. Right now, I am compelled to use controller/view in ember2.3 using the legacy-controller and legacy-view addons that have been provided here:
https://github.com/emberjs/ember-legacy-controllers
https://github.com/emberjs/ember-legacy-views
as part of the process to upgrade to Ember 2.3 (from 1.7).
Now, I have a route called recordTypes, which lists all recordTypes. in the legacy code, each recordType was then associated with an itemController 'recordType'. Like so:
{{#each result in searchResults itemController="recordType"}}
...
{{/each}}
Surprisingly, this legacy syntax for Ember did not render anything to the page, but the following one did:
{{#each searchResults itemController="recordType" as |result| }}
...
{{/each}}
The itemController recordType is a legacy Object Controller and the recordTypes controller itself is a legacy Array Controller.
Now, for each result I have a few actions that can be performed. For example, on clicking the result, the editResultName action was to be fired. This action, in the legacy code, was in the recordType controller. Therefore, clicking the item in the recordTypes page would then defer this action to the recordType controller, which would then happily handle the rest.
This is not being fired in ember2.3, even with the legacy controllers. What surprises me more is that this code can be found in ember-legacy-controller.js
export default {
name: 'ember-legacy-controllers',
initialize: function() {
/**
Adds support for ArrayController in the legacy {{each}} helper
*/
Ember._LegacyEachView.reopen({
_arrayController: computed(function() {
var itemController = this.getAttr('itemController');
var controller = get(this, 'container').lookupFactory('controller:array').create({
_isVirtual: true,
parentController: get(this, 'controller'),
itemController: itemController,
target: get(this, 'controller'),
_eachView: this,
content: this.getAttr('content')
});
return controller;
}),
_willUpdate(attrs) {
let itemController = this.getAttrFor(attrs, 'itemController');
if (itemController) {
let arrayController = get(this, '_arrayController');
set(arrayController, 'content', this.getAttrFor(attrs, 'content'));
}
}
});
}
};
Here, it does have a line that references the itemController. However, when this list of searchResults is rendered, and a result is clicked, the error I get is this:
Nothing handled the action 'editResultName'. If you did handle the action, this error can be caused by returning true from an action handler in a controller, causing the action to bubble.
The action is there for sure, but nothing in the itemController is being recognised. Unfortunately a lot of the legacy code I am updating has itemController loops and therefore it would be immensely helpful to be able to use itemController for the time being.
How can I use itemController like it used to be implemented?
Replacing an itemController.
Create a component from the contents inside the each helper. The itemController would become the js side of the component and the template code the template
From this:
{{#each result in searchResults itemController="recordType"}}
<span>result: {{result.title}}</span>
{{/each}}
To this:
{{#each searchResults as |result| }}
{{result-list-item result=result}}
{{/each}}
I want to accomplish the following feature:
A list of entries - when a single entry is clicked, the details of this entry should be displayed below the current entry.
It should be possible to access a single entry via a route, e.g. tasks/1
My approach:
I tried to solve this by including {{outlet}} within my #each loop but that did not quite work.
All tutorials an solutions I found are based on a List -> Details approach where only a single outlet is used.
I have no idea how I could solve this problem. Is there a better solution?
This is not possible through routing. You are better off using a component that expands, loads and displays the details upon clicking.
//list.hbs
{{#each task in tasks}}
{{#task-loader task=task}}
{{#task-loader-trigger}}
//list ui
{{/task-loader-trigger}}
{{#task-loader-content}}
//details ui
{{/task-loader-content}}
{{/task-loader}}
{{/each}}
//task-loader.js
export default Em.Component.extend({
isOpen: false
});
//task-loader-trigger.js
export default Em.Component.extend({
click: function() {
//toggle isOpen
}
}};
//task-loader-content.js
export default Em.Component.extend({
classNameBindings: ['parentView.isOpen:show:hide'],
didInsertElement: function() {
//send request and load data
}
});
If you do want to maintain state (the currently open task), use query params. Hope this helps :)
Hi I have a view call AutocompleteView its job is to put google places autocomplete on landing page.There is no map rendered on this page just a textbox.
what I am try to achieve is user should just use autocomplete textbox on landing page. On entering some place of choice. I will transition the user to a specific route where the same AutocompleteView will be rendered with map so tha user can change his choice of place.
When user chang the location on the page where map is rendered no transition is needed here
the approch I am trying is I check the parent view where
{{view AutocompleteView}}
has been rendered using this.get('ParentView") and based on the parent view
say if ParentView is Application(landing page) I will transition or else no transition will occur.
I have no good experienve of javascript mvc my doubt
Is it right to rely on ParentView for just transtions or is there a better way??
App.AutocompleteAddressView = Ember.View.extend({
tagName: 'input',
didInsertElement: function() {
var options = {
componentRestrictions: {country: "xx"}
};
var autocomplete = new google.maps.places.Autocomplete(this.$()[0], options);
console.log(this.get('parentView'));
}
});
It sounds like what you want is to handle the action of "entering text in the search box" differently based on which route is currently active -- the landing page or the results page.
The Ember way to do this is to trigger an action from your view using this.get('controller').send(#{actionName}). That action will bubble up from the view's controller to the current route. Then you can handle that action differently depending on which route is active, e.g.:
App.IndexRoute = Em.Route.extend({
...
actions: {
search: function() {
# transition to results page
}
}
});
App.ResultsRoute = Em.Route.extend({
...
actions: {
search: function() {
# update visible results
}
}
});
To make this easier, you might want to use a subclass of Ember.TextField for your AutocompleteAddressView. Then you can define an action property on your view that will automatically get triggered when the user types enter/return.
I'm using a render helper inside a template, which renders a searchbox with a typeahead.
Essentially (code removed for brevity):
script(type='text/x-handlebars', data-template-name='index')
{{render search}}
script(type='text/x-handlebars', data-template-name='search')
{{view App.TaggableInput valueBinding="searchText"}}
Which gives me a SearchController separated from the IndexController.
Inside App.TaggableInput I'm grabbing searchController to do some checking on the keyUp event:
App.TaggableInput = Ember.TextField.extend({
keyUp: function(e){
var controller = this.get('controller');
// Do stuff with the controller
}
});
On Ember RC7, I can access the controller inside theview as you'd expect with this.get('controller').get('searchText').
However in Ember 1.0.0 this.get('controller') returns the view, and whatever I do I can't get searchController.
I can't find any related info on the ember website regarding what's changed or what I'm supposed to do... for now I'm sticking with RC7.
Any ideas? I've spent hours on it this morning and can't figure it out. Thanks.
UPDATE: Fixed!
I swapped out this.get('controller') for this.get('targetObject') and it works as before. Had a peruse through a recent commit in ember source to find it...
Thanks for your suggestions guys!
I guess that in your code
App.TaggableInput = Ember.TextField.extend({
keyUp: function(e){
var controller = this.get('controller');
// Do stuff with the controller
}
});
this line
var controller = this.get('controller');
gets the controller associated to your (subview)
Try to use this line instead to access the route's controller:
var controller = this.get('parentView.controller');
Currently, the {{render}} helper takes 2 arguments, the first is the context, the second is the model.
I recommend using this method and following the naming convention for the model's controller rather than setting the controller explicitly.
You can find the docs here:
http://emberjs.com/guides/templates/rendering-with-helpers/#toc_the-code-render-code-helper
Accessing controllers from views was also being tracked in this discussion:
https://github.com/emberjs/ember.js/issues/1712#issuecomment-31183940
I think Ember has not changed its behaviour. I created a JSBin, where i managed to get the controller successfully.
What i did was creating a simple View and show it via {{render}} helper:
View:
App.FooView = Ember.TextField.extend({
didInsertElement : function(){
console.log(this.get("controller.constructor"));
console.log(this.get("context.constructor"));
}
});
Template:
{{render foo}}
And the first log statement showed an associated controller. Can you see any conceptual difference between my code and yours?
I'm new at using ember, but already familiar with it, basically following some tutorials here and there and reading the api docs. But tutorials don't go too deep into more complex topics.
These are the details: I already implemented a web page that shows a list of items. The following are the relevant code excerpts from different parts of the app.
// the data model, the view and the controller
App.Item = DS.Model.extend({
name: DS.attr('string')
});
App.ItemsController = Ember.ArrayController.extend();
App.ItemsView = Ember.View.extend({ templateName: 'items' })
// in the router's corresponding route
connectOutlets: function(router) {
router.get('applicationController').connectOutlet('items', App.Item.find())
}
// in the handlebars template
<ul class="items">
{{#each content}}
<li>{{name}}</li>
{{/each}}
</ul>
The data for this list is loaded remotely via ember-data (notice the App.Item.find() call in the route's connectOutlet method above) and a handlebar template displays it, and dynamically updates the list as the data changes. Up to here this is basic ember.
Now I want to have a text field at the top of the list, and when the user types in this text field, the list should be updated, by filtering and showing only the items with a name that matches what the user is typing. The actual definition of what a matching name is, is irrelevant to this question. It could be those names that contain the typed string, or that start with it.
I know my next step is to include a textfield view on top of the list in the handlebars template:
<div class="search-bar">
{{view Ember.TextField}}
</div>
<ul class="items">
{{#each content}}
<li>{{name}}</li>
{{/each}}
</ul>
So my questions at this point are the following:
How do I refer to this text field in javascript code so I can attach a listener to it to detect when it changes?
And more importantly, what do I need to do inside this event listener so the list gets filtered?
I would like to know how to achieve it filtering data loaded locally, but also how to do it by loading the filtering data remotely everytime the user types.
I actually need to implement something slightly more complex than this, but knowing how to do this will help.
You can have a computed property on your controller that filters the content based on a text field.
App.ItemsController = Ember.ArrayController.extend({
// ...
searchedContent: function() {
var regexp = new RegExp(this.get('search'));
return this.get('content').filter(function(item) {
return regexp.test(item.get('name'));
});
}.property('search', 'content.#each.name')
});
Then you just bind your text field to controller.search. Example: http://www.emberplay.com#/workspace/2356272909
To filter data remotely you should have ember data load more items every time search changes. This can be done by sending an event to the router every time there is a change.