Ember.js data transition after version upgrade - ember.js

So I'm writing a POC app, and I am running into an issue after upgrading my Ember library to RC1. I noticed that when I was transitioning to a route in the new version, a stringified version of the object appears to show up in the URL, like so...
http://localhost:3000/posts/<App.Post:ember269:511401b8c589137c34000001>
The routes work successfully when transitioned to like this, but obviously trying to visit a URL like that a second time won't work. So I figured I would edit my code to transition to the ID instead.
For my edit route, I have the following save event.
events: {
save: function(post){
post.one('didUpdate', this, function(){
this.transitionTo('posts.show', post);
});
post.get('transaction').commit();
}
}
This produces a URL like the above when the transition happens. So I corrected it to the following...
events: {
save: function(post){
post.one('didUpdate', this, function(){
this.transitionTo('posts.show', post.id);
});
post.get('transaction').commit();
}
}
This produces the correct URL format, but the show route doesn't produce any output. (note that show output DOES produce output when I visit the URL for the first time with a correct format, just not when I transition to it from the edit route).
App.PostsShowRoute = Em.Route.extend({
model: function(params){
return App.Post.find(params.id);
},
setupController: function(controller, model){
controller.set('content', model);
}
});
So I'm confused. Any insight into the cause of this problem (and if you know why the RC produces it) would be much appreciated. Help me have my cake and eat it, too. Thanks!

From your App.PostsShowRoute I can guess that you set up your route mapping like this:
App.Router.map(function() {
this.resource('posts', function() {
this.route('show', { path:'/:id' });
});
});
You need change :id to :post_id:
App.Router.map(function() {
this.resource('posts', function() {
this.route('show', { path:'/:post_id' });
});
});
Now, since you are using Ember conventions, you can take advantage of that by deleting the whole App.PostsShowRoute = Em.Route.extend ... because Ember can take care of it for you.
And use your first method which was correct:
events: {
save: function(post){
post.one('didUpdate', this, function(){
this.transitionTo('posts.show', post);
});
post.get('transaction').commit();
}
}

Related

Ember access array controller's content from another controller

I have two controllers: postsController and postController. the post route is NOT nested under posts ( I do not want to do so because I want the posts view to be replaced by the post view, not added to it.)
This is what my router looks like.
this.resource('posts', {path: '/'}, function(){
// this.route('index', {path: '/'});
});
this.resource('post', { path: 'posts/:id' }, function(){
this.route('show');
}
Now, the postsController:
App.PostsRoute = Ember.Route.extend({
model: function(){
return this.store.find('post');
},
})
App.PostsController = Ember.ArrayController.extend({
testProperty: "This is a test"
})
And the PostController:
App.PostRoute = Ember.Route.extend({
model: function(params){
return this.store.find('post', params.id);
},
})
App.PostController = Ember.ObjectController.extend({
needs: ['posts'],
percentValue: 100,
progressBarWidth: null,
advancePost: function(delta) {
var that = this;
var posts = that.get('controllers.posts');
// debugger <-- This is where all my questions are concerned with.
...
},
actions: {
nextPost: function() {
this.advancePost(1);
},
previousPost: function() {
this.advancePost(-1);
},
}
})
So. PostController 'needs' PostsController, and on action advancePost, needs to access PostsController's model (which is supposed to contain an array of Post Objects.) At the point where my debugger is, the variable posts accessed postsController. At this point, if I run
posts.get('testProperty') // => gives "This is a test", which is correct.
on the chrome console, I get "This is a test". So I know that I have gained access to the Posts Controller itself. However, any of the following attempts:
posts.get('model') // gives []??
posts.get('content') // gives []??
returns an empty array ( [] ).
I fail to understand why. If I have access to the Posts arrayController, why do I not have access to its data? Consequently, how would I gain access to its data?
I would greatly appreciate clarity on this.
First, if I understand your context, I think you should rather nest post under posts by doing this :
this.resource('posts', {path: '/'}, function(){
this.route('post', {path: '/:id'});
});
And then, since posts is your "parent" route, you no longer need to declare the needs dependency in your controller.
To access get your posts, apply the following in your Route :
setupController: function(controller, model) {
this._super(controller, model);
controller.set('posts', this.modelFor("posts").get("content"));
}
You should think about the interaction patterns you want to support. Is it typical for a user to go back to the list of items they were just looking at? Reloading posts whilst keeping the scroll position in the list they were at will be problematic. Also re-fetching from the server and tearing down and setting up the DOM has a lot more overhead than just leaving it there.
Are you intending to use animation on your transitions? Using something like liquid-fire will need both outlets rendered to perform an animation so it makes no sense to tear down the list in that case.
CSS can be used to have the nested outlet fill whatever container it is put in allowing the approach provided by Pascal Boutin to be used.

How to Add Child Record to Existing Parent Record?

I've been googling and scouring Stack Overflow for some sort of hint on this subject but the information is scattered at best.
I'm trying to Create a new Child Record (Comment) and save it to an existing Parent Record (Post). I am using Ember-Model, rather than Ember-Data, but any tips or pointers would be greatly appreciated.
At the moment, I've been successful creating a new, embedded Comment but only when it is created with a new Post record. So:
How do I go about loading/retrieving the currently loaded Post(parent record) in order to apply Comments (child records) to it?
I've been reading up on controller dependencies, using needs: and this.controllerFor and this.modelFor in order to have access to another controller/model's content but have been unable to wire these things together into something meaningful.
Anyway, here is what I've whittled my application code down to, in the hopes I might be able to stumble into the proper way of doing this...
Routes
App.Router.map(function() {
this.resource('post', { path: '/:post_id' }, function() {
this.resource('comments', { path: '/comments'} );
});
});
I removed all the other resources & routes, so I'm left with App.Post, App.PostIndex, and App.Comments. I think my routes are the issue here, I assume I'm not properly implementing the methods to use the loaded Post record in my Comments route.
App.IndexRoute = Ember.Route.extend({
model: function() {
return App.Post.find();
},
setupController: function(controller, model) { // I'm not certain if this
controller.set('content', model); // setupController is needed?
}
});
App.PostRoute = Ember.Route.extend({
model: function(params) {
return App.Post.find(params.post_id);
},
setupcontroller: function( controller, model) { // again, unsure if this
this.controllerFor('post').get('comments'); // is correct.
controller.set('content', comments);
}
});
App.CommentsRoute = Ember.Route.extend({
afterModel: function() {
this.set('post', this.modelFor('post'));
},
setupcontroller: function( controller, model) {
this.controllerFor('post').get('comments');
controller.set('content', comments);
}
});
Controller
App.CommentsController = Ember.ArrayController.extend({
needs: "post",
actions: {
addComment: function() {
var post = App.Post.create({
title: 'static post title'
});
post.get('comments').create({
message: 'static message'
});
post.save();
}
}
});
This is my current Comments Controller, which can create a new Post with an embedded Comment. I've found and been given numerous examples in which to create the Comment, but none seem to work for me. Basically, I'm struggling with defining the var post = ... as the currently loaded record. I've implemented various approaches in an attempt at trial & error. Thus far I have attempted:
var post = App.Post.create();, returns property undefined, as this would create a new record. However, I gave it a shot as every example i saw related to this defined their record as such.
var post = this.get('post');, returns a cannot call 'get' on undefined. I've tried using this method of defining my current post on both the Comments controller and Post controller.
var post = this.get('controllers.post.content);, returns a 'cyclic error' from the backend I'm using.
var post = App.Post.find();, returns a cannot call 'get' on undefined.
var post = App.Post.find(1);, Again, returns a cannot call 'get' on undefined. Figured I'd give it a shot because this is one of those recurring examples people provide. The backend I use applies its own ID to each record, and I'm unsure if I would be able to/how to have the .find() method use a dynamic ID value and retrieve only the model I just loaded.
I'm guessing that I'm not properly setting up my Routes and Controller dependencies?
If anyone has a suggestion, relevant link, or fix I would be very grateful.
This one (seemingly simple) issue/use case has me at wit's end at this point.
Try this (works pre beta 2):
App.CommentsController = Ember.ArrayController.extend({
actions: {
addComment: function() {
this.content.createRecord({
message: 'static message'
});
}
}
});
Ember Data Beta 2 and later:
App.CommentsController = Ember.ArrayController.extend({
needs: ["post"],
actions: {
addComment: function() {
var post = this.get('controllers.post');
var comment = this.get('store').createRecord('comment', {
message: 'static message',
post: post
});
comment.save().then(function() {
post.addObject(comment);
// You may or may not need to save your post, too. In my case my backend handles
// the inverses of relationships (if existing), so there's no need. We still need
// to do this for Ember, though
});
}
}
});

Idiomatic Emberjs for nested routes but non-nested templates

This is a follow-up from Understanding Ember routes.
Master/detail views are great but I'm trying to have a a hierarchical URL route without nesting their templates. However, I still need access to the parent model for things like breadcrumb links and other references.
So /users/1/posts should display a list of posts for user 1. And /users/1/posts/1 should display post 1 for user 1, but it shouldn't render inside the user template's {{outlet}}. Instead, it should completely replace the user template. However, I still need access to the current user in the post template so I can link back to the user, show the user's name, etc.
First I tried something like this (Method #1):
App.Router.map(function() {
this.resource('user', { path: '/users/:user_id' }, function() {
this.resource('posts', function() {
this.resource('post', { path: '/:post_id' });
});
});
});
...
App.PostRoute = Ember.Route.extend({
model: function(params) {
return App.Post.find(params.post_id);
},
renderTemplate: function() {
this.render('post', {
into: 'application'
});
}
});
This replaced the the user template with the post one, as expected. But when I click the browser's back button the user template doesn't render again. Apparently the post view is destroyed but the parent view is not re-inserted. There are a few questions on here that mention this.
I then got it to work with something like this (Method #2):
App.Router.map(function() {
this.resource('user', { path: '/users/:user_id' }, function() {
this.resource('posts');
this.resource('post', { path: '/users/:user_id/posts' }, function() {
this.resource('post.index', { path: '/:post_id' });
});
});
...
App.PostRoute = Ember.Route.extend({
model: function(params) {
return App.User.find(params.user_id);
},
setupController: function(controller, model) {
controller.set('user', model);
}
});
App.VideoIndexRoute = Ember.Route.extend({
model: function(params) {
return App.Post.find(params.post_id);
}
});
App.PostIndexController = Ember.ObjectController.extend({
needs: 'post'
});
But this seems a bit hacky to me and not very DRY.
First, I need to retrieve the User again in the PostRoute and add it as an ad-hoc variable to the PostController (this wouldn't be necessary if the routes were properly nested and I could just set a needs: 'user' property in the PostController). In addition, this may or may not have an impact on the back-end depending on the adapter implementation of ember-data or whatever technology is used to retrieve the data from the server (i.e. it may cause an unnecessary second call to load User).
I also need an additional `PostIndexController' declaration just to add that new dependency, which is not a big deal.
Another thing that doesn't feel right is that /users/:user_id/posts appears twice in the router configuration (one nested, one not).
I can deal with these issues and it does work but i guess that, overall, it seems forced and not as graceful. I'm wondering if I'm missing some obvious configuration that will let me do this with regular nested routes or if someone has a recommendation for a more "Ember.js way" of doing this.
I should mention that regardless of the technical merits of Method #2, it took me quite a while to figure out how to make it work. It took a lot of searching, reading, experimenting, debugging, etc. to find just the right combination of route definitions. I would imagine that this is not a very unique use-case and it should be very straightforward for a user to set up something like this without spending hours of trial and error. I'll be happy to write up some tips for this in the Ember.js documentation if it ends up being the right approach.
Update:
Thanks to #spullen for clarifying this. My case was not as straightforward as the example because some sub-routes need nested templates and some don't, but the answer helped me figure it out. My final implementation looks something like this:
App.Router.map(function() {
this.resource('users', { path: '/users/:user_id' }, function() {
this.resource('users.index', { path: '' }, function() {
this.resource('posts')
});
this.resource('post', { path: '/posts/:post_id' }, function() {
this.resource('comments', function() {
this.resource('comment', { path: '/:comment_id' });
});
});
});
});
So now posts renders under the users template but post replaces everything. comments then renders under post and comment, in turn, renders under comments.
All of them are sub-routes of users so the user model is accessible to all of them without acrobatics, by doing this.modelFor('users') in each Route where needed.
So the templates look like this:
users
|- posts
post
|- comments
|-comment
I don't know why the { path: '' } is needed for the users.index resource definition but if I take it out Ember doesn't find the users route. I would love to get rid of that last vestige.
You could define the parent template to just display the outlet and have an index route which will get displayed inside that. Then for the nested resource you can do the same thing.
<script type="text/x-handlebars" data-template-name="user">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="user/index">
<h2>user/index</h2>
</script>
<script type="text/x-handlebars" data-template-name="posts">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="posts/index">
<h2>posts/index</h2>
</script>
That way it won't be a master/detail.
The router would be:
App.Router.map(function() {
this.resource('user', function() {
this.resource('posts', function() { });
});
});
Then if you need to get information about the parent you can use modelFor. So if you were in posts, you could do this.modelFor('user');
Here's a jsbin that demonstrates this.
Hope this is helpful.

What's the proper way to access parameters from within Ember.Route. setupController?

Ember.Route.model has access to the params variable, but Ember.Route.setupController does not. This is troublesome for me, because my path has multiple dynamic segments, and I need to use all of them in my template.
Specifically, my path looks like this: /project/:project_id/task/:task_id. Note that a task can belong to many projects, not just one. Therefore we can't tell what project we're looking at just be looking at the task itself: we have to use the project ID found in the URL. Here's how I'm doing it currently:
App.TaskRoute = Ember.Route.extend({
// This works just fine:
serialize: function(model) {
return {
task_id: model.get('id'),
project_id: this.modelFor('project').get('id')
};
},
model: function(params) {
this.set('parentProjectId', params.project_id);
return App.Task.find(params.task_id);
},
setupController: function(controller, model) {
var parentProject = this.modelFor('project') ?
this.modelFor('project') :
App.Project.find(this.get('parentProjectId'));
controller.set('parentProject', parentProject);
controller.set('content', model);
}
});
Maybe I'm being paranoid, this just feels hacky. If the route was meant to have access to the parameters, then it would already have a params property attached to it. Is there a better way?
EDIT: I made some update to the code above. Also, my routes look like this:
App.Router.map(function() {
this.resource('project', { path: '/project/:project_id' });
this.resource('task', { path: 'project/:project_id/task/:task_id' });
});
You have no access to these params in the setupController hook. The model hook has access to a params object, because it is just called, when your app is entered via URL.
Your code looks quite fine, it you really know, that you want to do it this way. What does feel hacky to you about it? When looking at this code, i am asking myself why you did not split the logic of your Route into a ProjectRoute and a subordinated TaskRoute. Wouldn't that work for you?
Update: Response to your changes
Nesting resources is likely the key to success in your case:
App.Router.map(function() {
this.resource('project', { path: '/project/:project_id' }, function(){
this.resource('task', { path: '/task/:task_id' });
});
});
Since the TaskRoute is nested not you have to rename it to ProjectTaskRoute:
App.ProjectTaskRoute = Ember.Route.extend({
...
});
This should enable you to remove the parentProjectId property from the Route.
Since Ember 1.8, the Route class has a paramsFor function:
import Route from '#ember/routing/route';
export default Route.extend({
setupController(controller) {
this._super(...arguments);
const params = this.paramsFor('name.of.your.route')
}
});

Emberjs scroll to top when changing view

When the main view of my application is switched (new route that reconnects the main outlet of my application controller) I want the page to be scrolled to the top. Otherwise it's a bit strange that I navigate to another page-like view and the viewport is still lost somewhere where I left off.
I hacked a solution and wonder if there's a better way or if anyone has the same thing.
Here's what I do:
App.ApplicationController = Ember.Controller.extend({
connectOutlet: function(){
window.scrollTo(0, 0);
this._super.apply(this, arguments);
}
});
#Baruch's solution is good, but when I implemented it I had render on elements within my application state and would cause a scrollTop when it was not needed.
I found this to be much more effective as it only runs on the path change:
App.ApplicationController = Ember.Controller.extend({
currentPathChanged: function () {
window.scrollTo(0, 0);
}.observes('currentPath')
});
I achieved this with the following code:
Ember.Route.reopen({
render: function(controller, model) {
this._super();
window.scrollTo(0, 0);
}
});
Coffee Script:
Ember.Route.reopen
activate: ->
#_super()
window.scrollTo(0, 0)
Javascript:
Ember.Route.reopen({
activate: function() {
this._super();
window.scrollTo(0, 0);
}
});
You should probably try and extend Ember.Route and add your window.scrollTo in the enter callback. Then instead of using Ember's Route for your leaf routes, you call your route .extend(), so they'll automatically scroll up when you enter a route/state. Something similar to this:
// define your custom route and extend "enter"
var MyRoute = Em.Route.extend({
enter: function(router) {
// for now on, all the routes that extend this,
// will fire the code in this block every time
// the application enters this state
// do whatever you need to do here: scroll and whatnot
}
});
App.Router = Em.Router.extend({
enableLogging: true,
location: 'hash',
index: Em.Route.extend({
route: '/',
connectOutlets: function(router) {
...
},
// on your leaf routes, use your own custom route that
// does your scroll thing or whatever you need to do
home: MyRoute.extend({
route: '/',
connectOutlets: function (router, context) {
...
}
}),
// other routes...
})
});
does it make sense?
It's now render(name, options), and if you are specifically calling render (ie with a modal) you want to pass that to super()
Ember.Route.reopen({
render: function(name, options) {
if (name != null) {
return this._super(name, options);
} else {
return this._super();
}
}
});
Ember 3.12+ (this is technically 3.20 code listed here)
import EmberRouter from '#ember/routing/router';
const Router = EmberRouter.extend({
init() {
// call event everytime route changes
this.on('routeDidChange', () => {
this._super(...arguments);
window.scrollTo(0, 0); // scrolls to top
});
}
});
Router.map(function () {
// your mapping code goes here
});
export default Router;
Prior to 3.12 (this is technically 3.4 but the key code should be the same)
import EmberRouter from '#ember/routing/router';
const Router = EmberRouter.extend({
didTransition() {
this._super(...arguments);
window.scrollTo(0, 0);
}
});
Router.map(function () {
// your mapping code goes here
});
export default Router;
We have handled this problem serveral times and the way we've found that is the easiest and most straight-forward way is to configure this once in the router.js file using a 'route transition' event function. We used didTransition before it got deprecated in Ember 3.12 in lieu of routeDidChange. I've posted both examples below. Some syntax may differ slightly depending on which version of Ember you are on but this core code should be the same.