Ember JS Deep Linking - ember.js

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

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.

Ember - #each pass the instance of the model

For a small webapp I'm trying to do the following:
I have a list of objects (achievement-model)that's being served through a json api
Router
export default Ember.Route.extend({
model:function(){
return this.store.find('achievement');
});
});
Model
export default DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
});
Template
{{#each a in model}}
<div>
<h4>{{a.name}}</h4>
<p>{{a.description}}</p>
<button {{action 'addThis'}}/>
</div>
{{/each}}
The setup of the app is that there is a list of achievements. I want one list of achievements in a database. Every user that logs in can add with the button his own achievements to his profile. If a user logs in he should see the list of all the achievements but the one he already added to his profile should have a green background color and the button removed. I know this can be done with if-statements etc.
The problem however is, how do i pass the specific model to the controller so i can log this to the userprofile? I tried the following:
<button {{action 'addThis' a}}/>
and then in the controller
actions:
addThis: function(obj){
console.log(obj);
});
which logs the object, but somehow I can't acces it to get let's say the name or id to copy it to the user-profile.
I also don't know if this is the best approach for what I'm trying to achieve?
Edit
I think this has something to do with promises. I can see the data is logged in the above console.log. I just don't know how to target it. it's wrapped in _data. I tried the afterModel to wait untill everything's loaded, but that doesn't seem to work.
What you could is to use an ItemController, e.g. which handles each item in the ArrayController,
e.g.
{{#each a in model itemController="achievement"}}
<div>
<h4>{{a.name}}</h4>
<p>{{a.description</p>
<button {{action 'addThis'}}/>
</div>
{{/each}}
Since the itemController is "achievement", by naming convention, the controller becomes
App.AchievementController = Ember.ObjectController.extend({
init: function() {
var name = this.get('name');
var description = this.get('description');
}
});

ember-data: Inheritance in Parent-Child resources

Let's say this is my router setup -
router.js :
App.Router.map(function(){
this.resource('photos', function(){
this.resource('photo', {path: '/:photo_id'}, function(){
//this.route('edit');
});
});
});
photo.js:
App.Photo = DS.Model.extend({
path: DS.attr('string'),
resolution: DS.attr('string'),
author: DS.belongsTo('user'),
dateTaken: DS.attr('date'),
aperture: DS.attr('string'),
focalLength: DS.attr('string'),
.
.
.
exposure: DS.attr('string')
});
photosRoute.js:
App.PhotosRoute = Ember.Route.extend({
model: function(){
return this.store.find('photo');
}
});
photos.hbs:
<div class="container">
<div class="row">
{{#each photo in controller}}
<div class="col-md-4">
{{#link-to 'photo' photo}}{{photo.path}}{{/link-to}} <br />
By: {{photo.author}} <br />
</div>
{{/each}}
</div>
</div>
{{outlet}}
As seen above, I am only using {{photo.path}} and {{photo.author}} in the photos.hbs template to show the list of all photos. However, in this setup a call to /#/photos would fetch all the bunch of fields for every photo from my django REST server - which I am not interested. Is there a way to fetch just a few fields from the photo model for /#/photos and the complete photo model only when I click on individual photo i.e. /#/photos/photo_id
I have a tried a couple of things:
Created a new child resource called 'photoDetail' that extends the original 'photo'. Ideally this should replace the singular 'photo'.
From what I gather {async: true} property holds only for async fetching btween models that have relationships setup between them - but not for individual fields in the photo model like: 'exposure', 'focalLength'.
Any help would be greatly appreciated.
No, there's no partially loaded models in Ember-Data. (In fact, I don't think any of the big Ember-Data alternatives have that feature). For the most part your models should be small enough where loading all of the records won't really matter. By the time you enable GZIP, you probably won't even notice a difference. If you have a special use case where bandwidth is extremely limited, you'll probably just want to write your own persistence library. (You could probably also modify Ember-Data or abuse some of its features to accomplish the same task, but I'd recommend against it.)

I get empty page when trying to show a list of child objects filtered by parent id. Using linkTo. Only works when refresh browser

I am getting to know Ember JS. I am trying to have a list of Accounts, and beside each account I want a link to navigate to a list of Transactions of that account.
When I load the first page requesting /accounts , I successfully get my list of accounts. But when I click
{{#linkTo transactions this}}see transactions{{/linkTo}}
the browser URl goes to /#/transactions/1, but I get an empty page. If I load this URL directly in the browser, everything works properly. But going from accounts list and clicking in one account, I get nothing.
The Accounts template is as follows:
<ul id="conta-list">
{{#each controller}}
<li>
Account: {{name}} {{#linkTo transactions this}}see transactions{{/linkTo}}
</li>
{{/each}}
</ul>
The Transactions template is:
<ul>
{{#each controller}}
<li>
{{description}} = ${{value}}
</li>
{{/each}}
</ul>
Application code:
window.App = Ember.Application.create({
LOG_TRANSITIONS: true
});
App.Router.map(function () {
this.route('accounts');
this.route('transactions', { path: '/transactions/:account_id' });
});
App.AccountsRoute = Ember.Route.extend({
model: function () {
return App.Account.find();
}
});
App.TransactionsRoute = Ember.Route.extend({
model: function (params) {
return App.Transaction.find({ account_id: params.account_id });
}
});
App.Store = DS.Store.extend({
adapter: 'DS.RESTAdapter'
});
App.Account = DS.Model.extend({
name: DS.attr('string')
});
App.Transaction = DS.Model.extend({
description: DS.attr('string'),
value: DS.attr('number'),
account: DS.belongsTo('App.Account')
});
I get "Object [object Object] has no method 'addArrayObserver' " in console.
The route /transactions/:account_id only hits the server if I reload the browser.
I get "Object [object Object] has no method 'addArrayObserver' " in console.
This is because the linkTo helper is passing an account to the transactions route, but your template is expecting the transactions model to be an array of transactions.
If I load this URL directly in the browser, everything works properly. But going from accounts list and clicking in one account, I get nothing.
So when you visit via url the route's model hook is called, App.Transaction.find makes a request to the server and everything works. By design none of this happens when following a link - instead you pass context arguments to the linkTo helper and those arguments are used to set the route's model. So in this case ember router does not call the route's model hook.
The ember way to make this work is by modeling transactions as a nested resource. So your route's would look like this:
App.Router.map(function () {
this.route('accounts');
this.resource('account', { path: '/account/:account_id' }, function() {
this.route('transactions');
});
});
With this approach you can use the auto-generated Account route but will need to define an AccountTransactions route.
App.AccountTransactionsRoute = Ember.Route.extend({
model: function (params) {
return this.modelFor("account").get('transactions');
}
});

Bind html select to router param

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>