EmberJS: multiple outlets within #each loop - ember.js

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 :)

Related

How do I set a class on a parent element when a route is the first one loaded?

I have an Ember demo app that works fine if the first route loaded is 'index', 'list' or 'list/index', but not if the first route loaded is 'list/show'. Code is at https://github.com/DougReeder/beta-list , demo is running at https://ember-demo.surge.sh To see the problem, set your window narrower than 640px and surf to https://ember-demo.surge.sh/list/5 You'll see the list panel, rather than the detail panel.
The underlying problem is that, when the route is 'list/show', the divs with class 'panelList' and 'panelDetail' should also have the class 'right'.
I can't set this in the template, because panelList and panelDetail are created by the parent 'list' template. If I move panelList and panelDetail to the child templates 'list/index' and 'list/show', then the list gets re-rendered when going from 'list/index' to 'list/show' which would be a terrible performance hit.
Currently, I use the 'didTransition' action to toggle the class 'right'. This is called both then transitioning from 'list/index' to 'list/show', and when 'list/show' is the initial route. Unfortunately, if 'list/show' is the first route, none of the DOM elements exist when 'didTransition' is called.
I can envision two routes to a solution, but don't know how to implement either:
Toggle the class 'right' on some action which happens after DOM elements exist.
Insert conditional code in the 'list' template, which sets the class 'right' on 'panelList' and 'panelDetail' if the actual route is 'list/show'.
Suggestions?
Answer current as of Ember v2.12.0
You can use the link-to helper to render elements other than links, with styles that change based on the route. Utilizing the activeClass, current-when, and tagName properties, you can basically have that element be styled however you want depending on which route you are on. For example, to render your panelList div:
{{#link-to tagName='div' classNames='panelList' activeClass='right' current-when='list/show'}}
More markup
{{/link-to}}
I love a trick with empty component. In didInsertElement and willDestroyElement hooks you can add and remove a css class from parent element or (I like it better) body. Here is a code:
import Ember from 'ember';
export default Ember.Component.extend({
bodyClass: '',
didInsertElement() {
const bodyClass = this.get('bodyClass');
if (bodyClass) {
Ember.$('body').addClass(bodyClass);
}
},
willDestroyElement() {
const bodyClass = this.get('bodyClass');
if (bodyClass) {
Ember.$('body').removeClass(bodyClass);
}
}
});
I use it in template (in my example it's a template of player route) like this
{{body-class bodyClass='player-page-active'}}
To apply classes to parent element, you can use this.$().parent(), but using body is more reliable. Note that this component will create an empty div, but it shouldn't be a problem (can be in rare cases, fix it with classNames and css if needed).
Sukima suggested looking at currentRouteName, and I thus found hschillig's solution, which I simplified for my case. In the controller, I created an isShow function:
export default Ember.Controller.extend({
routing: Ember.inject.service('-routing'),
isShow: function() {
var currentRouteName = this.get('routing').get('currentRouteName');
return currentRouteName === 'list.show';
}.property('routing.currentRouteName'),
In the template, I now use the if helper:
<div class="panelList {{if isShow 'right'}}">
RustyToms's answer eliminates the need for adding a function to the Controller, at the expense of being less semantic.

link-to works in index.hbs but not in application.hbs

When I do a {{#link-to "cars/category" "mini"}} from index.hbs, everything works fine. I transition to cars/category -> templates/cars/category.hbs.
However, when I do the same from application.hbs (for navigation), I transition to an empty page and then automatically transition to the parent route cars.index -> templates/cars/index.hbs.
There is probably some logic to this. How can I transition to this route from a link click in application.hbs?
(a hard link <a href="/cars/mini" works fine, but I'll lose the state of the application.)
routes/cars/category.js model:
model(params) {
return this.store.findRecord('cars/category', params.category_id, {backgroundReload: false});
}
In route.js:
this.route('cars', () => {
this.route('cars/category', { path: '/cars/:category_id' });
});
For Ember 2.11
Have you try to change the route path to dot notation cars.category
A sample nested routes,
Router.map(function() {
this.route('photos', function(){
this.route('photo', { path: '/:photo_id' }, function(){
this.route('comments');
this.route('comment', { path: '/comments/:comment_id' });
});
});
});
A proper link-to helper link with multiple segements,
{{#link-to 'photo.comment' 5 primaryComment}}
Main Comment for the Next Photo
{{/link-to}}
You can read more at,
https://guides.emberjs.com/v2.5.0/templates/links/#toc_example-for-multiple-segments
For the below route.js
this.route('cars', function(){
this.route('category', { path: '/:category_id' });
});
You can do {{#link-to "cars.category" "mini"}}, this will transition to /cars/mini url.
You don't need to have cars/category. because it's already nested inside cars route.
Created Sample Twiddle .
For better understanding routing see AlexSpeller ember-diagonal
Someone suggested that since I transition to the desired page and then immediately transition to its parent, there must have been some other transition queued.
On further inspection, the link-to was indeed inside a drop down menu of a list-item which itself was a link-to too.
The solution was to add bubbles=false to the inner link-to.
The other answers here doubt the used routes. However, they are fine and setup like this for a reason. E.g. multiple routes with a subroute called category cannot all be in the root. However, it was my fault for not disclosing the exact code which put people on the wrong track, for they would probably have noticed the actual problem immediately.
Next time I will be more verbose in my code. I apologize, and thanks for thinking with me.

Having issues adding components within a ContainerView dynamically

I have a container view in application level and I wanted to add some components to this container view via some action. The way I do this is :
1) I push some objects to the application controller
2) ContainerView is within the context of application controller, so I observe the length of the array holding the objects mentioned in step 1
3) and then I create the components and append them to the container view
One weird thing I noticed was initially the observer defined in the container view was not triggering, but then I added initialize hook where I log the controller, and then the observer started working for me. This tells me maybe I am not doing something right. Here is my container view:
App.MyContainerView = Ember.ContainerView.extend({
initialize: function(){
//IF YOU WERE TO COMMENT THIS CONSOLE.LOG THEN THE OBSERVER NEVER GETS HIT WHEN YOU CLICK ON ADD BUTTON. MAYBE CONTROLLER IS NOT SET UP AND IT NEEDS SOME TIME???
console.log(this.get('controller').toString());
}.on('init'),
appendMyComponent: function(){
console.log("inside the observer");
this.get('controller.myComponents').forEach(function(comp){
console.log(this.toString());
this.pushOject(App.MyTestComponent.create({}));
}, this);
}.observes('controller.myComponents.length')
});
Do I have to use named outlet and then render the container view within the outlet? Not sure what I am doing wrong here.
Here is a jsbin: http://emberjs.jsbin.com/xamoxese/2/
Your feedback is much appreciated. Thanks good ppl of stackoverflow.
USING :
Ember : 1.5.0
Handlebars : 1.3.0
jQuery : 1.10.2
Allright some more updates, initially the solution provided by #bmeyers was not working for me although it was working in jsbin. So in my real app mu application.hbs looked like this:
{{outlet navbar}}
{{outlet}}
{{outlet modal}}
{{view App.MyContainerView}}
but then I re-ordered it as :
{{view App.MyContainerView}}
{{outlet navbar}}
{{outlet}}
{{outlet modal}}
and then the solution worked. Now only if I could get rid of that console.log thing :)
Here is updated jsbin that I have added on top of #bmeyers solution http://emberjs.jsbin.com/xamoxese/10/
I have fixed this for you and shown an example here: http://emberjs.jsbin.com/xamoxese/3/edit?html,css,js,output
I didn't fix your issue where you are adding the same components over and over again and never clearing the childView array, but maybe that was your intention.
Basically you do not push to the container view, you need to push to the childViews property.
Hope this helped!
UPDATE FIXED
http://emberjs.jsbin.com/xamoxese/6/edit
Ok so looks like you just needed a reference outside your foreach function and that for some reason allowed using it the way it is documented
UPDATE 2:: Change the containerView code to....
App.MyContainerView = Ember.ContainerView.extend({
init: function() {
this._super();
this.appendMyComponent();
},
appendMyComponent: function(){
this.get('controller.myComponents').forEach(function(comp){
this.pushObject(App.MyTestComponent.create({}));
}, this);
}.observes('controller.myComponents.length')
});
This just feels a bit cleaner than creating an empty view that you will never use. Picky I know but it was bugging me.
Finally figured out how to get rid of that console.log thing. So I actually have 2 solutions for this:
1) pass the controller to the container view
{{view 'myContainer' controller=this}}
2) set an empty view to the ContainerView
App.MyContainerView = Ember.ContainerView.extend({
childViews: [Ember.View.create()],
appendMyComponent: function(){
this.get('controller.myComponents').forEach(function(comp){
this.pushOject(App.MyTestComponent.create({}));
}, this);
}.observes('controller.myComponents.length')
});
Actually the best solution is as #bmeyers suggested, is to have init function for container view like this:
init: function() {
this._super();
/*
THIS IS THE OBSERVER METHOD THAT WE HAVE IN THE VIEW, THIS CALL DOES NOT EVEN
NEED TO ADD A CHILD VIEW, BUT STILL NEEDS TO BE CALLED - STRANGE I KNOW BUT WHY
I AM NOT SURE. ALSO FACED ISSUE DURING TEST FOR ABOVE 2 APPROACHES, THIS ONE WORKED WELL
*/
this.appendMyComponent();
}
Here is the working jsbin: http://jsbin.com/xamoxese/13/

Populating Ember.Select from a dependent controller using EmberData lazy-loading

I have spent about 10 days on a simple problem I could have solved in about 10 minutes with Dojo. I hope I am missing something simple - I'm a noob who would like to use Ember.
I am simply trying to populate the content of an Ember.Select with data from another Controller using EmberData. Consider the case: being able to select a FoodGroup for a Raw Ingredient.
It seemed clear to use a 'needs' dependency between RawIngredientController and FoodGroupsController, and bind on the content.
This is the closest I have gotten to success and it does not look sufficient to me - I will explain.
<script type="text/x-handlebars" data-template-name="rawIngredients">
{{#each item in controller itemController="rawIngredient" }}
{{! view Ember.Select
contentBinding="controllers.foodGroups.content"
optionValuePath="content.id"
optionLabelPath="content.name"
prompt="select Food Group" }}
{{/each}}
</script>
Cook.RawIngredientController = Ember.ObjectController.extend({
isEditing: false,
needs: ['foodGroups'],
...
});
Cook.RawIngredient = DS.Model.extend({
name: DS.attr('string'),
nameKey: DS.attr('string'),
foodGroup: DS.belongsTo('Cook.FoodGroup')
});
Cook.FoodGroupsRoute = Ember.Route.extend({
setupController: function(controller) {
controller.set('content', Cook.FoodGroup.all()); // have also tried find()
}
});
Cook.FoodGroup = DS.Model.extend({
name: DS.attr('string'),
nameKey: DS.attr('string')
});
This actually renders all RawIngredients with no errors, but the Selects are empty because the FoodGroups store is empty. I can navigate to the FoodGroups screen which loads the store, come back to RawIngredients, and the selects are populated with FoodGroups.
I've seen a number of posts on this issue but none sufficiently address the issue when EmberData is involved. There are plenty of post-loaded examples, like this clever one from pangratz http://jsfiddle.net/pangratz/hcxrJ/ - but I haven't found any using EmberData to lazy-load the content.
This post is pretty close
Ember.js: Ember.Select valueBinding and lazy loading
and contains a workaround using an observer, which I couldn't get to work. I ended up binding to a binding which never invoked the actual loading of the FoodGroup data. Here's an example, maybe not the best, of one such attempt (of probably 20+):
Cook.FoodGroupsController = Ember.ArrayController.extend({
...
// NOTE: This seems redundant
values: function() {
return this.get('content');
}.property(),
...
});
Cook.RawIngredientController = Ember.ObjectController.extend({
...
// this should be the instance; also tried the object name FoodGroupsController....also tried using needs + 'controllers.foodGroups.values' in the binding
allFoodGroups: Ember.Binding.oneWay('Cook.foodGroupsController.values'), '
/*
// ... or rather ....??
allFoodGroups: function() {
this.get('foodGroupsBinding');
},
foodGroupsBinding: "Cook.foodGroupsController.values",
*/
...
});
{{view Ember.Select
contentBinding="allFoodGroups"
optionValuePath="content.id"
optionLabelPath="content.name"
prompt="select Food Group" }}
but it errors saying 'allFoodGroups' is providing a Binding not an Ember.Array. I think I am lost in a sea of swirling naming conventions. I could show my other attempts at this, but all had errors of undefined content.
Ugh. So backing up a bit... at least using my original solution and pre-loading the FoodGroups store should provide a workaround, however I cannot seem to get EmberData to go out and load the data, programmatically. I tried an init() function in the RawIngredientController like
init: function() {
var item = this.get('controllers.foodGroups.content.firstObject');
},
but I haven't found the right combination there either. And even if I do, it seems like the wrong approach because this will set up a reference for each RawIngredient rather than use a single reference from RawIngredients (or FoodGroups?) - but that seems like a different topic.
This is my attempt at a fiddle describing the problem http://jsfiddle.net/jockor/Xphhg/13/
Has anyone figured out an efficient, effective way to load and use stores defined in other controllers, using EmberData to lazy-load the associated content?
Your basic problem seems to be that you're not accessing the foodGroups route, so its setupController() never gets executed, thus the content of the select controller never gets set.
In your example, when adding a link to the route in question and clicking it, the route gets initialized and the bindings work.
I tried to update your JSFiddle, but it is linking to the "latest" version of ember-data from Github which has not been updated in a while (you're supposed to build it yourself until they make an official release), and it's also using an old version of Ember, so I was getting some weird errors.
So here is a version with the latest Ember and Ember-data: http://jsfiddle.net/tPsp5/
Notice what happens when you click the Countries link. I left behind some debugger; statements that may help you understand what gets invoked when.
As a design note, your "parent" controller should probably not depend on "child" controllers.

infinite scroll with ember.js (lazy loading)

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.