Bind html select to router param - ember.js

I have a dashboard which shows some stats for a cause/charity that can be chosen from a dropdown. When the user changes a new cause/charity from the dropdown I want to transition the router so that the name of the cause/charity is included in the url e.g. /#dashboard/British Heart Foundation.
Here's my attempt, the controller observes the currently selected cause/charity and transitions if it changes. The problem is the transition is triggered when the current cause/charity is set after loading the route which causes an infinite loop. Now I know I could check the previous value of the cause and make sure it's not null before triggering a transition but my approach feels a little bit off. What would be the idiomatic way of binding controls to router params?
App.Router.map(function(){
this.resource('dashboard', { path: '/dashboard/:cause' });
});
App.DashboardRoute = Ember.Route.extend({
model: function(params){
return {
cause: params.cause,
causes: ['British Heart Foundation', 'Great Ormand Street Hospital']
}
}
});
App.DashboardController = Ember.ObjectController.extend({
changeCause : function(obj, keyName, value){
this.get("target").transitionTo("dashboard", {cause: this.get("cause")});
}.observes("cause")
});
<script type="text/x-handlebars" data-template-name="dashboard">
{{ view Ember.Select contentBinding="controller.causes" valueBinding="controller.cause" }}
<h1>{{ controller.cause }}</h1>
</script>

Related

Update ember component on model change

Using Ember 1.13
I have two nested resources, one of which renders a component based off the model returned by a dynamic route
something like
Router.map(function() {
this.resource('maps', function () {
this.resource('map', { path: '/:map_id' });
});
});
and a template for a map which renders a component
map.hbs
{{some-component model=model}}
{{#each maps as |map|}}
{{#link to 'map' map}}{{map.name}}{{/link-to}}
{{/each}}
when I first hit
/maps/1
the component renders
when I hit one of the links and go to
/maps/2
it appears as if the route never gets hit and the component never updates
is this a result of using link-to or is it true the route is not getting hit because just changing the model inside of a route does cause the same lifecyle hooks to go off?
What is the best way to force this component to rerender?
You're probably doing something wrong.
Here's a basic working example:
<h3>maps.index</h3>
<ul>
{{#each model as |item|}}
<li>
{{link-to item.name 'maps.map' item}}
</li>
{{/each}}
</ul>
<h3>maps.map</h3>
{{link-to "Back to index" 'maps.index'}}
<hr>
{{x-map map=model}}
<h4>components/x-map</h4>
<p>Id: {{map.id}}</p>
<p>name: {{map.name}}</p>
App.Router.map(function() {
this.route('maps', function () {
this.route('map', { path: '/:map_id' });
});
});
App.MapsIndexRoute = Ember.Route.extend({
model: function () {
return this.store.findAll('map');
}
});
App.MapsMapRoute = Ember.Route.extend({
model: function (params) {
return this.store.findRecord('map', params.mapId);
}
});
App.Map = DS.Model.extend({
name: DS.attr('string')
});
Demo: http://emberjs.jsbin.com/voquba/4/edit?html,js,output
Note that instead of passing the whole record into the child route:
{{link-to item.name 'maps.map' item}}
You can pass only its ID:
{{link-to item.name 'maps.map' item.id}}
This is useful when you know the ID but don't have the whole record at hand. Ember Data will look up the record of given ID in the store. If it's not there, it'll fetch it via the route's model hook.

Emberjs: Accessing parent route model which is a Promise (master-detail)

I've got master-detail page layout as on image. I access this page through #/masters/:master_id app url.
Routes a defined as follows:
App.Router.map(function() {
this.resource('masters', { path: '/masters' }, function() {
this.route('detail', { path: '/:master_id' });
});
});
App.MastersRoute = Ember.Route.extend({
model: function() {
return App.DataStore.getData('/api/masters'); //returns Promise!
},
setupController: function(controller, model) {
controller.set("content", model);
}
});
App.MastersDetailRoute = Ember.Route.extend({
model: function(params) {
return this.modelFor("masters").find(function(item) {
return item.get("id") == params.master_id;
});
}
});
Templates:
<script type="text/x-handlebars-template" data-template-name="masters">
<div id="masters-grid">
{{#each master in model}}
<div {{action "show" master}}>
{{master.name}}
</div>
{{/each}}
</div>
<div id="detail">
{{outlet}}
</div>
</script>
<script type="text/x-handlebars-template" data-template-name="masters/detail">
{{model.name}} <br />
{{model.age}} <br />
{{model.address}} <br />
</script>
When clicking through masters in the grid I want to show their details in Detail outlet and I do not want to reload all masters from API when changing the master selection.
I have a problem with MastersDetailRoute's model, because this.modelFor("masters") returns undefined. I think, it is caused by returning Promise in model hook. Any idea or workaround how to access one item from Masters model or controller in "child route" model hook?
I see a few things here.
when defining routes that have the same url as the route name theres no need to specify the path
the detail route should also be a resource as it is a route backed by a model
In the Masters route returning a promise is correct and supported natively by ember. The route wont be resolved until the promise is.
setup controller isn't required
its usually best to do the required api call to fetch the individual record in the detail route. This will only be used when loading the page for the first time (if f5 ing or coming from a bookmark)
in your masters template you can use id instead of typing data-template-name or better still look into use ember-cli/brocolli or grunt to precompile your templates
to prevent ember refetching your model when selecting a row use the handlebars helper link-to
{{#link-to 'masterDetail' master}}
{{master.name}}
{{/link-to}}
just to clarify, using link-to in this way passes the object specified in the second parameter as the model to the specified route (first parameter). In your case master will now be set as the model to the master detail route.
in masters detail theres no need to type "model" the default context (i.e. the value of "this") in your template is the controller, then if the property is not found on the controller it looks for it in the model.
Hope this helps

Ember JS Deep Linking

I have an Ember JS 1.5.1 app with ember-data 1.0.8 beta. There are TWO simple compiled templates the relevant parts are:
index
<div class="container-fluid">
<div class="col-md-2 sidebar">
<ul class="nav nav-sidebar">
{{#each model}}
<li>
{{#link-to 'activities' this}}{{name}}{{/link-to}}
</li>
{{/each}}
</ul>
</div>
<div class="col-md-10 col-md-offset-2">
{{outlet}}
</div>
</div>
activities
<div>
<ul>
{{#each model.activities}}
<div class="row">
<p>activity {{id}} is {{name}}</p>
</div>
{{/each}}
</ul>
</div>
The application is also simple, reduced to a few bits of fixture data and some route functions:
window.App = Ember.Application.create();
App.ApplicationAdapter = DS.FixtureAdapter;
App.Router.map( function(){
this.resource('index', {path: '/'}, function(){
this.resource('activities', { path:':name'}, function(){
this.resource('activity');
});
});
});
App.IndexRoute = Ember.Route.extend({
model: function(){
return this.store.find('role');
}
});
App.ActivitiesRoute = Ember.Route.extend({
model: function(params){
var roles = this.modelFor('index');
return roles.findBy('name', params.name).get('activites');
}
});
App.Role = DS.Model.extend({
name: DS.attr('string'),
activities: DS.hasMany('activity', {async:true} )
});
App.Activity = DS.Model.extend({
name: DS.attr('string')
});
App.Role.FIXTURES = [{
id: 1,
name: 'Management',
activities: [1]
},{
id: 2,
name: 'Analysis',
activities: [1,2]
},{
id: 3,
name: 'Development',
activities: [2]
}]
App.Activity.FIXTURES = [{
id: 1,
name: 'talking'
},{
id: 2,
name: 'doing'
}];
What I get when I navigate to localhost is a simple list of the three roles on the left hand side of the screen and nothing on the right hand side. (as expected)
When I then select a link (such as 'Analysis') the outlet on the right hand side fills with the expected list of two activity names "talking" and "doing".
LHS list RHS pane
========== ========
Management talking
Analysis doing
Development
So far so good.
I noticed that when I hovered over the 'Analysis' link the browser shows the url below as expected
localhost:/#/Analysis
However when I cut and paste this url into the browser address bar directly I only get the left hand side list of links and nothing in the main window. The list of "talking" and "doing" does no appear. There are no errors shown in the browser and ember does not raise and exceptions.
How do you get this simple nested route to refresh all the contents when you directly deep link rather than having to navigate from the root all the time?
When you use link-to and pass it the model, it will skip the model hook supplying the model from the link-to to the route. If you refresh the page, it will hit each route down the tree until it's fetched the models for each resource/route necessary to fulfill the request. So if we look at your routes one at a time it will do this:
Hit the application route, fetch its model if it exists (application route is the root of every Ember app).
Hit your index route, where it will return App.Role.find()
Hit your activites route, where it will return App.Activity.find()
Number 3 is where you real issue lies. Regardless of whether or not that part of the url says Analysis, Management, or Development you will already return App.Activity.find(). You've defined the dynamic slug :name, ember will parse the appropriate part of the url, and pass that part is as an object, in the case of Analysis Ember will pass in { name: 'Analysis' } to your model hook. You will want to take advantage of this, to return the correct model.
App.ActivitiesRoute = Ember.Route.extend({
model: function(params){
var roles = this.modelFor('index');
return roles.findBy('name', params.name);
}
});
Additionally you are using a fairly old version of Ember Data. Here's a small example of how Ember Data should be used with newer versions: http://emberjs.jsbin.com/OxIDiVU/617/edit
As you can see, you no longer declare the store. Additionally you may run into trouble with what would be considered async properties, and might want to read https://github.com/emberjs/data/blob/master/TRANSITION.md

rendering one item from a list in Ember

In my Ember app, I get a list of all the restaurants using an ajax call copied from Discourse co-founder's blog post http://eviltrout.com/2013/02/27/adding-to-discourse-part-1.html
App.Restaurant.reopenClass({
findAll: function() {
return $.getJSON("restaurants").then(
function(response) {
var links = Em.A();
response.restaurants.map(function (attrs) {
links.pushObject(App.Restaurant.create(attrs));
});
return links;
}
);
},
I have a Restaurants route set up which calls the findAll shown above and renders it into the application template
App.RestaurantsRoute = Ember.Route.extend({
model: function(params) {
return(App.Restaurant.findAll(params));
},
renderTemplate: function() {
this.render('restaurants', {into: 'application'});
}
});
The restaurants are displayed as a restaurants template like this with a link to each individual restaurant. I've also included the restaurant template
<script type="text/x-handlebars" id="restaurants">
<div class='span4'>
{{#each item in model}}
<li> {{#link-to 'restaurant' item}}
{{ item.name }}
{{/link-to }}</li>
{{/each}}
</ul>
</div>
<div class="span4 offset4">
{{ outlet}}
</div>
</script>
In the Ember router, I have a parent/child route set up like this
this.resource("restaurants", function(){
this.resource("restaurant", { path: ':restaurant_id'});
});
Therefore, I'm hoping that when I click on the link to a particular restaurant in the restaurants list, it'll insert this restaurant template into the outlet defined in the restaurantS (plural) template
<script type="text/x-handlebars" id="restaurant">
this text is getting rendered
{{ item }} //item nor item.name are getting rendered
</script>
This restaurant template is getting rendered, however, the data for the item is not getting displayed.
When I click {{#link-to 'restaurant' item}} in the list, item represents that restaurant.
In this setup, does Ember need to make another ajax call to retrieve that particular item, even though it's already been loaded from the findAll call?
In the event that I do need to query for the individual restaurant (again) I created a new route for the individual restaurant
App.RestaurantRoute = Ember.Route.extend({
model: function(params) {
console.log(params);
console.log('resto');
return App.Restaurant.findItem(params);
}
});
and a findItem on the Restaurant model
App.Restaurant.reopenClass({
findItem: function(){
console.log('is this getting called? No...');
return 'blah'
}
but none of those console.logs are getting called.
In the Ember starter video https://www.youtube.com/watch?v=1QHrlFlaXdI, when Tom Dale clicks on a blog post from the list, the post appears in the template defined for it without him having to do anything more than set up the routes (as I did) and the {{outlet}} within the posts template to receive the post.
Can you see why the same is not working for me in this situation?
When you navigate to the restaurant route, the item will be the model to this route.
So in your template, if you try
<script type="text/x-handlebars" id="restaurant">
this text is getting rendered
{{ model.name }}
</script>
You'll be able to see the name of the restaurant
And also the model hook is not called, and the further console.logs are not working,
because
Note: A route with a dynamic segment will only have its model hook called when it is entered via the URL. If the route is entered through a transition (e.g. when using the link-to Handlebars helper), then a model context is already provided and the hook is not executed. Routes without dynamic segments will always execute the model hook.
Hope everything will be clear now.

Ember.js - filtered model change does not trigger a change in rendered template

I have an application that prints a list of reservations. There are 2 routes:
reservations.allReservations
reservations.newReservations
The Problem:
The allReservations route works fine. If reservations are deleted or added, it is reflected automatically on the page. However, the newReservations route does not get refreshed on changes - however if I reload the page the changes are there.
The setup:
The main difference between the two is the routes model field. For the allReservation route it is:
App.Reservations.all();
for the newReservation route it is:
App.Reservations.all().filterProperty('isNew',true);
The application models and data store is setup as in the Ember.js version of the TodoMVC app.
Route:
App.ReservationsNewReservationsRoute = Em.Route.extend({
model: function(){
return App.Reservation.all().filterProperty('isNew', true);
},
renderTemplate: function(){
this.render('reservationList');
},
});
Controller:
App.ReservationsNewReservationsController = Ember.ArrayController.extend({
isEmpty: function() {
return this.get( 'length' ) == 0;
}.property( '#each.length', '#each.isNew'),
});
Template reservationList.hbs:
{{#if isEmpty}}
<li>
<div class="text-center" style="height: 40px; margin: auto 0;">
<label>No Reservations!</label>
</div>
</li>
{{/if}}
{{#each controller}}
... print reservations ...
{{/each}}
In order to use a filtered array, I think you must use
model: function(){
return App.Reservations.filter(function(reservation){
return reservation.get('isNew') === true;
});
});
which returns a live FilteredRecordArray, updated when there is a new reservation loaded in the store.
When you use filterProperty(), you loose the FilteredRecordArray, and then the resulted array is not live.
Since you're not using ember-data, you have to manually maintain the filter. I think there could be to way of doing this.
The simplest would be to add a computed property on the controller, which would return the filtered content, and populating the model with App.reservations.all()
Obviously in the template, you will use {{#each controller.filteredContent}}
Something like:
App.ReservationsNewReservationsController = Ember.ArrayController.extend({
isEmpty: function() {
return this.get( 'filtered.length' ) == 0;
}.property('filtered.length'),
//will be evaluated each time a reservation is added/removed,
//or each time a reservation's isNew property changed.
filteredContent: function(){
return this.get('content').filterProperty('isNew', true);
}.property('#each.isNew')
});
The other solution could be to add the filter method on the Reservation model, and consistently in the store.