Routing a modal on top of any route - ember.js

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.

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
}
});

Rendering a form inside a bootstrap modal in Ember

There are plenty of questions already that ask about modals in Ember (like this one and this one). Even the cookbook has an article that explains how to use modals, but none of these cover form submissions that require validation from the server within the modal (ie, username already taken).
Following along with the cookbook, I have a named outlet in my application template.
{{outlet}}
{{outlet modal}}
And an action that triggers rendering a template inside the modal outlet (using bootstrap).
App.ApplicationRoute = Ember.Route.extend
actions:
openModal: (template, model) ->
#controllerFor(template).set('model', model)
#render template, into: 'application', outlet: 'modal'
closeModal: ->
#render '', into: 'application', outlet: 'modal'
Obviously I'm calling this action from within a link.
<a href="#" {{action openModal "person.edit" model}}>Edit</a>
Where model is the model of the current route.
In the PesonEditView I hook into the didInsertElement function to enable the bootstrap modal. Inside this hook, I also listen to the hidden.bs.modal event fired by bootstrap when the close button is clicked to clear out the modal outlet.
App.PersonEditView = Ember.View.extend
didInsertElement: ->
#$('.modal').modal('show').on 'hidden.bs.modal', =>
#get('controller').send('closeModal')
My question is, how can I define a save action that will close the modal (using bootstraps animations) after it has validated on the server? The sequence of events that I see are required are, 1) save gets triggered on controller, 2) if successful save, close the modal (which would require calling #$('.modal').modal('hide') from the view).
I'm not sure what Ember experts would suggest here, since it seems as though the view and controller will need to communicate very closely.
Thanks!
EDIT
In response to edpaez's comment, I have tried resolving the promise returned by save from within the view. For example, in my template
<button type="button" class="btn btn-primary" {{action "save" target="view"}}>Save</button>
The view
App.PersonEditView = Ember.View.extend
actions:
save: ->
#get('controller').send('save').then(#closeModal.bind(#))
closeModal: ->
#$('.modal').modal('hide')
# the rest omitted for brevity
And the controller
App.PersonEditController = Ember.ObjectController.extend
actions:
save: ->
#get('model').save()
I get the following error
Uncaught TypeError: Cannot call method 'then' of undefined
Try targeting the save action to the view:
<button type="button" class="btn btn-primary" {{action "save" target="view"}}>Save</button>
and call a save method in the controller. That method would return a promise that would resolve when the save method from the model resolves.
App.PersonEditView = Ember.View.extend
actions:
save: ->
#get('controller').save().then(#closeModal.bind(#))
closeModal: ->
#$('.modal').modal('hide')
App.PersonEditController = Ember.ObjectController.extend
save: ->
#get('model').save()
This way, the controller abstracts the model-saving logic and the view gets notified when the model saves so it can behave as expected.
Make sure you call the save method in the controller instead of sending an action. the send method returns nothing.
Hope it helps!

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

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

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();
});
},

designing a custom component/view on emberjs

I'm having trouble doing things on Ember, and I'm sure it's because I still haven't fully grasped "the Ember way" of doing things, and I'm a trying to do a couple of things that get out of the scope of standard tutorials out there.
I'm developing some sort of textfield-with-suggestions component that will appear in every single page of my web app. I won't ask here all the details on how to do it, but just a couple specific things I am having trouble accomplishing from the start. The following are the relevant code fragments of what I have so far.
// handlebars template: searchbar.hbs
{{view App.SearchField viewName="searchField"}}
<div class="results" {{bindAttr class="searchField.hasFocus"}}>
This is where the results for whatever the user has typed are shown.
</div>
// coffeescript source of the views: searchbar.coffee
App.SearchBar: Ember.View.extend
templateName: 'searchbar'
App.SearchField: Ember.TextField.extend
placeholder: 'Search'
hasFocus: false
eventManager: Ember.Object.create
focusIn: (event, view) ->
#set('hasFocus', true)
focusOut: (event, view) ->
#set('hasFocus', false)
// somewhere in the layout of the pages in my app
<div id="header">
{{App.SearchBar}}
</div>
This will probably also need a controller, but I haven't developed it yet, because I don't know where it fits within this setup yet.
First, I want the suggestions popup panel to appear as soon as the search field obtains focus. That's the reason of my attempt above to implement a hasFocus property on the searchfield. But how do I achieve making my div.results panel react to the focus state of the input field?
In general, and this is the core of my question here, how do I connect all things to develop this component? If the answer is by attaching it to a controller, then how do I setup a controller for this component, and how do I specify that it is the controller for it, so that it acts as a context for everything?
I think you have to clearly separate concerns. Stuff related to the view (ie manipulating the DOM with jquery) should stay in the view. Stuff related to the application state should be in the controller. Though,in your case, I think you can simply bind an observer on the hasFocus property, and show the suggestions. Something like:
App.SearchField: Ember.TextField.extend
placeholder: 'Search'
hasFocus: false
eventManager: Ember.Object.create
focusIn: (event, view) ->
#set('hasFocus', true)
focusOut: (event, view) ->
#set('hasFocus', false)
focusDidChange: (->
if #hasFocus
$('.results')... // here I let you do the suggestion stuff
// based on data retrieved from the controller
else
// probably hide the results div.
).observes('hasFocus')