Ember.js - Getting routes and models working together - javascript

i'm learning Ember.js but i'm struggling to figure out why my routes aren't working properly.
Here are the relevant parts of my app.js:
// Routes
App.Router.map(function() {
this.resource('posts', { path: '/posts' });
this.resource('post', { path: '/post/:id' });
});
// Handle route for posts list
App.PostsRoute = Ember.Route.extend({
model: function() {
return App.Post.findAll();
}
});
// Handle route for single post
App.PostRoute = Ember.Route.extend({
model: function(params){
return App.Post.findById(params.id);
}
});
// Post model
App.Post = Ember.Object.extend();
App.Post.reopenClass({
findAll: function(){
var posts = [];
$.getJSON("/api/posts").then(function(response){
response.posts.forEach(function(post){
posts.pushObject(App.Post.create(post));
});
});
return posts;
},
findById: function(id){
$.getJSON("/api/post/" + id).then(function(response){
return App.Post.create(response.post);
});
}
});
Then in my template I have this:
<!-- Post list -->
<script type="text/x-handlebars" data-template-name="posts">
<div class="large-12 columns">
<h1>Posts</h1>
<hr>
<ul>
{{#each post in model}}
<li>{{#linkTo 'post' post}}{{post.title}}{{/linkTo}}</li>
{{/each}}
</ul>
</div>
</script>
<!-- Single post -->
<script type="text/x-handlebars" data-template-name="post">
<div class="large-12 columns">
<h1>{{title}}</h1>
<div class="content">
{{post_content}}
</div>
</div>
</script>
I'm having a few issues here. Firstly, the href attribute on the links in the post list are coming out like this:
#/post/<App.Post:ember217>
I can fix this by changing my post route to:
this.resource('post', { path: '/post/:post_id' });
But then when I try to navigate directly to a post by using a URL like /#/post/1 I get an error:
Assertion failed: Cannot call get with 'id' on an undefined object.
Finally, if I leave my post route how it is (/post/:id) then visit the URL /#/post/1 none of the post data is displayed. I can see the correct API endpoint is called and no errors are shown in the console.
However, if i click through to the single post from the posts list the post is displayed properly but it uses the weird URL that I mentioned earlier - #/post/<App.Post:ember217>.
If this helps, this is the JSON the post models are created from:
{"post":
{
"id":2,
"title":"Second post",
"alias":"second-post",
"postedOn":"2013-08-12 09:11:37",
"authorId":1,
"post_content":"Post content"
}
}
Sorry i know there's quite a bit there - I hope it's enough to give a clear picture of what I'm doing wrong.
Thank you

You are receiving this url #/post/<App.Post:ember217> because your dynamic segment is /post/:id, you have to change to yourmodel_id, in your case is /post/:post_id. Using this, by default the route serialize method will know that you want the id atribute of the post, in the url: /post/1, /post/2 etc. And no override will be needed in that case.
You have said that changing to post_id make the url generation works, but the navigation no, when navigate to url directly, but the problem isn't the routing, I think that is because you are using:
App.Post.findById(params.id);
You have to update to:
App.Post.findById(params.post_id);
Other problem that I see (don't know if is a typo mistake), you forget the return in ajax call:
findById: function(id){
// you must return the ajax
return $.getJSON("/api/post/" + id).then(function(response){
return App.Post.create(response.post);
});
}
I hope it helps.

Ember likes your objects to have an id property for generating the url. If you are going to use something other than id in the route (such as :post_id) you'll need tell Ember how to deserialize your model for generating the url.
App.PostRoute = Ember.Route.extend({
model: function(params){
return App.Post.findById(params.id);
},
serialize: function(model) {
return { id: model.get('id') };
}
});
App.PostRoute = Ember.Route.extend({
model: function(params){
return App.Post.findById(params.id);
},
serialize: function(model) {
return { post_id: model.get('post_id') };
}
});

Related

Dynamic segment value

Just getting started with Ember and have a question about the ember way to handle a common pattern.
I have a have the following router.js:
export default Router.map(function() {
this.resource('posts', function(){
this.route('post', { path: "/:title" });
this.route('new');
});
});
I'm wondering how to use the value of the post title as the dynamic segment so that post urls show up as /posts/my-post-title-here
I'm confused as to which model this is being looked up on or if there is an "ember way" to handle this common patter (besides using the posts_id for the dynamic segment).
All my posts are defined in routes/posts.js, so I thought I simply needed to lookup the values in this route inside of my routes/post.js route, like this:
export default Ember.Route.extend({
model: function(params) {
var posts = this.modelFor('posts')
return posts.findBy('title', params.title);
}
});
I'm seeing the /posts/:title route in my Ember inspector, but in the browser, the links are all undefined ( /posts/undefined ).
{{#each model as |post|}}
{{#link-to "posts.post" model }}
<li>{{post.title}}</li>
{{/link-to}}
{{/each}}
I'd love any advice about the proper way to handle this situation or explanations about how Ember looks up values for nested routes.
You need to setup a serializer on your routes/post.js, like this:
// routes/post.js
export default Ember.Route.extend({
model: function(params) {
var posts = this.modelFor('posts')
return posts.findBy('title', params.title);
},
serialize: function(model) {
return { post_slug: model.get('title') };
}
});
See Dynamic Segments

EmberJS: How to load multiple models on the same route?

While I am not new to web development, I am quite new to to client-side MVC frameworks. I did some research and decided to give it a go with EmberJS. I went through the TodoMVC guide and it made sense to me...
I have setup a very basic app; index route, two models and one template. I have a server-side php script running that returns some db rows.
One thing that is very confusing me is how to load multiple models on the same route. I have read some information about using a setupController but I am still unclear. In my template I have two tables that I am trying to load with unrelated db rows. In a more traditional web app I would have just issued to sql statements and looped over them to fill the rows. I am having difficulty translating this concept to EmberJS.
How do I load multiple models of unrelated data on the same route?
I am using the latest Ember and Ember Data libs.
Update
although the first answer gives a method for handling it, the second answer explains when it's appropriate and the different methods for when it isn't appropriate.
BEWARE:
You want to be careful about whether or not returning multiple models in your model hook is appropriate. Ask yourself this simple question:
Does my route load dynamic data based on the url using a slug :id? i.e.
this.resource('foo', {path: ':id'});
If you answered yes
Do not attempt to load multiple models from the model hook in that route!!! The reason lies in the way Ember handles linking to routes. If you provide a model when linking to that route ({{link-to 'foo' model}}, transitionTo('foo', model)) it will skip the model hook and use the supplied model. This is probably problematic since you expected multiple models, but only one model would be delivered. Here's an alternative:
Do it in setupController/afterModel
App.IndexRoute = Ember.Route.extend({
model: function(params) {
return $.getJSON('/books/' + params.id);
},
setupController: function(controller, model){
this._super(controller,model);
controller.set('model2', {bird:'is the word'});
}
});
Example: http://emberjs.jsbin.com/cibujahuju/1/edit
If you need it to block the transition (like the model hook does) return a promise from the afterModel hook. You will need to manually keep track of the results from that hook and hook them up to your controller.
App.IndexRoute = Ember.Route.extend({
model: function(params) {
return $.getJSON('/books/' + params.id);
},
afterModel: function(){
var self = this;
return $.getJSON('/authors').then(function(result){
self.set('authors', result);
});
},
setupController: function(controller, model){
this._super(controller,model);
controller.set('authors', this.get('authors'));
}
});
Example: http://emberjs.jsbin.com/diqotehomu/1/edit
If you answered no
Go ahead, let's return multiple models from the route's model hook:
App.IndexRoute = Ember.Route.extend({
model: function() {
return {
model1: ['red', 'yellow', 'blue'],
model2: ['green', 'purple', 'white']
};
}
});
Example: http://emberjs.jsbin.com/tuvozuwa/1/edit
If it's something that needs to be waited on (such as a call to the server, some sort of promise)
App.IndexRoute = Ember.Route.extend({
model: function() {
return Ember.RSVP.hash({
model1: promise1,
model2: promise2
});
}
});
Example: http://emberjs.jsbin.com/xucepamezu/1/edit
In the case of Ember Data
App.IndexRoute = Ember.Route.extend({
var store = this.store;
model: function() {
return Ember.RSVP.hash({
cats: store.find('cat'),
dogs: store.find('dog')
});
}
});
Example: http://emberjs.jsbin.com/pekohijaku/1/edit
If one is a promise, and the other isn't, it's all good, RSVP will gladly just use that value
App.IndexRoute = Ember.Route.extend({
var store = this.store;
model: function() {
return Ember.RSVP.hash({
cats: store.find('cat'),
dogs: ['pluto', 'mickey']
});
}
});
Example: http://emberjs.jsbin.com/coxexubuwi/1/edit
Mix and match and have fun!
App.IndexRoute = Ember.Route.extend({
var store = this.store;
model: function() {
return Ember.RSVP.hash({
cats: store.find('cat'),
dogs: Ember.RSVP.Promise.cast(['pluto', 'mickey']),
weather: $.getJSON('weather')
});
},
setupController: function(controller, model){
this._super(controller, model);
controller.set('favoritePuppy', model.dogs[0]);
}
});
Example: http://emberjs.jsbin.com/joraruxuca/1/edit
NOTE: for Ember 3.16+ apps, here is the same code, but with updated syntax / patterns: https://stackoverflow.com/a/62500918/356849
The below is for Ember < 3.16, even though the code would work as 3.16+ as fully backwards compatible, but it's not always fun to write older code.
You can use the Ember.RSVP.hash to load several models:
app/routes/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return Ember.RSVP.hash({
people: this.store.findAll('person'),
companies: this.store.findAll('company')
});
},
setupController(controller, model) {
this._super(...arguments);
Ember.set(controller, 'people', model.people);
Ember.set(controller, 'companies', model.companies);
}
});
And in your template you can refer to people and companies to get the loaded data:
app/templates/index.js
<h2>People:</h2>
<ul>
{{#each people as |person|}}
<li>{{person.name}}</li>
{{/each}}
</ul>
<h2>Companies:</h2>
<ul>
{{#each companies as |company|}}
<li>{{company.name}}</li>
{{/each}}
</ul>
This is a Twiddle with this sample: https://ember-twiddle.com/c88ce3440ab6201b8d58
Taking the accepted answer, and updating it for Ember 3.16+
app/routes/index.js
import Route from '#ember/routing/route';
import { inject as service } from '#ember/service';
export default class IndexRoute extends Route {
#service store;
async model() {
let [people, companies] = await Promise.all([
this.store.findAll('person'),
this.store.findAll('company'),
]);
return { people, companies };
}
}
Note, it's recommended to not use setupController to setup aliases, as it obfuscates where data is coming from and how it flows from route to template.
So in your template, you can do:
<h2>People:</h2>
<ul>
{{#each #model.people as |person|}}
<li>{{person.name}}</li>
{{/each}}
</ul>
<h2>Companies:</h2>
<ul>
{{#each #model.companies as |company|}}
<li>{{company.name}}</li>
{{/each}}
</ul>
I use something like the answer that Marcio provided but it looks something like this:
var products = Ember.$.ajax({
url: api + 'companies/' + id +'/products',
dataType: 'jsonp',
type: 'POST'
}).then(function(data) {
return data;
});
var clients = Ember.$.ajax({
url: api + 'clients',
dataType: 'jsonp',
type: 'POST'
}).then(function(data) {
return data;
});
var updates = Ember.$.ajax({
url: api + 'companies/' + id + '/updates',
dataType: 'jsonp',
type: 'POST'
}).then(function(data) {
return data;
});
var promises = {
products: products,
clients: clients,
updates: updates
};
return Ember.RSVP.hash(promises).then(function(data) {
return data;
});
If you use Ember Data, it gets even simpler for unrelated models:
import Ember from 'ember';
import DS from 'ember-data';
export default Ember.Route.extend({
setupController: function(controller, model) {
this._super(controller,model);
var model2 = DS.PromiseArray.create({
promise: this.store.find('model2')
});
model2.then(function() {
controller.set('model2', model2)
});
}
});
If you only want to retrieve an object's property for model2, use DS.PromiseObject instead of DS.PromiseArray:
import Ember from 'ember';
import DS from 'ember-data';
export default Ember.Route.extend({
setupController: function(controller, model) {
this._super(controller,model);
var model2 = DS.PromiseObject.create({
promise: this.store.find('model2')
});
model2.then(function() {
controller.set('model2', model2.get('value'))
});
}
});
The latest version of JSON-API as implemented in Ember Data v1.13 supports bundling of different resources in the same request very well, if you don't mind modifying your API endpoints.
In my case, I have a session endpoint. The session relates to a user record, and the user record relates to various models that I always want loaded at all times. It's pretty nice for it all to come in with the one request.
One caveat per the spec is that all of the entities you return should be linked somehow to the primary entity being received. I believe that ember-data will only traverse the explicit relationships when normalizing the JSON.
For other cases, I'm now electing to defer loading of additional models until the page is already loaded, i.e. for separate panels of data or whatever, so at least the page is rendered as quickly as possible. Doing this there's some loss/change with the "automatic" error loading state to be considered.

When switching from dynamic segments route to other route, application outlet is overwritten

When any route (except windowsProdver) is loaded first, I can transition between all other routes without any problem. When windowsProdver is called directly by URL and then another route is called through linkTo or transitionToRoute, I just get a blank page with the error Uncaught TypeError: Cannot read property 'parentNode' of null in ember.js:23544 in the javascript console of the browser.
As far as I could see so far is, that on calling windowsProdver route directly via URL and switching to another route (with transitionToRoute or linkTo), the application template is overwritten or destroyed so the new template cannot be inserted into the application template / DOM.
windowsIndex should show general stats with a list of product versions ('prodver')
windowsProdver should show specific stats for the prodver chosen in windowsIndex or by direct call by URL
Those are the routes I have specified:
App.Router.map(function() {
this.resource('serverversions', {path: '/serverversions'});
this.resource('windowsIndex', {path: '/stats/windows'});
this.resource('windowsProdver', {path: '/stats/windows/:prodver'});
this.resource("users", {path: "users"}, function() {
this.resource("users.user", {path: ":sernum"});
});
});
The result is:
The routes:
App.WindowsIndexRoute = Ember.Route.extend({
model: function() {
return App.StatsWindowsGeneral.get();
}
});
App.WindowsProdverRoute = Ember.Route.extend({
prodver: null,
model: function(params) {
if(params && params.prodver) {
App.StatsWindowsGeneral.get();
this.prodver = params.prodver;
return $.getJSON(App.Config.api.url + "/stats/windows/" + this.prodver).then(function(response) {
// <some logic here>
return response;
});
}
}
});
application template:
{{view App.HeaderView}}
<div class="container">
{{outlet}}
</div>
The templates are loaded like this:
var loaderObj = {
templates: [
'application.hbs',
'loading.hbs',
'header.hbs',
'index.hbs',
'serverversions.hbs',
'serverversionsserver.hbs',
'stats-index.hbs',
'windowsIndex.hbs',
'windowsProdver.hbs',
'users.hbs',
'user.hbs'
]
};
load_templates(loaderObj.templates);
function load_templates(templates) {
$(templates).each(function() {
var tempObj = $('<script>');
tempObj.attr('type', 'text/x-handlebars');
var dataTemplateName = this.substring(0, this.lastIndexOf('.'));
dataTemplateName = dataTemplateName.replace(/\-/g, '/');
console.log(dataTemplateName);
tempObj.attr('data-template-name', dataTemplateName);
$.ajax({
async: false,
type: 'GET',
url: 'assets/templates/' + this,
success: function(resp) {
tempObj.html(resp);
$('body').append(tempObj);
}
});
});
}
EDIT
I first did the routing in the following way:
App.Router.map(function() {
this.resource('serverversions', {path: '/serverversions'});
this.resource('stats', {path: '/stats'}, function() {
this.route('windows');
this.route('windowsProdver', {path: '/windows/:prodver'});
});
this.resource("users", {path: "users"}, function() {
this.resource("users.user", {path: ":sernum"});
});
});
but after reading the article on http://hashrocket.com/blog/posts/ember-routing-the-when-and-why-of-nesting, I switched to the routing shown in the code at the top.
I was having a similar issue today, transitioning to a route in my app and getting the aforementioned error message. Then I came upon this discussion. Like it says, I found some malformed HTML in my handlebar templates that caused the issue for me. Now my app transitions between routes just fine. I'm not sure if that covers all your issue, but I hope it helps.
Bryan
In my case, my nested template was containing the script tag, e.g.,
<script type="text/x-handlebars" data-template-name="home">
...
</script>
... removing it fixed the problem.

Emberjs template not binding to restful data

Got this route to get the data from a restful service
var App = Ember.Application.create({rootElement: '#planner'});
App.Store = DS.Store.extend();
App.Router.map(function(){
this.resource('home');
});
App.HomeRoute = Ember.Route.extend({
model: function(){
return Ember.$.getJSON('/api/get-planner/');
}
});
And template:
<script type="text/x-handlebars" data-template-name="home">
{{name}}
</script>
Somehow the value of name is not displayed. I can confirm the api is returning correct json data.
Ember-Data expects the JSON like this:
{
planner: {
name: 'Test'
// your data
}
}
So if your API returns this JSON:
{
name: 'Test'
}
It won't work.
I would suggest to use Ember-Model instead (https://github.com/ebryn/ember-model), since it is more stable and allows you to customize the behavior of the REST adapter.
Your code might look like this:
App.PlannerModel = Ember.Model.extend({
name: Ember.attr(),
// see the documentation of ember-model for this
});
App.PlannerModel.url = '/api/get-planner/';
App.PlannerModel.adapter = Ember.RESTAdapter.create();
App.HomeRoute = Ember.Route.extend({
model: function() {
return App.PlannerModel.find();
}
});
If you want to take this approach, make sure not to include Ember-Data and use Ember-Model instead.

ember linkTo sending empty params in route's model hook

I have a blog site where going to the url /posts should list all posts. Then clicking a link for /posts/:post_id should display the details of that post. However, when clicking the link, it seems that the linkTo is not passing the :post_id properly. Here is my code:
Router:
App.Router.map(function() {
this.resource("posts", function() {
this.resource("post", { path: "/:post_id" }, function() {
//other routes
});
});
this.route("posts.index", { path: "/" });//Links root url to posts index template
});
Routes:
/****** Posts ******/
App.PostsRoute = Ember.Route.extend({
});
App.PostsIndexRoute = Ember.Route.extend({
model: function() {
return App.Post.find();
}
});
/****** Post *******/
App.PostRoute = Ember.Route.extend({
});
App.PostIndexRoute = Ember.Route.extend({
});
Controllers:
/****** Posts ******/
App.PostsController = Ember.ArrayController.extend({
});
App.PostsIndexController = Ember.ArrayController.extend({
});
/****** Post ******/
App.PostController = Ember.ObjectController.extend({
});
App.PostIndexController = Ember.ObjectController.extend({
});
Posts/Index Template:
{{#each post in controller}}
<tr>
<td>{{post.name}}</td>
<td>{{post.title}}</td>
<td>{{post.content}}</td>
<td>{{#linkTo post.index post}}View{{/linkTo}}</td>
</tr>
{{/each}}
Every time I click the {{linkTo post.index post}}, it takes me to the correct url, renders the correct templates (post/index), but nothing displays. I tried putting a model hook in the PostIndexRoute like this:
App.PostIndexRoute = Ember.Route.extend({
model: function(params) {
return App.Post.find(params.post_id);
}
});
But params = {} It has no data in it. Can someone point me in the right direction? what am I doing wrong?
Since you are passing the post object you need to {{linkTo}} a route that accepts a dynamic URL segment. In your example posts.index doesn't accept a parameter.
<td>{{#linkTo post post}}View{{/linkTo}}</td>
Here is an example of a way to structure your code to better handle the routing: JSBin example
You should also check out the Ember.js routing guides as an example of how to structure routes.

Categories