I'm beginning to learn Ember and it's not clear what the best, most acceptable, or even intended method to handle events is. Is it acceptable to check the target in the click functions event argument, should I make a new view for each item that requires an event other than {{action}}, or something totally different?
IMO you should use the {{action}} helper where possible. If you want to attach events on a tag in the template, use {{action}}; no need to make a new View:
<a {{action showPosts href=true}}>All Posts</a>
<form {{action validate target="controller"}}>
// ...
</form>
An exception to the above is when you want to handle more than one events on a specific element:
// Template
<ul>
{{#each post in controller}}
{{#view App.PostView}}
{{title}}
{{#if view.showDetails}}
<span>{{summary}}</span>
{{/if}}
{{/view}}
{{/each}}
</ul>
// View
App.PostView = Ember.View.extend({
tagName: li,
classNames: ['post-item'],
mouseEnter: function(event) {
this.set('showDetails', true);
},
mouseLeave: function(event) {
this.set('showDetails', false);
}
});
As we need to capture both mouseEnter and mouseLeave (to show and hide the details of the post respectively), it is better to do it in the View, avoiding too much logic in the templates. The alternative way for the above would be to use as many nested tags as the number of events we want to handle (in our case, 2):
// Template
<ul>
{{#each post in controller}}
<li class="post-item" {{action showTheDetails post on="mouseEnter" target="controller"}}>
<span class="dummy" {{action hideTheDetails post on="mouseLeave" target="controller"}}
{{title}}
{{#if post.showDetails}}
<span>{{summary}}</span>
{{/if}}
</span<
</li>
{{/each}}
</ul>
And then in the controller:
// Controller
App.PostsController = Ember.ArrayController.extend({
showTheDetails: function(event) {
var post = event.context;
post.set('showDetails', true);
},
hideTheDetails: function(event) {
var post = event.context;
post.set('showDetails', false);
}
});
But I think you will agree that this is uglier. See here.
In cases where you want to use Ember control views (Ember.TextField, Ember.TextArea, etc.) you have no choice but to capture events in the View. So you extend the control view and define the event handlers in the View:
// Template
<legend>Add a comment</legend>
{{view App.CommentInputField valueBinding="comment"}}
// View
App.CommentInputField = Ember.TextField.extend({
focusOut: function(event) {
this.get('controller').validateComment();
},
keyDown: function(event) {
if (event.keyCode === 13) { // Enter key
this.get('controller').createComment();
return false;
}
}
});
Related
I'm trying to figure out the idiomatic way to prevent a button from being clicked multiple times.
Imagine I have a simple controller action like so ...
var FooController = Ember.ObjectController.extend({
actions: {
go: function() {
console.log("done!");
}
}
});
and in my template I have a button defined like so ...
<button {{action go}}>Click Me Fast</button>
Does the action have an option to disable it immediately / making it so only once true event will be handled by the controller (until it's disabled for example)
Edit
I'm looking for a long term / multi use solution. One idea I'm thinking about is creating a special ember-component called "button-disable" that would allow me to create a custom button type that generally disables after a single click -but will still allow me to bubble up events to a parent controller. This feels a little heavier weight than I'd like so if another option exists, or if someone has created an addon for just this - let me know
As a one-off, if you bind the disabled attribute on your button like so
<button {{action go}} {{bind-attr disabled=actionPerformed}}>
and then set up your controller like
var FooController = Ember.ObjectController.extend({
actionPerformed: false,
actions: {
go: function() {
this.set("actionPerformed", true);
console.log("done!");
}
}
});
then the button will become disabled after you click it once
If you want a reusable component I'd borrow the spinner button from http://emberjs.com/guides/cookbook/helpers_and_components/spin_button_for_asynchronous_actions/ and tweak it as you need.
So your JS would be along the lines of
window.SpinEg = Ember.Application.create({});
SpinEg.ApplicationController = Ember.Controller.extend({
isLoading:false,
buttonText:"Submit",
actions:{
saveData:function(){
var self = this;
var saveTime = Ember.run.later(function(){
self.set('isLoading', false);
}, 1000);
}
}
});
SpinEg.SpinButtonComponent = Ember.Component.extend({
classNames: ['button'],
buttonText:"Save",
isLoading:false,
actions:{
showLoading:function(){
if(!this.get('isLoading')){
this.set('isLoading', true);
this.sendAction('action');
}
}
}
});
The template for your component would be
<script type='text/x-handlebars' id='components/spin-button'>
<button {{bind-attr id=id}} {{action 'showLoading'}}>
{{#if isLoading}}
<img src="http://i639.photobucket.com/albums/uu116/pksjce/spiffygif_18x18.gif"></img>
{{else}}
{{buttonText}}
{{/if}}
</button>
</script>
and you would then just include the following where you need the button to appear
<script type='text/x-handlebars' id='application'>
{{spin-button id="forapplication" isLoading = isLoading buttonText=buttonText action='saveData'}}
</script>
I currently have a view setup that renders the template depending on the model:
<ul>
{{#each controller.sortedAll}}
{{view App.ScoreView}}
{{/each}}
</ul>
.
App.ScoreView = Ember.View.extend({
templateName: function(){
var item = this.get('context')
if (item.sort < 8){
return 'low'
} else {
return 'high'
}
}.property(),
})
I'm struggling with assigning a specific controller for each separate view. The jsbin is: http://jsbin.com/tahag/5/edit
Is it possible to specify the controller in App.ScoreView using controller:? Or would I be better off trying to set an item controller:
{{view App.ScoreView itemController="VAL"}}
And pass the VAL from the parent controller as a property?
The controller is inherited from the current scope (defining itemController on the view won't do anything). You should do it on the each.
{{#each controller.sortedAll itemController='val'}}
{{view App.ScoreView}}
{{/each}}
Then within the view you can do this.get('controller')...
Also you can do an if statement and do {{render 'high' this}} if you want to have different types of controllers on each item.
{{#each controller.sortedAll}}
{{#if isBlue}}
{{render 'blue' this}}
{{/if}}
{{#if isGreen}}
{{render 'green' this}}
{{/if}}
{{/each}}
Personal recommendations:
To avoid making your template super convoluted I would just use a single controller.
{{#each controller.sortedAll itemController='score'}}
{{input value=sort}}
{{view App.ScoreView}}
{{/each}}
Add what the computed property is dependent on in order for it to automagically update:
App.ScoreView = Ember.View.extend({
templateName: function(){
var sort = this.get('controller.sort');
if (sort < 8){
return 'low';
} else {
return 'high';
}
}.property('controller.sort'),
});
Take advantage of the computed helpers
App.ScoreController = Ember.ObjectController.extend({
isVeryHigh: Em.computed.gt('sort', 20),
isVeryLow: Em.computed.lt('sort', 4)
});
Example: http://jsbin.com/sidebozi/1/edit
I wanna render a block in Ember Handlebars only, if a specific route is active.
So, how can I create a 'ifRoute' helper, with the same conditons then the 'active' class on the 'linkTo' helper?
I want this, because I've a two layer navigation. So, I want to show the sub-navigation only, if the head navigation point is active. I dont wanna use the 'active' class, because I use lazy loading and I only want to load the sub navigation when the head navigation point is active.
So, what I want to do is:
<ul>
{{#each assortmentGroups}}
<li>
{{#linkTo "assortmentGroup" this}} {{description}} {{/linkTo}}
{{#ifRoute "assortmentGroup" this}}
<ul>
{{#each itemCategories}}
<li>{{#linkTo "itemCategory" this}} {{description}} {{/linkTo}}</li>
{{/each}}
</ul>
{{/ifRoute}}
</li>
{{/each}}
<ul>
How can I do this or is there a better solution?
Thanks
Just add to the controller:
needs: ['application'],
isCorrectRouteActive: Ember.computed.equal('controllers.application.currentRouteName', 'correctRoute')
Similarly:
isCorrectPathActive: Ember.computed.equal('controllers.application.currentPath', 'correct.path')
isCorrectURLActive: Ember.computed.equal('controllers.application.currentURL', 'correctURL')
I am quite sure latest Ember does the rest
Here are two possible options, although for both you first have to save the currentPath in your ApplicationController to have access to it whenever you need it:
var App = Ember.Application.create({
currentPath: ''
});
App.ApplicationController = Ember.ObjectController.extend({
updateCurrentPath: function() {
App.set('currentPath', this.get('currentPath'));
}.observes('currentPath')
});
Using a computed property
Then in the controller backing up the template, let's say you have a NavigationController you create the computed property and define also the dependency to the ApplicationController with the needs API to gather access, then in the CP you check if the currentPath is the one you want:
App.NavigationController = Ember.Controller.extend({
needs: 'application',
showSubMenu: function(){
var currentPath = this.get('controllers.application.currentPath');
return (currentPath === "assortmentGroup");
}.property('controllers.application.currentPath')
});
So you can use a simple {{#if}} helper in your template:
...
{{#linkTo "assortmentGroup" this}} {{description}} {{/linkTo}}
{{#if showSubMenu}}
<ul>
{{#each itemCategories}}
<li>{{#linkTo "itemCategory" this}} {{description}} {{/linkTo}}</li>
{{/each}}
</ul>
{{/if}}
</li>
...
Using a custom '{{#ifRoute}}' helper
But if your really want a custom helper to deal with your condition then this is how you could do it, note that the currentPath stored on your application is still needed since we need a way to get the value of the current route:
Ember.Handlebars.registerHelper('ifRoute', function(value, options) {
if (value === App.get('currentPath')) {
return options.fn(this);
}
else {
return options.inverse(this);
}
});
And then you could use it like this:
...
{{#linkTo "assortmentGroup" this}} {{description}} {{/linkTo}}
{{#ifRoute "assortmentGroup"}}
<ul>
{{#each itemCategories}}
<li>{{#linkTo "itemCategory" this}} {{description}} {{/linkTo}}</li>
{{/each}}
</ul>
{{/ifRoute}}
</li>
...
See here also a simple Demo of the "custom helper" solution: http://jsbin.com/izurix/7/edit
Note: with the second solution there is a catch! Since bound helpers do not support blocks (in embers handlebars customization) I used a simple helper that does not reevaluate the condition depending on bindings which is may not what you want.
Hope it helps.
After investigating the ember code for the linkTo and if helpers, the answer from intuitivepixel and a blog post about writing my own bound block helper, I've found a solution:
var resolveParams = Ember.Router.resolveParams;
var resolvedPaths = function(options) {
var types = options.options.types.slice(1),
data = options.options.data;
return resolveParams(options.context, options.params, { types: types, data: data });
};
Ember.Handlebars.registerHelper('ifRoute', function(name) {
var options = [].slice.call(arguments, -1)[0];
var params = [].slice.call(arguments, 1, -1);
var theResolvedPaths = resolvedPaths({ context: this, options: options, params: params });
var router = options.data.keywords.controller.container.lookup('router:main');
var self = this;
var evaluateIsCurrentRoute = function() {
self.set('current_route_is_active_bool_for_ifroute', (function() {
return router.isActive.apply(router, [name].concat(theResolvedPaths)) ||
router.isActive.apply(router, [(name + '.index')].concat(theResolvedPaths));
})());
};
evaluateIsCurrentRoute();
router.addObserver('url', evaluateIsCurrentRoute);
options.contexts = null;
return Ember.Handlebars.helpers.boundIf.call(this, 'current_route_is_active_bool_for_ifroute', options);
});
I found an easy way to check if a route is active, but to get this into a computed property may not be so easy.
// Test if you are currently in a route by it's lowercase name
App.isInRoute = function(name) {
return App.Router.router.currentHandlerInfos.mapProperty('name').contains(name);
}
To use:
App.isInRoute('posts.show'); // true if in the route
In my application I display a list of accounts like so:
<script type="text/x-handlebars" data-template-name="accounts">
{{#each account in controller}}
{{#linkTo "account" account class="item-account"}}
<div>
<p>{{account.name}}</p>
<p>#{{account.username}}</p>
<i class="settings" {{ action "openPanel" account }}></i>
</div>
{{/linkTo}}
{{/each}}
</script>
Each account has a button which allows users to open a settings panel containing settings just for that account. as you can see in this quick screencast:
http://screencast.com/t/tDlyMud7Yb7e
I'm currently triggering the opening of the panel from within a method located on the AccountsController:
Social.AccountsController = Ember.ArrayController.extend({
openPanel: function(account){
console.log('trigger the panel');
}
});
But I feel that it's more appropriate to open the panel from within a View that I've defined for this purpose. This would give me access to the View so that I can perform manipulations on the DOM contained within it.
Social.MainPanelView = Ember.View.extend({
id: 'panel-account-settings',
classNames: ['panel', 'closed'],
templateName: 'mainPanel',
openPanel: function(){
console.log('opening the panel');
}
});
<script type="text/x-handlebars" data-template-name="mainPanel">
<div id="panel-account-settings" class="panel closed">
<div class="panel-inner">
<i class="icon-cancel"></i>close
<h3>Account Settings</h3>
Disconnect Account
</div>
</div>
</script>
The problem I'm encountering is that I don't see how I can trigger a method on the Social.MainPanelView from the context of the AccountsController. Is there a better solution?
UPDATE 1
I've worked up a Fiddle to illustrate what I'm talking about:
http://jsfiddle.net/UCN6m/
You can see that when you click the button it calls the showPanel method found on App.IndexController. But I want to be able to call the showPanel method found on App.SomeView instead.
Update:
Approach One:
Simplest of all
Social.AccountsController = Ember.ArrayController.extend({
openPanel: function(account){
/* we can get the instance of a view, given it's id using Ember.View.views Hash
once we get the view instance we can call the required method as follows
*/
Ember.View.views['panel-account-settings'].openPanel();
}
});
Fiddle
Approach Two:(Associating a controller, Much Cleaner)
Using the Handlebars render helper: what this helper does is it associates a controller to the view to be displayed, so that we can handle all our logic related to the view in this controller, The difference is
{{partial "myPartial"}}
just renders the view, while
{{render "myPartial"}}
associates App.MyPartialController for the rendered view besides rendering the view, Fiddle
now you can update your code as follows
application.handlebars(The place you want to render the view)
{{render "mainPanel"}}
accounts controller
Social.AccountsController = Ember.ArrayController.extend({
openPanel: function(account){
this.controllerFor("mainPanel").openPanel();
}
});
main panel view
Social.MainPanelView = Ember.View.extend({
id: 'panel-account-settings',
classNames: ['panel', 'closed']
});
main panel controller
Social.MainPanelController = Ember.Controller.extend({
openPanel: function(){
console.log('opening the panel');
}
})
Approach Three:
This one is the manual way of accomplishing Approach Two
Social.MainPanelView = Ember.View.extend({
id: 'panel-account-settings',
controllerBinding: 'Social.MainPanelController',
classNames: ['panel', 'closed'],
templateName: 'mainPanel'
});
Social.MainPanelController = Ember.Controller.extend({
openPanel: function(){
console.log('opening the panel');
}
})
use this.controllerFor("mainPanel").openPanel()
You need to use the action helper rather than directly coding the links. The action helper targets the controller by default, but you can change it to target the view instead:
<a {{action openPanel target="view"}}></a>
Your second link should be a linkTo a route, since you are specifying a link to another resource. The whole snippet, revised:
Social.MainPanelView = Ember.View.extend({
id: 'panel-account-settings',
classNames: ['panel', 'closed'],
templateName: 'mainPanel',
openPanel: function(){
console.log('opening the panel');
}
});
<script type="text/x-handlebars" data-template-name="mainPanel">
<div id="panel-account-settings" class="panel closed">
<div class="panel-inner">
<a {{action openPanel target="view"} class="button button-close"><i class="icon-cancel"></a></i>
<h3>Account Settings</h3>
{{#linkTo "connections"}}Disconnect Account{{/linkTo}}
</div>
</div>
</script>
Consider a View that defines a list of objects:
App.ListView = Ember.View({
items: 'App.FooController.content'
itemClicked: function(item){
}
)};
with the template:
<ul>
{{#each items}}
{{#view App.ItemView itemBinding="this" tagName="li"}}
<!-- ... -->
{{/view}}
{{/each}}
</ul>
and the ItemView:
App.ItemView = Ember.View.extend({
click: function(event){
var item = this.get('item');
// I want to call function itemClicked(item) of parentView
// so that it handles the click event
}
})
So basically my question is how do I pass events to parent views, especially in the case where the parent view is not known by the child view? I understand that you can get a property foo of a parentView with either this.getPath('parentView').get('foo') or this.getPath('contentView').get('foo'). But what about a function (in this case, itemclicked())?
this.get('parentView').itemClicked(this.get('item')); should do the trick.
You can use the {{action}} helper, see: http://jsfiddle.net/smvv5/
Template:
<script type="text/x-handlebars" >
{{#view App.ListsView}}
{{#each items}}
{{#view App.ListView itemBinding="this" }}
<li {{action "clicked" target="parentView" }} >{{item.text}}</li>
{{/view}}
{{/each}}
{{/view}}
</script>
JS:
App = Ember.Application.create({});
App.Foo = Ember.ArrayProxy.create({
content: [Ember.Object.create({
text: 'hello'
}), Ember.Object.create({
text: 'action'
}), Ember.Object.create({
text: 'world'
})]
});
App.ListsView = Ember.View.extend({
itemsBinding: 'App.Foo',
clicked: function(view, event, ctx) {
console.log(Ember.getPath(ctx, 'item.text'));
}
});
App.ListView = Ember.View.extend({
});
Recent versions of Ember use the actions hash instead of methods directly on the object (though this deprecated method is still supported, it might not be for long). If you want a reference to the view passed to the handler, send through "view" as a parameter and use the parentView as the target.
<button {{action "onClicked" view target="view.parentView"}}>Click me.</button>
App.ListsView = Ember.View.extend({
actions: {
onClicked: function(view) {
}
}
});
{{action}} helper does not send through the event object. Still not sure how to get reference to the event if you need it.
source