Ember.js - Using the Handlebars helper to detect that a preview has been displayed

There are numerous questions that one way or another ask: "How can I do something after rendering some part of the view?" ( here , here , and here just to give a few). The answer is usually:

  • use didInsertElement to run the code when rendering the view.
  • use Ember.run.next(...) to run your code after changing the type of change if you need to access the created DOM elements.
  • use an observer in isLoaded or a similar property to do something after loading the data.

What annoys this is that it leads to some very awkward looking things like this:

 didInsertElement: function(){ content.on('didLoad', function(){ Ember.run.next(function(){ // now finally do my stuff }); }); } 

And this does not even necessarily work when you use ember-data, because isLoaded may already be true (if the record has already been downloaded before and is not requested again from the server). Therefore, getting the correct ordering is difficult.

In addition, you probably already see isLoaded in your view template as follows:

 {{#if content.isLoaded}} <input type="text" id="myTypeahead" data-provide="typeahead"> {{else}} <div>Loading data...</div> {{/if}} 

and re-executing in your controller is like duplication.

I came up with a slightly new solution, but it either needs work, or is actually a bad idea ... any case could be true:

I wrote a small Handlebars helper called {{fire}} that will fire an event with a custom name when executing the containing descriptor template (i.e., should it be every time the view is re-viewed?).

Here is my earliest 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 as follows:

 {{#if content.isLoaded}} {{fire typeaheadHostDidRender}} <input type="text" id="myTypeahead" data-provide="typeahead"> {{else}} <div>Loading data...</div> {{/if}} 

This essentially works as it is, but it has a few drawbacks that I already know about:

  • It calls the method on the controller ... it would probably be better, at least as far as possible, to send an "event" to the ancestor's view object, perhaps even to make this the default behavior. I tried {{fire typeaheadHostDidRender target="view"}} and it did not work. I still don’t see how to get the “current” view from what is passed to the helper, but obviously the {{view}} helper can do this.
  • I suppose there is a more formal way to trigger a custom event than what I am doing here, but so far I have not found out. jQuery .trigger() does not seem to work on controller objects, although it can work with views. Is there an Ember way to do this?
  • There may be things that I don’t understand, for example, the case when this event is fired, but the view will not actually be added to the DOM ...?

As you might have guessed, I am using the Bootstrap Typeahead control, and I need to connect it after rendering the <input> , which actually happens only after several nested blocks {{#if}} are evaluated as true in my template, I also use jqPlot, so I often come across the need for this template. This seems like a viable and useful tool, but maybe I am missing something of a large image that makes this approach stupid. Or maybe there is another way to do this that did not appear in my searches?

Can someone improve this approach for me or tell me why this is a bad idea?

UPDATE

I calculated a few bits:

  • I can get the first "real" containing view with options.data.view.get('parentView') ... obviously possible, but I did not think that everything would be so simple.
  • In fact, you can create the obj.trigger(evtName) jQuery style for any arbitrary object ... but the object should extend Ember.Evented mixing! So, I suppose this is the right way to accomplish such an event by sending to Ember. Just make sure the intended target extends Ember.Evented (the views are already running).

Here's an improved version:

 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, almost all I am missing is figuring out how to convey the intended goal (for example, a controller or view - the code above is trying to guess). Or, figuring out if there is any unexpected behavior that violates the whole concept.

Any other input?

+8
Dec 07
source share
1 answer

UPDATED

Updated for Ember 1.0 final, I am currently using this code on Ember 1.3.1.

OK, I think I get it. Here's the "full" helper helper:

 Ember.Handlebars.registerHelper('trigger', function (evtName, options) { // See http://stackoverflow.com/questions/13760733/ember-js-using-a-handlebars-helper-to-detect-that-a-subview-has-rendered // for known flaws with this approach var options = arguments[arguments.length - 1], hash = options.hash, hbview = options.data.view, concreteView, target, controller, link; concreteView = hbview.get('concreteView'); if (hash.target) { target = Ember.Handlebars.get(this, hash.target, options); } else { target = concreteView; } Ember.run.next(function () { var newElements; if(hbview.morph){ newElements = $('#' + hbview.morph.start).nextUntil('#' + hbview.morph.end) } else { newElements = $('#' + hbview.get('elementId')).children(); } target.trigger(evtName, concreteView, newElements); }); }); 

I changed the name from {{fire}} to {{trigger}} to more closely match the Ember.Evented / jQuery convention. This updated code is based on the built-in Ember {{action}} helper and should accept any target="..." argument in your template, as {{action}} does. Where it differs from {{action}} (except for automatic triggering when rendering a section of a template):

  • By default, an event is sent to the view. Sending to a route or controller by default will not make much sense, since it should probably be primarily used for view-oriented actions (although I often use it to send events to the controller).
  • It uses Ember.Evented style events, therefore, to send an event to an arbitrary object without viewing (including the controller), the object must extend Ember.Evented and must have a registered listener. (To be clear, it does not call something in the actions: {…} hash actions: {…} !)

Note: if you send an event to an Ember.View instance, all you have to do is implement the method with the same name (see docs , code ). But if your goal is not a view (for example, a controller), you must register the listener on an object with the extension obj.on('evtName', function(evt){...}) or Function.prototype.on .

So here is an example of the real world. I have a view with the following pattern using Ember and Bootstrap:

 <script data-template-name="reportPicker" type="text/x-handlebars"> <div id="reportPickerModal" class="modal show fade"> <div class="modal-header"> <button type="button" class="close" data-dissmis="modal" aria-hidden="true">&times;</button> <h3>Add Metric</h3> </div> <div class="modal-body"> <div class="modal-body"> <form> <label>Report Type</label> {{view Ember.Select viewName="selectReport" contentBinding="reportTypes" selectionBinding="reportType" prompt="Select" }} {{#if reportType}} <label>Subject Type</label> {{#unless subjectType}} {{view Ember.Select viewName="selectSubjectType" contentBinding="subjectTypes" selectionBinding="subjectType" prompt="Select" }} {{else}} <button class="btn btn-small" {{action clearSubjectType target="controller"}}>{{subjectType}} <i class="icon-remove"></i></button> <label>{{subjectType}}</label> {{#if subjects.isUpdating}} <div class="progress progress-striped active"> <div class="bar" style="width: 100%;">Loading subjects...</div> </div> {{else}} {{#if subject}} <button class="btn btn-small" {{action clearSubject target="controller"}}>{{subject.label}} <i class="icon-remove"></i></button> {{else}} {{trigger didRenderSubjectPicker}} <input id="subjectPicker" type="text" data-provide="typeahead"> {{/if}} {{/if}} {{/unless}} {{/if}} </form> </div> </div> <div class="modal-footer"> <a href="#" class="btn" data-dissmis="modal">Cancel</a> <a href="#" {{action didSelectReport target="controller"}} class="btn btn-primary">Add</a> </div> </div> </script> 

I needed to know when this element was available in the DOM, so I could attach a file like:

 <input id="subjectPicker" type="text" data-provide="typeahead"> 

So, I put the {{trigger}} helper in one block:

 {{#if subject}} <button class="btn btn-small" {{action clearSubject target="controller"}}>{{subject.label}} <i class="icon-remove"></i></button> {{else}} {{trigger didRenderSubjectPicker}} <input id="subjectPicker" type="text" data-provide="typeahead"> {{/if}} 

And then didRenderSubjectPicker implemented in my view class:

 App.ReportPickerView = Ember.View.extend({ templateName: 'reportPicker', didInsertElement: function () { this.get('controller').viewDidLoad(this); } , didRenderSubjectPicker: function () { this.get('controller').wireTypeahead(); $('#subjectPicker').focus(); } }); 

Done! Typeahead now connects when (and only when) the template sub-channel is finally displayed. Note the difference in utility, didInsertElement used when the main (or perhaps "specific" is the right term) is rendered, and didRenderSubjectPicker started when the submode of the view is displayed.

If I wanted to send the event directly to the controller instead, I would just modify the template to read:

 {{trigger didRenderSubjectPicker target=controller}} 

and do it in my controller:

 App.ReportPickerController = Ember.ArrayController.extend({ wireTypeahead: function(){ // I can access the rendered DOM elements here }.on("didRenderSubjectPicker") }); 

Done!

The only caveat is that this may happen again when the view section is already on the screen (for example, if the parent view is re-displayed). But in my case, starting typeahead initialization again is fine anyway, and it would be pretty easy to detect and code if needed. And this may be desirable in some cases.

I release this code as a public domain, no guarantees or responsibilities were accepted. If you want to use this, or Ember people want to include it in the baseline, go straight ahead! (Personally, I think it would be a great idea, but this is not surprising.)

+16
Dec 13 '12 at 10:28
source share
— -



All Articles