How to prevent double clicks with ember.js? - ember.js

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>

Related

Dashboard style quick editing in Ember.js

I'm attempting to make an admin backend for my Rails app with Ember.
Here's a JsBin illustrating the problems I'm having.
http://emberjs.jsbin.com/titix/20/edit
In short, I want to be able to edit the title of a arbitrary model inside of a list of other models when a user clicks on it.
Relevant CoffeeScript with questions in the comments:
App.ItemView = Ember.View.extend
templateName: "item"
isEditing: false
didInsertElement: ->
# 1. Is there a better way to toggle the isEditing property when the title is clicked?
view = #
#$('.title').click ->
view.toggleProperty('isEditing')
# 2. How would I unset isEditing when the user clicks on a different App.ItemView?
# 3. How do I set App.ItemController to be the controller for App.ItemView?
App.ItemController = Ember.Controller.extend
# 4. How would I then toggle the isEditing property of App.ItemView on either save of cancel from App.ItemController?
actions:
save: ->
# set isEditing is false on App.ItemView
#get('model').save()
cancel: ->
# set isEditing is false on App.ItemView
#get('model').rollback()
Any help on any of these questions would be appreciated.
Okay, let's see if I can remember to answer all of the questions.
Firstly we decide to wrap the entire set of items in an array controller (this allows us to keep track of all of the children item controllers). It also allows us to define an itemController which the items can use.
<script type="text/x-handlebars" data-template-name="item-list">
<h3>{{view.title}}</h3>
<ul>
{{render 'items' view.content}}
</ul>
</script>
App.ItemsController = Em.ArrayController.extend({
itemController:'item',
resetChildren: function(){
this.forEach(function(item){
item.set('isEditing', false);
});
}
});
Secondly the render template is defined ({{render 'items' view.content}} will render the items template)
<script type="text/x-handlebars" data-template-name="items">
{{#each item in controller}}
<li>{{view App.ItemView content=item}}</li>
{{/each}}
</script>
Thirdly since we iterated over the controller it will use this modified item controller
App.ItemController = Ember.ObjectController.extend({
isEditing: false,
isSaving: false,
actions: {
startEditing: function(){
this.parentController.resetChildren();
this.set('isEditing', true);
},
save: function() {
var self = this;
this.set('isEditing', false);
this.set('isSaving', true);
this.get('model').save().finally(function(){
//pretend like this took time...
Em.run.later(function(){
self.set('isSaving', false);
}, 1000);
});
},
cancel: function() {
this.set('isEditing', false);
this.get('model').rollback();
}
}
});
and here's our template
<script type="text/x-handlebars" data-template-name="item">
{{#if controller.isEditing}}
{{input value=controller.title }}
<button {{ action 'cancel' }}>Cancel</button>
<button {{ action 'save' }}>Save</button>
{{else}}
<div {{action 'startEditing'}}>
<div class="title">{{controller.title}}</div>
</div>
{{/if}}
{{#if controller.isSaving}}
Saving...
{{/if}}
</script>
Example: http://emberjs.jsbin.com/jegipe/1/edit
Here is a working bin toggles the state of the form item in the following conditions, save button click, cancel button click and click on an another item.
Every time we click on an item, I save the item views reference to the index controller. When an other item is clicked, I use the a beforeObserver to set the previous item views state to false.
I also specified the item controller in the template.
App.IndexController = Em.ObjectController.extend({
currentEditingItem: null,
currentEditingItemWillChange: function() {
if(this.get('currentEditingItem')) {
this.set('currentEditingItem.isEditing', false);
}
}.observesBefore('currentEditingItem'),
});
App.ItemController = Ember.Controller.extend({
needs: ['index'],
formController: Em.computed.alias('controllers.index'),
currentEditingItem: Em.computed.alias('formController.currentEditingItem'),
actions: {
save: function() {
this.set('currentEditingItem.isEditing', false);
return this.get('model').save();
},
cancel: function() {
this.set('currentEditingItem.isEditing', false);
return this.get('model').rollback();
}
}
});

Ember: how to set classNameBindings on parent element of view

See http://jsfiddle.net/4ZyBM/6/
I want to use Bootstrap for my UI elements and I am now trying to convert certain elements to Ember views. I have the following problem:
I embed an input element in a DIV with a given class (control-group). If a validation error occurs on the field, then I want to add an extra class "error" to the DIV.
I can create a view based on the Ember.TextField and specify that if the error occurs the ClassNameBinding should be "error", but the problem is that class is the set to the input element and not to the DIV.
You can test this by entering a non alpha numeric character in the field. I would like to see the DIV border in red and not the input field border.
HTML:
<script type="text/x-handlebars">
<div class="control-group">
{{view App.AlphaNumField valueBinding="value" type="text" classNames="inputField"}}
</div>
</script>
JS:
App.AlphaNumField = Ember.TextField.extend({
isValid: function () {
return /^[a-z0-9]+$/i.test(this.get('value'));
}.property('value'),
classNameBindings: 'isValid::error'
})
Can I set the classNameBindings on the parent element or the element closest to the input ? In jQUery I would use:
$(element).closest('.control-group').addClass('error');
The thing here is that without using jQuery you cannot access easily the wrapping div around you Ember.TextField's. Also worth mentioning is that there might be also a hundred ways of doing this, but the simplest solution I can think of would be to create a simple Ember.View as a wrapper and check the underlying child views for validity.
Template
{{#view App.ControlGroupView}}
{{view App.AlphaNumField
valueBinding="value"
type="text"
classNames="inputField"
placeholder="Alpha num value"}}
{{/view}}
Javascript
App.ControlGroupView = Ember.View.extend({
classNameBindings: 'isValid:control-group:control-group-error',
isValid: function () {
var validFields = this.get('childViews').filterProperty('isValid', true);
var valid = validFields.get('length');
var total = this.get('childViews').get('length')
return (valid === total);
}.property('childViews.#each.isValid')
});
App.AlphaNumField = Ember.TextField.extend({
isValid: function () {
return /^[a-z0-9]+$/i.test(this.get('value'));
}.property('value')
});
CSS
.control-group-error {
border:1px solid red;
padding:5px;
}
.control-group {
border:1px solid green;
padding:5px;
}
Working demo.
Regarding bootstrap-ember integration and for the sake of DRY your could also checkout this ember-addon: https://github.com/emberjs-addons/ember-bootstrap
Hope it helps.
I think that this is the more flexible way to do this:
Javascript
Boostrap = Ember.Namespace.create();
To simplify the things each FormControl have the properties: label, message and an intern control. So you can extend it and specify what control you want. Like combobox, radio button etc.
Boostrap.FormControl = Ember.View.extend({
classNames: ['form-group'],
classNameBindings: ['hasError'],
template: Ember.Handlebars.compile('\
<label class="col-lg-2 control-label">{{view.label}}</label>\
<div class="col-lg-10">\
{{view view.control}}\
<span class="help-block">{{view.message}}</span>\
</div>'),
control: Ember.required()
});
The Boostrap.TextField is one of the implementations, and your component is a Ember.TextField. Because that Boostrap.TextField is an instance of Ember.View and not an Ember.TextField directly. We delegate the value using Ember.computed.alias, so you can use valueBinding in the templates.
Boostrap.TextField = Boostrap.FormControl.extend({
control: Ember.TextField.extend({
classNames: ['form-control'],
value: Ember.computed.alias('parentView.value')
})
});
Nothing special here, just create the defaults values tagName=form and classNames=form-horizontal, for not remember every time.
Boostrap.Form = Ember.View.extend({
tagName: 'form',
classNames: ['form-horizontal']
});
Create a subclass of Boostrap.Form and delegate the validation to controller, since it have to be the knowledge about validation.
App.LoginFormView = Boostrap.Form.extend({
submit: function() {
debugger;
if (this.get('controller').validate()) {
alert('ok');
}
return false;
}
});
Here is where the validation logic and handling is performed. All using bindings without the need of touch the dom.
App.IndexController = Ember.ObjectController.extend({
value: null,
message: null,
hasError: Ember.computed.bool('message'),
validate: function() {
this.set('message', '');
var valid = true;
if (!/^[a-z0-9]+$/i.test(this.get('value'))) {
this.set('message', 'Just numbers or alphabetic letters are allowed');
valid = false;
}
return valid;
}
});
Templates
<script type="text/x-handlebars" data-template-name="index">
{{#view App.LoginFormView}}
{{view Boostrap.TextField valueBinding="value"
label="Alpha numeric"
messageBinding="message"
hasErrorBinding="hasError"}}
<button type="submit" class="btn btn-default">Submit</button>
{{/view}}
</script>
Here a live demo
Update
Like #intuitivepixel have said, ember-boostrap have this implemented. So consider my sample if you don't want to have a dependency in ember-boostrap.

Calling controller action from view in Ember

I have a submit button with a onClick view event. This event checks a flag and depending upon the condition it will allow form submission. I'd like the submit action on the controller to be called. What is the best way to do this?
Here another solution based on the example by albertjan for the case you have to perform some logic in your View and afterwards delegate to your controller. This is the way i understood your question:
HBS:
<script type="text/x-handlebars" data-template-name="index">
<button {{action submit target="view"}} >Sumbit</button>
</script>
View:
App.ThingView = Ember.View.extend({
submit : function(){
//do the view part of your logic
var object = //do whatever you may need
this.get("controller").send("submitInController", object); //you do not have to send object, if you do not need to
}
});
Controller:
App.ThingController = Em.ObjectController.extend({
submitInController: function(model) {
// do the controller part of your logic
}
});
Note: The call from your view will also bubble up to your current route. So this is basically the same code, that ember is executing when using the action helper.
I would handle the whole event on the controller:
HBS:
<script type="text/x-handlebars" data-template-name="index">
<button {{action "submit"}}>Sumbit</button>
</script>
Controller:
App.ThingController = Em.ObjectController.extend({
submit: function() {
//handle things here!
//change the state of your object here to reflect the changes that
//the submit made so that the view shows these.
}
});
In Ember version 1.0.0, I've been having success adding actions to their own object literal in the controller.
IndexTemplate.html
<script type="text/x-handlebars" data-template-name="index">
<button {{action "submit"}}>Submit</button>
</script>
ThingController.js
App.ThingController = Ember.ObjectController.extend({
actions: {
submit: function() {
//handle things here!
//change the state of your object here to reflect the changes that
//the submit made so that the view shows these.
}
}
});
For more information, check out the {{action}} helper documentation from Ember Guides.
You can trigger an action from a view if the view uses the ViewTargetActionSupport mixin. The following example demonstrates its usage:
App.SomeController = Ember.Controller.extend({
actions: {
doSomething: function() {
alert('Doing something!');
}
}
});
App.SomeView = Ember.View.extend(Ember.ViewTargetActionSupport, {
someMethod: function() {
this.triggerAction({action: 'doSomething'});
}
});

Global Notifications View using Ember

I have a notification view responsible for displaying global messages at the top of the page (info, warning, confirmation messages ...)
I created a NotificationView for the purpose, defined its content property and provided two handlers to show and hide the view.
APP.NotificationView = Ember.View.extend({
templateName: 'notification',
classNames:['nNote'],
content:null,
didInsertElement : function(){
},
click: function() {
var _self = this;
_self.$().fadeTo(200, 0.00, function(){ //fade
_self.$().slideUp(200, function() { //slide up
_self.$().remove(); //then remove from the DOM
});
});
_self.destroy();
},
show: function() {
var _self = this;
_self.$().css('display','block').css('opacity', 0).slideDown('slow').animate(
{ opacity: 1 },
{ queue: false, duration: 'slow' }
);
}
});
Ideally, i should be able to send an event from any controller or route to show the view with the proper content and styling. What would be the best way to architect this
I thought of using a named outlet in my application's template, however outlets are not quite suited for dynamic views.
<div id="content">
{{outlet notification}}
{{outlet}}
</div>
I was also thinking of architecting the notification view to be a response to "The application" or "A Module" state.
Because you have animations you want to run when the notifications change, you will want to create a subclass of Ember.View (a "widget"):
App.NotificationView = Ember.View.extend({
notificationDidChange: function() {
if (this.get('notification') !== null) {
this.$().slideDown();
}
}.observes('notification'),
close: function() {
this.$().slideUp().then(function() {
self.set('notification', null);
});
},
template: Ember.Handlebars.compile(
"<button {{action 'close' target='view'}}>Close</button>" +
"{{view.notification}}"
)
});
This widget will expect to have a notification property. You can set it from your application template:
{{view App.NotificationView id="notifications" notificationBinding="notification"}}
This will gets its notification property from the ApplicationController, so we will create a couple of methods on the controller that other controllers can use to send notifications:
App.ApplicationController = Ember.Controller.extend({
closeNotification: function() {
this.set('notification', null);
},
notify: function(notification) {
this.set('notification', notification);
}
});
Now, let's say we want to create a notification every time we enter the dashboard route:
App.DashboardRoute = Ember.Route.extend({
setupController: function() {
var notification = "You have entered the dashboard";
this.controllerFor('application').notify(notification);
}
});
The view itself manages the DOM, while the application controller manages the notification property. You can see it all working at this JSBin.
Note that if all you wanted to do was display a notification, and didn't care about animations, you could just have done:
{{#if notification}}
<div id="notification">
<button {{action "closeNotification"}}>Close</button>
<p id="notification">{{notification}}</p>
</div>
{{/if}}
in your application template, with the same ApplicationController, and everything would just work.
I don't agree that Notifications should be a View, I think they should be a Component. Then they are also more flexible to be used across your application.
You could a Notification Component instead as answered here: How can I make an Alert Notifications component using Ember.js?

What is the best way to handle events in Ember.js?

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;
}
}
});