ember.js v1.0.0-rc.1 -- Using a modal outlet, how do I route out of the modal on close? - ember.js

I'm attempting to render all my modals through application routing, but I'm having trouble figuring out the proper way to return to a previous state after I dismiss the modal.
Here's the basic setup:
I have an outlet in my application template which I'm using to display modal dialogs.
It looks something like:
{{ outlet modal }}
In my route mappings, I've defined hooks for the individual modal. For instance, my help dialog pops up with:
App.HelpRoute = Ember.Route.extend({
renderTemplate: function(controller, model) {
this.render({ outlet: 'modal' });
}
});
Right now, I can display my modal through the uri:
foo.com/#/help
I have a hook to dismiss the modal using jQuery:
$('#modalHelpWindow').modal('hide');
But this doesn't really help, because I'm just hiding the element. I need to update URI on dismissal. If I hit the same route again, the modal is already hidden, and doesn't display.
What methods should I be using to dismiss the modal, and route the application back to its previous state? Should I just be using history.back()?
Note, I am aware of this SO question, but the solution is not the preferred 'ember' way of doing things, as programmatically created views will not have their controllers associated properly What's the right way to enter and exit modal states with Ember router v2?

Looks like I can hook up the hidden handler to do a history.back() call in my view's didInsertElement method, a la:
didInsertElement: function() {
var $me = this.$();
$me.modal({backdrop:"static"});
$me.on('hidden', function() {
history.back();
});
},

Related

EmberJS - Rendering separate route as a modal into a specific outlet

In my EmberJS applications I have two separates routes as follows,
Route A - "main/books/add"
Route B - "main/authors/add"
I have an "Add Authors" button In Route A template and when a user presses that button I want to load and render Route B in a modal to add new authors.
I know its possible to achieve somewhat smiler to this by using the route's render method to render the Route B template and respective controller.
But in that case, the "model" hook of Route B in "main/authors/add.js" file does not get invoked.
It would be really nice if someone can suggest me a method to render a separate route into a modal.
EDIT - Although this is entirely valid (the premise of rendering into using named outlets, views are now deprecated in Ember 1.1. The same can be achieved by using a Component
Yup, you can do this:
What you'd want to do is create a modal in a template and assign a named outlet into it (or create a view that is a modal with an outlet):
in modal.hbs:
<div class='modal'>
{{outlet "modalContent"}}
</div>
Then I would override your base button like so:
App.BasicButton = Em.View.extend({
context: null,
template: Em.Handlebars.compile('<button>Click Me!</button>');
click: function(evt) {
this.get('controller').send('reroute', this.get('context'));
}
});
And in your template set up your button to trigger your modal:
in trigger.hbs
<!-- content and buttons for doing stuff -->
{{View App.BasicButton context='modalContent'}}
Finally, you want to create a method in your route which handles rendering specific content into your outlet:
App.TriggerRoute = Em.Route.extend({
actions: {
reroute: function(route) {
this.render(route, {into: 'modal', outlet: route});
}
}
});
So in essence, you're rendering the template (called "modalContent") into a specific outlet (called "modalContent"), housed within the template/view (called "modal")
You would also want to write some logic to trigger the modal to open on element insertion. To do that, I would use the didInsertElement action in the modal view:
App.ModalView = Em.View.extend({
didInsertElement: function() {
this.$.css("display", "block");
//whatever other properties you need to set to get the modal to pop up
}
});

Emberjs Modal as a route

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.

Emberjs Modals on a route

So I am trying to figure out how best to put modals on a route, such that you can navigate to them via the url.
application.hbs
{{outlet}}
{{outlet modal}}
There is a discussion here and emberjs cookbook provides another example but nothing covers how you can have modals on a specific route.
The closest thing I have seen is this Stack Overflow question but it suffers from two problems:
When the modal route is visited, the view in the main outlet gets destroyed. So in your UI, things underneath the modal get wiped out.
history.back() is that you essentially revisit that route causing that view to be redrawn and feels very hackish.
This is where I feel a solution would exist but not sure what exactly:
App.MyModalRoute = Ember.Route.extend({
renderTemplate: function(controller, model) {
/**
* When my modal route is visited, render it in the outlet
* called 'modal' but somehow also persist the default outlet.
**/
this.render({ outlet: 'modal' });
}
});
Here is how we handle it:
In the route you use a render piece similar to what you described:
App.UserDeleteRoute = Ember.Route.extend({
renderTemplate: function() {
this.render({
into: 'application',
outlet: 'modal'
});
},
actions: {
closeModel: function() {
this.transitionTo('users.index');
}
},
deactivate: function() {
this.render('empty', {
into: 'application',
outlet: 'modal'
});
}
}
Clearing out outlet on the "deactivate" hook is an important step.
For the main template all you need is:
{{outlet}}
{{outlet "modal"}}
This does not override the default outlet.
One thing you may want to do for all your modals is implement a layout:
App.UserDeleteView = Ember.View.extend({
layoutName: 'settings/modals/layout',
templateName: 'settings/modals/user_delete'
});
Another gotcha is that there needs to be a parent route rendered into the main outlet. If whatever is in that outlet is not the parent of your modal, then the act of moving away from that route will remove it from the outlet.
working JS bin
You've got several options here:
make modal route child of route you want to persist - than either render modal nested in application template, like in example you mentioned, or put it inside its own template id="myModal"
add modal outlet inside persisted route's outlet and render it in renderTemplate method
renderTemplate: function(controller, model) {
this.render(); //render default template
this.render('myModalTemplate', { //render modal to its own outlet
outlet: 'modal',
into: 'myModal', //parent view name. Probably same as route name
controller : controller
});
}
Besides, you can render template with modal in named outlet any moment(on action f.e.), by just calling render method with proper arguments

Routing a modal on top of any route

I'm currently trying to implement a requirement for a routable modal in my ember app. The goal is to have a tabbed modal overlay where each tab has it's own URL (e.g., /settings, /settings/profile, or /settings/billing) but still displayed over whatever route is currently in the background.
A live example might be twitter.com's direct messages modal, but having it so that opening the modal (and clicking links inside the modal) update the app's URL.
The main wall I've hit in trying to implement this is that my outlets get disconnected upon exit of the 'top' route. I've had some initial success with overriding Route#teardownViews but I'm worried about other side-effects.
EDIT: For clarity, I do render the modal in a separate outlet:
SettingsRoute = Ember.Route.extend
renderTemplate: ->
#render
into: 'application'
outlet: 'modal'
Might there be a better approach overall to take here?
This seems to work conceptually (I only tear down the 'main' route's views if I'm not transitioning to a messages or settings route (which I have defined as 'modal' routes)):
Ember.Route.reopen
teardownViews: ->
#_super() unless #controllerFor('application').get('currentTransition.targetName') == 'messages.index'
actions:
willTransition: (transition) ->
controller = #controllerFor('application')
controller.set('currentTransition', transition)
transition.then ->
controller.set('currentTransition', null)
, ->
controller.set('currentTransition', null)
Will post back if I make any other progress.

emberjs didInsertElement is triggered when view is in "preRender"

I have some child views that I'm programmatically pushing into a container view during the didInsertElement event of the container view. I then perform some jquery actions on the rendered child view DOM elements during their didInsertElement events. This setup works fine when I refresh the page. But when I transition into the page via a link-to, the didInsertElement events of the child views are triggered when they are in the "preRender" state, and before they've been inserted into the DOM.
Why would the didInsertElement event get triggered during "preRender"? What would cause the difference in behavior of the child views when refreshing page as opposed to transitioning into page via the router?
I don't know why you receive this problem, but sometimes using the afterRender queue solves the problem.
App.SomeView = Ember.ContainerView.extend({
didInsertElement: function() {
// here your view is in rendered state, but the child views no
Ember.run.scheduleOnce('afterRender', this, this._childViewsRendered);
},
_childViewsRendered: function() {
// here your child views is in the rendered state
}
});
The after render queue is executed when all rendering is finished, so probally your child views will be in the inDOM state instead of preRender.
I hope it helps
While you refresh, the dom gets created and your jquery actions/events works fine and when you transition to a differenct page and come back via the url the jquery events/actions attached to the dom are still there hence they fire before the rendering.
The solution is to clean up the jquery related stuff after the view has been removed from the DOM using willDestroyElement:
App.MyAwesomeComponent = Em.Component.extend({
didInsertElement: function(){
this.$().on('click', '.child .elem', function(){
// do stuff with jQuery
});
},
willDestroyElement: function(){
this.$().off('click');
}
});
for more on this please visit this tutorial