The afterModel hook is not being hit when you attempt to transitionTo from a parent route to the same route.
I expected the afterModel hook to always fire.
Twiddle
Edit:
It's kind of hard to understand unless you are can look at the twiddle but here's the code
// Controller/application.js
export default Ember.Controller.extend({
appName: 'Ember Twiddle',
actions: {
goTo() {
this.transitionToRoute('food');
}
}
});
// templates/application.hbs
<button onclick={{action 'goTo'}}>Food Options</button>
<br>
{{outlet}}
//router.js
const Router = Ember.Router.extend({
location: 'none',
rootURL: config.rootURL
});
Router.map(function() {
this.route('food', function() {
this.route('options');
this.route('fruits');
this.route('veggies');
});
});
//routes/food.js
export default Ember.Route.extend({
model() {},
afterModel() {
this.transitionTo('food.options');
}
});
Clicking the food options button the first time correctly calls the afterModel in food route. Clicking it the second time does not hit the afterModel.
Refer AlexSpeller Diagonal to see what route structure, templates and route hooks are for a given ember route definition. Its an awesome graphical representation of the route structure.
Question:
I am in a sub route under food, say food/fruits. If I click the button
again I expect to be taken to the food route and then redirected to
options
Answer: food route has no URL, as it is a parent route. You will always be in a child state of this route (e.g. food.index). So when you say transitionTo('food') that means you are transitioning to food.index route.
URL /food/fruits - will execute food model hooks and fruits model hook.
URL /food - will execute food/index model hook and it will not execute food model hook.
So in your case, You can remove this.transitionTo('food.options') from food afterModel hook and you can introduce food.index route which has food options.
Look at modified ember twiddle which includes index route.
Related
There's this really annoying feature about Ember that I'm not sure how to get around. I may have a url that looks like the following
http://{my-blog-name}/posts/view/{some-blogpost-ID}
The way I get to this page is by clicking on a link inside of my {my-blog-name}/posts page. This works and will display the page as expected. However, if I try to refresh the page, or if I just literally type my http://{my-blog-name}/posts/view/{some-blogpost-ID} into my url search box, I will get
Assertion Failed: `id` passed to `findRecord()` has to be non-empty string or number
Here is how I navigate to the posts/view/{some-blog-id} page.
post.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.findAll('post');
}
});
posts.hbs
<li class="title-list-item">{{#link-to "posts.view" posts}}{{posts.title}}{{/link-to}}</li>
view.js
import Ember from 'ember';
var siteId;
export default Ember.Route.extend({
model(params) {
siteId = params.site_id;
return this.store.findRecord('post', params.site_id);
}
});
view.hbs
<div id="Links">
<h1 id="blog-header-title">My Blog</h1>
<!--<p>{{!#link-to 'welcome'}} See about me{{!/link-to}}</p>-->
{{outlet}}
</div>
{{outlet}}
router.js
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: config.locationType,
rootURL: config.rootURL
});
Router.map(function() {
this.route('index', { path: '/' }); // This is usually automatic if path undeclared, but declared here to support /index below
this.route('posts', function() {
this.route('view', {path: '/view/:post_id'});
});
this.route('welcome');
}
This is really frustrating because it means I can't make a blog post and share the link with a friend. Why does this happen and is there a good way to get around it?
posts.js route is returning all the posts available, that's the RecordArray.
<li class="title-list-item">{{#link-to "posts.view" posts}}{{posts.title}}{{/link-to}}</li>
so in the above posts - refers to single post model or RecordArray of post model ?. if the above is single model then you will receive params.post_id in model hook of view.js, currently you are taking params.site_id instead of params.post_id.
Reason for not executing the model hook.
https://guides.emberjs.com/v2.13.0/routing/specifying-a-routes-model/#toc_dynamic-models
Note: A route with a dynamic segment will always 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), and a
model context is provided (second argument to link-to), then the hook
is not executed. If an identifier (such as an id or slug) is provided
instead then the model hook will be executed.
i would like to open a new perspective with query params.
That means in my index.hbs i click a button get the value from the input field.
After that i will open a new route in my case map-view with path '/map' with query params like
localhost:4200/map/?search=xyz
when i do in my index controller:
queryparams:['location'],
location:null,
and in my route
actions:{
search(location){
this.transitionTo('map-view');
}
}
i get on my index url instantly
/?location=xcyxyx
but i want on my map route
localhost:4200/map/?search=xyz
Define search property in index.js controller, use it for queryParams value transitioning to map-view route.
index.hbs
{{input value=search}}
<br />
<button {{action 'transitionToLocation'}}> Search</button>
controllers/index.js - refer transitionToRoute
import Ember from 'ember';
export default Ember.Controller.extend({
search:'india',
actions:{
transitionToLocation(){
this.transitionToRoute('map-view',{queryParams: {search: this.get('search')}});
}
}
});
routes/map-view.js
Define queryParams search with refreshModel true then this will force to fire beforeModel and model and afterModel hook for search property change.
import Ember from 'ember';
export default Ember.Route.extend({
queryParams:{search: {refreshModel:true}},
});
The queryparam is going to be 'search' since that is what you want showing in your URL.
The syntax can be found on:
https://guides.emberjs.com/v2.10.0/routing/query-params/
Further down on that page, it shows how to specify query parameters when using transitionTo. Hope this helps.
I'm looking for a way for templates to change depending on whether a child route is active or not.
Let's say we have the routes /post/1/ and /post/1/comments.
I'd like post.hbs to be able to do something like this:
<div class={{if isChildRouteActive 'compact' 'expanded'}}>...</div>
{{outlet}}
Such that /post/1 would yield isChildRouteActive=false and /posts/1/comments would yield isChildRouteActive=true.
One could conceptualize it this way:
{{#if outlet}}...{{else}}...{{/if}}
How can I expose this property to all of my templates?
I think index child route will be good for you. Every parent route has index route child, to show something when others children routes are inactive. See index route in nested routes section here: http://guides.emberjs.com/v1.12.0/routing/defining-your-routes/
In your example:
// post template
{{outlet}}
//post/index template
You see this text when post.comments route is inactive. If you activate post.comments this text will dissapear and you will see comments quantity.
//post/comments template
Comments: {{comments.length}}
May be this looks good to you.
//post controller
import Ember from 'ember';
export default Ember.Controller.extend({
isChildRouteActive: false,
});
//child route
import Ember from 'ember';
export default Ember.Route.extend({
parentController: Ember.computed( function() {
return this.controllerFor('post');
}),
setupController: function(controller, model) {
this._super(controller, model);
this.get('parentController').set('isChildRouteActive', false);
},
deactivate: function() {
this.get('parentController').set('isChildRouteActive', true);
}
});
I am trying to figure out how to convert a route into a modal, such that you can navigate to it via any other route WHILE preserving underlying(previous) template.
For example:
http://example.com/site/index goes to index.hbs
http://example.com/site/page2 goes to page2.hbs
http://example.com/site/article/1234 goes to article.hbs if user comes from another domain(fresh start)
BUT http://example.com/site/article/1234 opens up article.hbs inside the "article-modal" outlet if user comes any other route.
Here is the router.js
Market.Router.map(function() {
this.route('index', { path: '/' });
this.route('start', { path: 'start' });
this.route('article', { path: 'article/:article_id' });
this.route('404', { path: '*:' });
});
here is application.hbs
<div class="main-container">{{outlet}}</div>
{{outlet "article-modal"}}
and here is article.js route Alternative case #1
Em.Route.extend({
beforeModel: function(transition, queryParams) {
if(!Em.isEmpty(this.controllerFor('application').get('currentRouteName'))) {
this.render('article', {
into: 'application',
outlet: 'article-modal'
});
return Em.RSVP.reject('ARTICLE-MODAL');
}
},
model: function(params) {
return this.store.find('article', params.id);
},
actions: {
error: function(reason) {
if(Em.isEqual(reason, 'ARTICLE-MODAL')) { // ARTICLE-MODAL errors are acceptable/consumed
//
return false;
}
return true;
}
}
});
and here is article.js route Alternative case #2
Em.Route.extend({
renderTemplate: function() {
if(!Em.isEmpty(this.controllerFor('application').get('currentRouteName'))) {
this.render({into: 'index', outlet: 'article-modal'});
} else {
this.render({into: 'application'});
}
},
model: function(params) {
return this.store.find('product', params.id);
},
});
Problem with case #1 is that browser address bar does not reflect current route. If user goes from index route to article route the browser address bar still shows /index.. So if he presses back button app breaks.
Problem with case #2 is that it discards the contents of index.hbs because the article route is not nested.
Is it possible to even have such functionality with Ember?
Thanks :)
This is my second answer to this question. My original answer (https://stackoverflow.com/a/27947475/1010074) didn't directly answer OP's question, however, I outlined three other approaches to handling modals in Ember in that answer and am leaving it there in case it's helpful to anyone else.
Solution: define multiple routes with the same path
While Ember doesn't usually allow you to define two routes that use the same path, you can actually have a nested route and an un-nested route with the same effective path, and Ember works with it just fine. Building off of option 3 from my original answer, I have put together a proof of concept that I think will work for you. Here's a JS Fiddle: http://jsfiddle.net/6Evrq/320/
Essentially, you can have a router that looks something like this:
App.Router.map(function () {
this. resource("index", {path: "/"}, function(){
this.route("articleModal", {path: "/article"});
});
this.route("article", {path: "/article"});
});
And within your templates, link to the index.articleModal route:
{{#link-to "index.articleModal"}}View article!{{/link-to}}
Since articleModal renders inside of index, your index route isn't un-rendered. Since the URL path changes to /article, a reload of the page will route you to your regular Article route.
Disclaimer: I am unsure if this is exploiting a bug in current Ember or not, so mileage here may vary.
Edit: Just re-read OP's question and realized I didn't understand his question, so I created a new answer (https://stackoverflow.com/a/27948611/1010074) outlining another approach that I came up with after experimenting with something.
Option 1: Ember's suggested method for handling Modals
The Ember website has a "cookbook" for how they recommend handling modal dialogs:
http://emberjs.com/guides/cookbook/user_interface_and_interaction/using_modal_dialogs/
Essentially, you would create an action in a route that opens the modal:
App.ApplicationRoute = Ember.Route.extend({
actions: {
openArticleModal: function(article) {
return this.render(article, {
into: 'application',
outlet: 'modal',
controller: this.controllerFor("article")
});
}
}
});
And then call this.send("openArticleModal", article) either from your controller / another route or you could do something like <button {{action "openArticleModal" article}}>View Artice</button> in your template.
Essentially this method takes the modal out of a routed state, which means the modal won't be URL bound, however if you need to be able to open the modal from anywhere in the app and not un-render the current route, then it's one of your few options.
Option 2: If you need URL-bound modals that can be opened from anywhere
For a current project, I have done something that works for this use case by using query params. To me, this feels a little hacky, but it works fairly well in my tests so far (others in the community - if you have opinions on this, please let me know). Essentially, it looks like this:
App.ApplicationController = Ember.Controller.extend({
queryParams: ["articleId"],
articleId: null,
article: function() {
if(!this.get("articleId") return null;
return this.get("store").find("article", this.get("articleId"));
}
});
In application.hbs:
{{#if article.isFulfilled}}
{{render "articleModal" article.content}}
{{/if}}
Then I can use normal {{link-to}} helpers and link to the query param:
{{#link-to (query-params articleId=article.id)}}View Article{{/link-to}}
This works, but I'm not entirely happy with this solution. Something slightly cleaner might be to use an outlet {{outlet "article-modal"}} and have the application route render into it, but it might take more LOC.
Option 3: If the modal is only ever opened from one route
You can make the route that the modal will open into a parent of the modal route. Something like this:
Market.Router.map(function() {
this.resource('articles', { path: '/articles' }, function() {
this.route('modal', { path: '/:article_id' });
});
});
This works well if your modal can only "open" from within a single route. In the example above, the modal will always open on top of the articles route, and if you link-to the modal route from anywhere else in the app, the articles route will render underneath the modal. Just make sure that the "close" action of your modal transitions you out of the modal route, so a user can't close your modal but and still be on the modal route.
The task:
Open a form in a lightbox to create a new "event"; the opened form should be bookmarkable.
The road blocks:
There are examples of opening a lightbox using {{action}} tags, but could not find one that opened in its own route.
There are many examples using older versions of ember.js.
There is not a lot of documentation related to ember-data and REST (I know, I know...it isn't "production ready").
The problem:
The fields in the form were not being tied to a backing model so "null" was being posted to my servlet (a Spring controller).
My very first iteration was not too far off from the final outcome (jsfiddle). The thing that finally made it works swapping this:
EP.EventsNewRoute = Ember.Route.extend({
...
setupController : function(controller, model) {
controller.set("model", model);
},
...
});
...for this:
EP.EventsNewRoute = Ember.Route.extend({
...
setupController : function(controller, model) {
this.controllerFor("events-new").set("model", model);
},
...
});
The question:
Why does the setupController function need to call controllerFor in order to properly set up the model?
And finally, since I struggled to find a fully-functional example, I wanted to make this accessible (and hopefully discover improvements).
Here's the fiddle: http://jsfiddle.net/6thJ4/1/
Here are a few snippets.
HTML:
<script type="text/x-handlebars">
<div>
<ul>
{{#linkTo "events.new" tagName="li"}}
Add Event
{{/linkTo}}
</ul>
</div>
{{outlet events-new}}
</script>
<script type="text/x-handlebars" data-template-name="events-new">
<form>
<div>
<label>Event Name:</label>
{{view Ember.TextField valueBinding="name"}}
</div>
<div>
<label>Host Name:</label>
{{view Ember.TextField valueBinding="hostName"}}
</div>
</form>
</script>
JavaScript:
...
EP.Router.map(function() {
this.resource("events", function() {
this.route("new");
});
});
EP.EventsNewRoute = Ember.Route.extend({
model : function() {
return EP.Event.createRecord();
},
setupController : function(controller, model) {
//controller.set("model", model); // Doesn't work? Why not?
this.controllerFor("events-new").set("model", model); // What does this do differently?
},
...
});
EP.EventsNewController = Ember.ObjectController.extend({
save : function() {
this.get("content.transaction").commit(); // "content.store" would commit _everything modified_, we only have one element changed, so only "content.transaction" is necessary.
}
});
EP.EventsNewView = Ember.View.extend({
...
});
EP.Event = DS.Model.extend({
name : DS.attr("string"),
hostName : DS.attr("string")
});
Resources:
http://emberjs.com/guides/routing/setting-up-a-controller/
http://emberjs.com/guides/getting-started/toggle-all-todos/ (trying to mimic what I learned, but morph the add-new to a new route)
Writing a LightboxView causes problems / Integrating DOM Manipulating jQuery-Plugins makes actions unusable (lightbox "example")
Dependable views in Ember (another lightbox "example" but doesn't have routes for the lightbox opening)
Why does the setupController function need to call controllerFor in order to properly set up the model?
Ember makes URLs a very integral part of its conventions. This means that the state of your application is represented by the route it is on. You've grokked most of this correctly. But there are couple of subtle nuances, that I will clarify below.
First consider an app with the following URLs,
/posts - shows a list of blog posts.
/posts/1 - shows a single blog post.
And say clicking on a post in the list at /posts takes you to /posts/1.
Given this scenario, there 2 ways a user will get to see the post at /posts/1.
By going to /posts and clicking on the 1st post.
By typing in /posts/1, via bookmarks etc.
In both these cases, the PostRoute for /posts/1 will need the model corresponding to Post id 1.
Consider the direct typing scenario first. To provide a way to lookup the id=1 post model, you would use,
model: function(params) {
return App.Post.find(params.post_id);
}
Your template for post will get the model and it can render using it's properties.
Now consider the second scenario. Clicking on post with id=1 takes you to /posts/1. To do this your template would use linkTo like this.
{{#linkTo 'post' post}} {{post.title}} {{/linkTo}}
Here you are passing in the post model to the linkTo helper. It then serializes the data for the post into a URL, ie:- '/posts/1'. When you click on this link Ember realizes that it needs to render the PostRoute but it already has the post model. So it skips the model hook and directly calls setupController.
The default setupController is setup to simply assign the model on the controller. It's implemented to do something like,
setupController: function(controller, model) {
controller.set('model', model);
}
If you do not need to set custom properties on your controller, you don't need to override it. Note: if you are augmenting it with additional properties you still need to call _super to ensure that the default setupController behaviour executes.
setupController: function(controller, model) {
this._super.apply(this, arguments);
controller.set('customProp', 'foo');
}
One final caveat, If you are using linkTo and the route does not have dynamic segments, then the model hook is still called. This exception makes sense if you consider that you were linking to the /posts route. Then the model hook has to fire else Ember has no data to display the route.
Which brings us to the crux of your question. Nearly there, I promise!
In your example you are using linkTo to get to the EventsNewRoute. Further your EventsNewRoute does not have dynamic segments so Ember does call the model hook. And controller.set("model", model); does work in so much as setting the model on the controller.
The issue is to do with your use of renderTemplate. When you use render or {{render}} helper inside a template, you are effectively getting a different controller to the one you are using. This controller is different from the one you set the model on, hence the bug.
A workaround is to pass the controller in the options, which is why renderTemplate gets this controller as an argument.
renderTemplate: function(controller) {
this.render("events-new", {
outlet : "events-new", controller: controller
});
}
Here's an updated jsfiddle.
Final Note: Unrelated to this question, you are getting the warning,
WARNING: The immediate parent route ('application') did not render into the main outlet and the default 'into' option ('events') may not be expected
For that you need to read this answer. Warning, it's another wall of text! :)