There are numerous questions that ask in one way or another: "How do I do something after some part of a view is rendered?" (here, here, and here just to give a few). The answer is usually:
- use
didInsertElementto run code when a view is initially rendered. - use
Ember.run.next(...)to run your code after the view changes are flushed, if you need to access the DOM elements that are created. - use an observer on
isLoadedor a similar property to do something after the data you need is loaded.
What's irritating about this is, it leads to some very clumsy looking things like this:
didInsertElement: function(){
content.on('didLoad', function(){
Ember.run.next(function(){
// now finally do my stuff
});
});
}
And that doesn't really even necessarily work when you're using ember-data because isLoaded may already be true (if the record has already been loaded before and is not requested again from the server). So getting the sequencing right is hard.
On top of that, you're probably already watching isLoaded in your view template like so:
{{#if content.isLoaded}}
<input type="text" id="myTypeahead" data-provide="typeahead">
{{else}}
<div>Loading data...</div>
{{/if}}
and doing it again in your controller seems like duplication.
I came up with a slightly novel solution, but it either needs work or is actually a bad idea...either case could be true:
I wrote a small Handlebars helper called {{fire}} that will fire an event with a custom name when the containing handlebars template is executed (i.e. that should be every time the subview is re-rendered, right?).
Here is my very early attempt:
Ember.Handlebars.registerHelper('fire', function (evtName, options) {
if (typeof this[evtName] == 'function') {
var context = this;
Ember.run.next(function () {
context[evtName].apply(context, options);
});
}
});
which is used like so:
{{#if content.isLoaded}}
{{fire typeaheadHostDidRender}}
<input type="text" id="myTypeahead" data-provide="typeahead">
{{else}}
<div>Loading data...</div>
{{/if}}
This essentially works as is, but it has a couple of flaws I know of already:
- It calls the method on the controller...it would probably be better to at least be able to send the "event" to the ancestor view object instead, perhaps even to make that the default behavior. I tried
{{fire typeaheadHostDidRender target="view"}}and that didn't work. I can't see yet how to get the "current" view from what gets passed into the helper, but obviously the{{view}}helper can do it. - I'm guessing there is a more formal way to trigger a custom event than what I'm doing here, but I haven't learned that yet. jQuery's
.trigger()doesn't seem to work on controller objects, though it may work on views. Is there an "Ember" way to do this? - There could be things I don't understand, like a case where this event would be triggered but the view wasn't in fact going to be added to the DOM...?
As you might be able to guess, I'm using Bootstrap's Typeahead control, and I need to wire it after the <input> is rendered, which actually only happens after several nested {{#if}} blocks evaluate to true in my template. I also use jqPlot, so I run into the need for this pattern a lot. This seems like a viable and useful tool, but it could be I'm missing something big picture that makes this approach dumb. Or maybe there's another way to do this that hasn't shown up in my searches?
Can someone either improve this approach for me or tell me why it's a bad idea?
UPDATE
I've figured a few of the bits out:
- I can get the first "real" containing view with
options.data.view.get('parentView')...obvious perhaps, but I didn't think it would be that simple. - You actually can do a jQuery-style
obj.trigger(evtName)on any arbitrary object...but the object must extend theEmber.Eventedmixin! So that I suppose is the correct way to do this kind of event sending in Ember. Just make sure the intended target extendsEmber.Evented(views already do).
Here's the improved version so far:
Ember.Handlebars.registerHelper('fire', function (evtName, options) {
var view = options.data.view;
if (view.get('parentView')) view = view.get('parentView');
var context = this;
var target = null;
if (typeof view[evtName] == 'function') {
target = view;
} else if (typeof context[evtName] == 'function') {
target = context;
} else if (view.get('controller') && typeof view.get('controller')[evtName] == 'function') {
target = view.get('controller');
}
if (target) {
Ember.run.next(function () {
target.trigger(evtName);
});
}
});
Now just about all I'm missing is figuring out how to pass in the intended target (e.g. the controller or view--the above code tries to guess). Or, figuring out if there's some unexpected behavior that breaks the whole concept.
Any other input?
didInsertElementevent on each subview explicitly. But that really seems like overkill, just for something that otherwise only needs{{#if isLoaded}}to work. But granted that might be a better option if the "subviews" are more complex.