So I have this issue where Ember will not render my view more than once, even after I have destroyed it.
The code I have, works without using components, so it is probably some issue with the actual view not being destroyed properly.
I render into an outlet in my ApplicationRoute
App.ApplicationRoute = Em.Route.extend({
actions: {
showModal: function() {
// This does not work the second time:
this.render('modal', {
into: 'application',
outlet: 'modal'
});
}
}
});
I set up an event listener for when the Bootstrap modal is hidden
App.BaseModalComponent = Em.Component.extend({
afterRenderEvent: function() {
var self = this;
this.$('.modal')
.on('hidden.bs.modal', function(){
// I am destroying the component,
// when the modal is hidden
self.destroy();
})
.modal();
}
});
The afterRenderEvent is a listener I have attached to the view's afterRender event.
See here for markup, etc.: http://emberjs.jsbin.com/wolicutiwiro/1/edit
A working example without using components: http://emberjs.jsbin.com/lodamojikaqo/1/edit
Check this JSBin It does what you want.
App.ApplicationRoute = Em.Route.extend({
actions: {
showModal: function() {
// This does not work the second time:
this.render('modal', {
into: 'application',
outlet: 'modal'
});
},
closeModal: function() {
console.log("closing modal");
return this.disconnectOutlet({
outlet: 'modal',
parentView: 'application'
});
}
}
});
I believe the main challenge is that I cannot call a closeModal action
from a button in my modal view. Bootstrap itself handles hiding the
modal, but I need to disconnect the outlet to allow the same or
another modal to render.
In order to call this action from your component, you have to send the action from the component to the controller current templates controller:
App.BaseModalComponent = Em.Component.extend({
afterRenderEvent: function() {
var self = this;
this.$('.modal')
.on('hidden.bs.modal', function(){
self.sendAction('action');
})
.modal();
},
});
And when you use your component, make sure to assign the action name:
<script type="text/x-handlebars" data-template-name="modal">
{{#base-modal action='closeModal'}}
<p>One fine body…</p>
{{/base-modal}}
</script>
The action will bubble from the controller to the route. This solution allows you to use bootstrap exactly as is, but I find that the solution Code Jack suggested to be much more Ember.
Related
I am following the Ember cookbook for rendering a route into a modal here: http://emberjs.com/guides/cookbook/user_interface_and_interaction/using_modal_dialogs/. This works, but I am not sure how to pass variables to my rendered view.
Specifically I want to load a 'users/filters' route into the modal, which has access to a jobTitles array. This is defined in my application route simply as this.store.find('jobTitle'). The problem is that this does not seem to be accessible from the users/filters controller or template. The users/filters route doesn't seem to be run at all because I am using the render method as follows:
App.ApplicationRoute = Ember.Route.extend({
actions: {
openModal: function(modalName) {
return this.render(modalName, {
into: 'application',
outlet: 'modal'
});
}
}
});
How can I pass this into the rendered modal? Many thanks.
One possibility would be to pass a controller to the modal rendering function:
App.ApplicationRoute = Ember.Route.extend({
actions: {
openModal: function(modalName, controller) {
return this.render(modalName, {
into: 'application',
outlet: 'modal',
controller: controller
});
}
}
});
With the above code call the openModal hook in your route's template and pass the controller name (the name, not the controller itself) of the route to it. This way you should be able to access all properties of the controller.
I have a component nested several levels down in other components. I'm trying to propagate an action all the way up to the AppController in order to open a modal.
The only way I know of doing this is to pass in the action to each component - but this seems extremely impractical. Is there a better way to access the AppController from a nested component?
See my jsbin for the code
App.IndexRoute = Ember.Route.extend({
model: function() {
return ['red', 'yellow', 'blue'];
}
});
App.AppController = Ember.Controller.extend({
actions: {
openModal: function(){
alert('this would open the modal')
}
}
})
App.MainComponentComponent = Ember.Component.extend({})
App.SubComponentComponent = Ember.Component.extend({
actions: {
triggerModal: function(){
// need to trigger the openModal action on the AppController
this.sendAction('openModal')
}
}
})
.
<script type="text/x-handlebars" data-template-name="index">
<h1>Index</h1>
{{main-component model=model}}
</script>
<script type="text/x-handlebars" data-template-name="components/main-component">
<h2>Main component</h2>
{{#each color in model}}
{{sub-component color=color}}
{{/each}}
</script>
<script type="text/x-handlebars" data-template-name="components/sub-component">
<button {{action "triggerModal"}}>{{color}}</button>
</script>
EDIT: I'm aware that I can render a template into the modal outlet:
this.render(modalName, {
into: 'application',
outlet: 'modal'
});
But I'm trying to access an action on the AppController.
You can utilize Ember.Instrumentation module, which can be used like a pub/sub.
Here is a working JS Bin example.
Solution outline:
1. On ApplicationController init, the controller subscribes to "openModal" event.
2. The neseted component instruments the event "openModal" within an action.
3. The instrumentation can be executed with a payload, so this would be the place to determine the modal content.
App.ApplicationController = Ember.Controller.extend({
actions: {
openModal: function(options) {
alert('this would open the modal with the content: ' + options.modalContent);
}
},
subscribeEvents: function() {
this.set('openModalSubscriber', Ember.Instrumentation.subscribe('openModal', {
before: Ember.K,
after: Ember.run.bind(this, function(name, timestamp, payload, beforeRet) {
this.send('openModal', payload);
}),
}, this));
}.on('init')
});
App.SubComponentComponent = Ember.Component.extend({
actions: {
triggerModal: function() {
Ember.Instrumentation.instrument('openModal.sub-component', {
modalContent: 'Inner content of modal'
}, Ember.K, this);
}
}
});
Components are supposed to be pretty isolated, therefore it probably doesn't make sense to be jumping over other components, going straight to their controllers... See the following discussion here
There is a targetObject property, which might be of use to you, although I am not 100% sure what you would set it to in this case.
I have routes such that,
Router.map(function() {
this.resource('board', { path: '/boards/:board_id' }, function() {
this.route('calendar');
this.route('attachments');
this.resource('card', { path: '/cards/:card_id' });
});
});
and the board template has two outlets, one is the default, and the other is named outlet.
/templates/board.hbs
<div id="master-content">
{{outlet}}
</div>
<div id="detail-content">
{{outlet 'detail'}}
</div>
/routes/card.js
App.Card = Ember.Route.extend({
renderTemplate: function(controller, model) {
this.render('card', { outlet: 'detail', controller: controller, into: 'board' });
}
});
When I transition to board.index or board.calendar or board.attachments, their templates will be displayed in the default outlet and I want to display the card template into the outlet named 'detail'.
Here I have a question. Based on how Ember works in general, when I move to card route, the card template will be into detail outlet, but the other default outlet will become empty. I'd like to keep the default outlet as it was when I move to the card route.
My first approach is to have a controller that stores the information of what was in the default outlet and render them again whenever I move to card route.
Any best practices about this situations?
I personally have never used Query Parameters in Ember (They're still in experimental and available in canary builds) but I believe they are a good fit for you.
http://emberjs.com/guides/routing/query-params/
You can use the full transition in the route (here) to have the templates rendered according to the values from query parameters.
App.CardRoute = Ember.Route.extend({
model: function(params) {
this.set('calendar', params.calendar); //assuming calendar it's the name of your query param
this.set('attachements', params.attachements); //assuming calendar it's the name of your query param
},
renderTemplate: function(controller, model) {
this.render('card', { outlet: 'detail', controller: controller, into: 'board' });
if (this.get('calendar')) {
this.render('calendar', { controller: 'calendar', into: 'board' });
} else if (this.get('attachements')) {
this.render('attachements', { controller: 'attachements', into: 'board' });
}
},
actions: {
queryParamsDidChange: function() {
// This is how we opt-in to
// a full-on transition that'll
// refire the `model` hook and
// give us a chance to reload data
// from the server.
this.refresh();
}
}
});
Your other option would be to have the card resource as a subroute of both calendar and attachments routes.
I hope this helps you!
From my router, I'm rendering a view:
App.MonthSummaryRoute = Ember.Route.extend({
events: {
selectTab: function(name) {
this.render(name, { into: 'month/summary', outlet: 'tab' });
}
}
});
As an example, name is "summaryCompany". If I add a
<script type="text/x-handlebars" data-template-name="summaryCompany">
<h2>Test template</h2>
</script>
this template displays. But I tried to add a view to handle the events:
App.SummaryCompanyView = Ember.View.extend({
didInsertElement: function() {
console.log('here');
}
});
and I'm not getting anything. What am I missing?
Could you provide your entire code selection, or a JSBin / JSFiddle?
Possible approaches:
What's in your month/summary template / route / view?
Maybe you can't call render from an event. What happens when instead of doing the render from inside selectTab you do it from the route's renderTemplate hook?
renderTemplate: function() { this.render("summaryCompanyView", { into: 'month/summary', outlet: 'tab' }); }
You can try seeing if the view is inserted at all: in web inspector, find the ember-id of the div corresponding to view (somethign like <div id="ember310" ...>, then access the actual view object via Ember.Views.views.ember310 (or whatever id). You can check the view's class and see if it's App.SummaryCompanyView or a generic Ember.View
Lastly, what happens if you remove the inlined-template and specify the template on the View object via templateName?
My page layout (application template) looks like this (simplified):
I use it for different routes (offer list + offer detail, customer list + customer detail). Lists are shown in the sub-navigation outlet.
My router's code:
App.Router.map(function () {
//...
this.resource('offers', function () {
this.resource('offer', { path: '/:offer_id' });
});
}
My Routes:
App.OffersRoute = Ember.Route.extend({
model: function () {
return App.Offer.find();
},
renderTemplate: function (controller, model) {
this.render('offer-list', {
into: 'application', outlet: 'sub-navigation', controller: 'offers' });
this.render('offer-list-title', { into: 'application', outlet: 'page-title' });
this.render('offer-list-content', { into: 'application' });
}
});
App.OfferRoute = Ember.Route.extend({
model: function (params) {
return App.Offer.find(params.offer_id);
},
renderTemplate: function () {
this.render('offer-title', { into: 'application', outlet: 'page-title' });
this.render('offer-content', { into: 'application' });
}
});
Now this works so far.
http://.../#/offers
shows the list and the title "Offer summary" and static html content. I click on one of the offers in the list, going to
http://.../#/offers/23
all okay: it still shows the list of offers in the sub-navigation area and the correct title and the content of the offer.
Now my problem:
If I return to the
http://.../#/offers
page (using a #linkTo helper on a menu), then the {{outlet}} / content area becomes empty (not the static html from before) and the title is still the title in {{page-title}} of the offer/23 route.
How can I let my app "re-render" the template as defined in the OffersRoute renderTemplate()?
P.S.: I'm using Ember.js 1.0.0-RC.3
Using the built-in Index routes and maintaining the ApplicationRoute -> OffersRoute -> OfferRoute hierarchy will solve your issue.
If you turn on the router transition logging you will see that when navigating to Offers you are actually entering the Offers.Index route:
App = Ember.Application.create({
LOG_TRANSITIONS: true
});
This means that you can set your static Offers title and set the static Offers content in OffersIndexRoute and it will be correctly set the first time and set again if you link back to it from inside of an offer detail page. For this to work you also must preserve the ApplicationRoute -> Offers -> Offer {{outlet}} hierarchy by not directly rendering everything into the ApplicationRoute's {{outlet}}. The reason you must preserve this hierarchy is that by rendering the child (Offer template) directly into the Application template you remove the Offers template and when you try to go back to the OffersRoute its template has been removed and it shows nothing.
Index route
Use OffersIndexRoute to fill in the ApplicationRoute's {{outlet}} and the {{outlet page-title}}.
JS:
//this renders the title and the main content for Offers
App.OffersIndexRoute = Ember.Route.extend({
renderTemplate: function (controller, model) {
this.render('offer-list-title', { into: 'application', outlet: 'page-title' });
this.render();
}
});
App.OffersRoute = Ember.Route.extend({
model: function () {
return App.Offer.find();
},
renderTemplate: function (controller, model) {
this.render('offer-list', {
into: 'application', outlet: 'sub-navigation', controller: 'offers' });
// render this in OffersIndexRoute instead
//this.render('offer-list-title', { into: 'application', outlet: 'page-title' });
this.render('offer-list-content', { into: 'application' });
}
});
Handlebars:
<script type="text/x-handlebars" data-template-name="offer-list-content">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="offers/index">
Offers content
</script>
The outlet in the offers-list-content will be filled in by the OffersIndexRoute or by the Offer template, depending on what the current route is.
Maintaining {{outlet}} hierarchy
Allow the OfferRoute to render it's content template into the OffersRoute template instead of forcing it into the ApplicationRoute.
App.OfferRoute = Ember.Route.extend({
model: function (params) {
return App.Offer.find(params.offer_id);
},
renderTemplate: function () {
this.render('offer-title', { into: 'application', outlet: 'page-title' });
// preserve the hierarchy and render into the Offers {{outlet}}, which is the default
//this.render('offer-content', { into: 'application' });
this.render('offer-content');
}
});
Working JSBin example