Ember.js - Using a Handlebars helper to detect that a subview has rendered - ember.js

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 didInsertElement to 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 isLoaded or 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 the Ember.Evented mixin! So that I suppose is the correct way to do this kind of event sending in Ember. Just make sure the intended target extends Ember.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?

UPDATED
Updated for Ember 1.0 final, I'm currently using this code on Ember 1.3.1.
Okay, I think I got it all figured out. Here's the "complete" handlebars 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 Ember.Evented/jQuery convention. This updated code is based on the built-in Ember {{action}} helper, and should be able to accept any target="..." argument in your template, just as {{action}} does. Where it differs from {{action}} is (besides firing automatically when the template section is rendered):
Sends the event to the view by default. Sending to the route or controller by default wouldn't make as much sense, as this should probably primarily be used for view-centric actions (though I often use it to send events to a controller).
Uses Ember.Evented style events, so for sending an event to an arbitrary non-view object (including a controller) the object must extend Ember.Evented, and must have a listener registered. (To be clear, it does not call something in the actions: {…} hash!)
Note that if you send an event to an instance of Ember.View, all you have to do is implement a method by the same name (see docs, code). But if your target is not a view (e.g. a controller) you must register a listener on the object with obj.on('evtName', function(evt){...}) or the Function.prototype.on extension.
So here's a real-world example. I have a view with the following template, 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">×</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">
Cancel
Add
</div>
</div>
</script>
I needed to know when this element was available in the DOM, so I could attach a typeahead to it:
<input id="subjectPicker" type="text" data-provide="typeahead">
So, I put a {{trigger}} helper in the same 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 implemented didRenderSubjectPicker 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! Now the typeahead gets wired when (and only when) the sub-section of the template is finally rendered. Note the difference in utility, didInsertElement is used when the main (or perhaps "concrete" is the proper term) view is rendered, while didRenderSubjectPicker is run when the sub-section of the view is rendered.
If I wanted to send the event directly to the controller instead, I'd just change the template to read:
{{trigger didRenderSubjectPicker target=controller}}
and do this in my controller:
App.ReportPickerController = Ember.ArrayController.extend({
wireTypeahead: function(){
// I can access the rendered DOM elements here
}.on("didRenderSubjectPicker")
});
Done!
The one caveat is that this may happen again when the view sub-section is already on screen (for example if a parent view is re-rendered). But in my case, running the typeahead initialization again is fine anyway, and it would be pretty easy to detect and code around if need be. And this behavior may be desired in some cases.
I'm releasing this code as public domain, no warranty given or liability accepted whatsoever. If you want to use this, or the Ember folks want to include it in the baseline, go right ahead! (Personally I think that would be a great idea, but that's not surprising.)

Related

How can I add a class in ember js

<script type="text/x-handlebars">
<div class="wrapper">
<div class="sideMenu">
{{#link-to 'home'}}Home{{/link-to}}
{{#link-to 'posts'}}Posts{{/link-to}}
</div>
<div class="content">
{{outlet}}
</div>
</div>
</script>
I am new to ember js. How can I add a class on 'content' class each time when view changes.
We do something like this:
Ember.Route.reopen({
activate: function() {
var cssClass = this.toCssClass();
// you probably don't need the application class
// to be added to the body
if (cssClass !== 'application') {
Ember.$('body').addClass(cssClass);
}
},
deactivate: function() {
Ember.$('body').removeClass(this.toCssClass());
},
toCssClass: function() {
return this.routeName.replace(/\./g, '-').dasherize();
}
});
It would add a class to the body (in your case just use content), that is the same as the current route.
#torazaburo had some excellent points about #Asgaroth (accepted) answer, but I liked the idea of not having to write this same functionality over and over again. So, what I am providing below is a hybrid of the two solutions plus my own two cents and I believe it addresses #torazaburo concerns regarding the accepted answer.
Let's start with the 2nd point:
I also don't like the idea of polluting Ember.Route
Can you pollute Ember.Route without polluting Ember.Route? (Huh?) Absolutely! :) Instead of overwriting activate, we can write our own function and tell it to run .on(activate) This way, our logic is run, but we are not messing with the built-in/inherited activate hook.
The accepted answer is very procedural, imperative, jQuery-ish, and un-Ember-like.
I have to agree with this as well. In the accepted answer, we are abandoning Ember's data binding approach and instead fall back on the jQuery. Not only that, we then have to have more code in the deactivate to "clean up the mess".
So, here is my approach:
Ember.Route.reopen({
setContentClass: function(){
this.controllerFor('application').set("path", this.routeName.dasherize());
}.on('activate')
});
We add our own method to the Ember.Route class without overwriting activate hook. All the method is doing is setting a path property on the application controller.
Then, inside application template, we can bind to that property:
<div {{bind-attr class=":content path"}}>
{{outlet}}
</div>
Working solution here
Just bind the currentPath property on the application controller to the class of the element in the template:
<div {{bind-attr class=":content currentPath"}}>
{{outlet}}
</div>
In case you're not familiar with the {{bind-attr class= syntax in Ember/Handlebars:
the class name preceded with a colon (:content) is always added to the element
properties such as currentPath result in the current value of that property being inserted as a class, and are kept dynamically updated
To be able to access currentPath in a template being driven by a controller other than the application controller, first add
needs: ['application']
to the controller, which makes the application controller available under the name controllers.application, for use in the bind-attr as follows:
<div {{bind-attr class=":content controllers.application.currentPath"}}>
You may use currentRouteName instead of or in addition to currentPath if that works better for you.
The class name added will be dotted, such as uploads.index. You can refer to that in your CSS by escaping the dot, as in
.uploads\.index { }
Or, if you would prefer dasherized, add a property to give the dasherized path, such as
dasherizedCurrentPath: function() {
return this.('currentPath').replace(/\./g, '-');
}.property('currentPath')
<div {{bind-attr class=":content dasherizedCurrentPath"}}>
This has been tested in recent versions of ember-cli.

Accessing model data in router when using a view

I am looking for a way to access model data in a route when using a view to display model attributes.
Example
Template
<h2>New post</h2>
<form {{action save model on="submit"}}>
<label>Title</label>
{{input type="text" value=title placeholder="title" id="title"}}
<label>Text</label>
{{view "tinymce" value=text }}
<button>Post</button>
</form>
View Template
<textarea id="tinymce">
</textarea>
View
export default Ember.View.extend({
templateName: 'views/tinymce-textarea',
didInsertElement: function() {
tinymce.EditorManager.execCommand('mceRemoveEditor',true, 'tinymce');
tinymce.EditorManager.execCommand('mceAddEditor',true, 'tinymce');
}
});
Router
export default Ember.Route.extend({
....
actions : {
save : function(model) {
if (!model.get('title').trim() || !model.get('text').trim()) {
return;
}
model.save().then(this.onSuccessfulSave.bind(this), this.onFailedSave.bind(this));
}
}
});
Now, obviously this doesn't work, since model.text is never bound in the view, like it would be if I were to use the textarea template helper:
{{textarea value=text placeholder="text" id="text"}}
But this is just one of many (many) ways I have tried to get this to work, and I am at a complete loss as how one would access model attributes in the route when using a view. And it does seem like a pretty common usecase to me too.
I have failed to find information regarding this on SO or anywhere else, so if anyone is able to help me, thanks in advance! / AS.
So one of the main things that you're missing out is binding the view to the controller. This is actually really straight forward to do, but without it Ember doesn't know that it should propagate changes between the two. The first thing I would do is this:
{{view "tinymce" valueBinding="text" }}
This says that the views value will be binded to the controller's text value. Whenever view's value is updated, it will propogate to the controller and vice versa.
The next item to take care of is actually binding the value in the view. All you need to do is tell the input to bind it's value to the view's value. This can be done like this
{{textarea value="view.value" placeholder="text" id="text"}}
Try this out, and you can use this jsbin that I created as an example:
http://emberjs.jsbin.com/qanudu/26/
If you have any other questions just let me know, but this should solve your issues!

getting back reference to a specific model using Ember's Array Controller

I'm new to Ember and am finding some of their concepts a bit opaque. I have a app that manages inventory for a company. There is a screen that lists the entirety of their inventory and allows them to edit each inventory item. The text fields are disabled by default and I want to have an 'edit item' button that will set disabled / true to disabled / false. I have created the following which renders out correctly:
Inv.InventoryitemsRoute = Ember.Route.extend({
model: function(params) {
return Ember.$.getJSON("/arc/v1/api/inventory_items/" + params.location_id);
}
<script type="text/x-handlebars" data-template-name="inventoryitems">
{{#each}}
<div class='row'>
<p>{{input type="text" value=header disabled="true"}}</p>
<p>{{input type="text" value=detail disabled="true"}}</p>
<button {{action "editInventoryItem" data-id=id}}>edit item</button>
<button {{action "saveInventoryItem" data-id=id}}>save item</button>
</div>
{{/each}}
</script>
So this renders in the UI fine but I am not sure how to access the specific model to change the text input from disabled/true to disabled/false. If I were just doing this as normal jQuery, I would add the id value of that specific model and place an id in the text input so that I could set the textfield. Based upon reading through docs, it seems like I would want a controller - would I want an ArrayController for this model instance or could Ember figure that out on its own?
I'm thinking I want to do something like the following but alerting the id give me undefined:
Inv.InventoryitemsController=Ember.ArrayController.extend({
isEditing: false,
actions: {
editInventoryItem: function(){
var model = this.get('model');
/*
^^^^
should this be a reference to that specific instance of a single model or the list of models provided by the InventoryitemsRoute
*/
alert('you want to edit this:' + model.id); // <-undefined
}
}
});
In the Ember docs, they use a playlist example (here: http://emberjs.com/guides/controllers/representing-multiple-models-with-arraycontroller/) like this:
App.SongsRoute = Ember.Route.extend({
setupController: function(controller, playlist) {
controller.set('model', playlist.get('songs'));
}
});
But this example is a bit confusing (for a couple of reasons) but in this particular case - how would I map their concept of playlist to me trying to edit a single inventory item?
<script type="text/x-handlebars" data-template-name="inventoryitems">
{{#each}}
<div class='row'>
<p>{{input type="text" value=header disabled="true"}}</p>
<p>{{input type="text" value=detail disabled="true"}}</p>
<button {{action "editInventoryItem" this}}>edit item</button>
<button {{action "saveInventoryItem" this}}>save item</button>
</div>
{{/each}}
</script>
and
actions: {
editInventoryItem: function(object){
alert('you want to edit this:' + object.id);
}
}
Is what you need. But let me explain in a bit more detail:
First of all, terminology: Your "model" is the entire object tied to your controller. When you call this.get('model') on an action within an array controller, you will receive the entire model, in this case an array of inventory items.
The {{#each}} handlebars tag iterates through a selected array (by default it uses your entire model as the selected array). While within the {{#each}} block helper, you can reference the specific object you are currently on by saying this. You could also name the iteration object instead of relying on a this declaration by typing {{#each thing in model}}, within which each object would be referenced as thing.
Lastly, your actions are capable of taking inputs. You can declare these inputs simply by giving the variable name after the action name. Above, I demonstrated this with {{action "saveInventoryItem" this}} which will pass this to the action saveInventoryItem. You also need to add an input parameter to that action in order for it to be accepted.
Ok, that's because as you said, you're just starting with Ember. I would probably do this:
<script type="text/x-handlebars" data-template-name="inventoryitems">
{{#each}}
<div class='row'>
<p>{{input type="text" value=header disabled=headerEnabled}}</p>
<p>{{input type="text" value=detail disabled=detailEnabled}}</p>
<button {{action "editInventoryItem"}}>edit item</button>
<button {{action "saveInventoryItem"}}>save item</button>
</div>
{{/each}}
</script>
with this, you need to define a headerEnabled property in the InventoryitemController(Note that it is singular, not the one that contains all the items), and the same for detailEnabled, and the actions, you can define them also either in the same controller or in the route:
App.InventoryitemController = Ember.ObjectController.extend({
headerEnabled: false,
detailEnabled: false,
actions: {
editInventoryItem: function() {
this.set('headerEnabled', true);
this.set('detailEnabled', true);
}
}
});
that's just an example how you can access the data, in case the same property will enable both text fields, then you only need one, instead of the two that I put . In case the 'each' loop doesn't pick up the right controller, just specify itemController.

ember.js load data in dialog

i try to create my first ember.js app. A calendar-
my day model
App.Day = Ember.Object.extend({
today : null,
dayNumber : null,
addEvent : function() {
console.log(this);
$("#myModal").modal('show');
}
});
the html view
<div class="cal">
{{#each App.DayList}}
{{#if this.today}}
<div class="day today" {{action "addEvent" target="model" }}>
{{#with this as model}}
<span class="text">{{this.dayNumber}}</span>
{{/with}}
</div>
{{else}}
<div class="day" {{action "addEvent" target="model" }}>
{{#with this as model}}
<span class="text">{{this.dayNumber}}</span>
{{/with}}
</div>
{{/if}}
{{/each}}
</div>
so on click on day i show the bootstrap dialog and I wont to load extern data, but I need a information about clicked day.
My understanding is I create a view
App.DayDetails = Ember.View.extend({
});
and inside this view I send an ajax request, but how to get information about clicked day inside this view?
You should almost never be doing any AJAX in a view.
Views do two things:
(1) draw themselves
(2) respond to UI events (clicks, typing, etc)
Your view should get its contents from a controller, in this case I suppose App.DayController or DayDetailsController. (that's another thing, it's best practice to end your subclasses with View or Controller, etc, so its obvious at a glance what they do).
Where the controller gets that data from is where things might get complicated. Ideally, in a mature app, you'd have a data store (a combination—in concept—of your server-side database and ActiveRecord, if you use rails) that would be queried. Simplistically, however, you could have the controller be responsible for using jQuery to manually handle an ajax request. So long as we're taking short-cuts, you could put such a call in a number of place, (a singleton controller, a day-specific item controller, the day model itself), just NOT the view. And it's important when taking these sorts of short-cuts to limit the contagion... all you should be doing with the manual ajax is fetching the JSON and then immediately and expeditiously getting it back into the ember ecosystem by setting it as the content of an array controller. I.e., no going one or two steps further by trying to insert the data into a view manually or whatnot. Don't fight Ember, if you can avoid it.
A few things:
(1) Your use of this is superfluous, as are the {{with}} statements. Inside an {{each}} block the context will be the current object (or its wrapping controller, if you're using itemController) in the iteration. (UNLESS you use "x in y" syntax, in which case the context remains the controller)
(2) The model should NOT be attempting to modify the DOM. Instead, rely on bindings and your controllers to coordinate UI changes. What you might want to do is have a App.DayController that you can put addEvent on, and then in your {{each}} use itemController="App.DayController".
App.DayController = Ember.ObjectController.extend({
addEvent: function () {
// ...
}
});
Then, the context for each loop in your {{each}} template will be each individual day controller. The controller will automatically be the target and context for the views so your template would look like this:
{{#each App.DayList itemController="App.DayController"}}
<div {{bindAttr class=":day today"}} {{action addEvent}}>{{dayNumber}}</div>
{{/each}}
(the : in :day means that day will always be a class, but today will only be a class if the today property on the context is truthy)
Because each day sends addEvent to its own controller, there's no need for figuring out what day to load.

Selected item in a template, is there any solution for a context aware bindAttr?

The problem is as follows:
In our application we have several buttons, navigation icons etc., which we want to be 'selected' when they have been clicked. We can have multiple elements marked at the same time.
The secondary reason for me wanting to do this is that when I read the new Guides on emberjs.com I get the feeling that templates should be used more than stated before and that templates should have the responsibility of rendering the DOM, while the views should be used to handle sophisticated events (if any) or to create common/shared components to be reused in the application.
Currently the view is handling this:
app.NavView = Ember.CollectionView.extend({
...
itemViewClass: Ember.View.extend({
...
classNameBindings: ['isSelected:selected']
isSelected: function () {
return this.get('controller.selected') === this.get('content');
}.property('controller.selected')
})
});
But that is all the View basically is doing, I would like to drop the entire View and just use a template for this
I have tried with a template approach, and dropped the entire View concept.
<div id="main-menu">
{{#each content}}
<div {{bindAttr class="controller.isSelected:selected"}}>
{{{iconsvg}}}
{{name}}
</div>
{{/each}}
</div>
But my problem here of course is that bindAttr doesn't know about the context it’s in, and cannot 'send' this to the isSelected property on the controller to evaluate if it is this element that is selected or not.
Is there a good solution to do this without a view, or am I forced to use a view?
Or am I thinking the design part and responsibility of Templates/views/controllers wrong?
Any response is appreciated!
In the current documentation: http://emberjs.com/guides/templates/displaying-a-list-of-items/ there is a mention explaining how to use the {{each}} helper which doesn't override the current context.
In your case, this would be something like:
<div id="main-menu">
{{#each item in controller}}
<div {{bindAttr class="isSelected:selected"}}>
{{{item.iconsvg}}}
{{item.name}}
</div>
{{/each}}
</div>
Note I have remove the reference to 'controller' in the {{bindAttr}} since I assume it's an ember controller, then it's the current context, so basically isSelected is equivalent to controller.isSelected