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();
}
}
});
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 have adopted a simple Ember app. Currently, I load a set of locations via the model method on the route like this:
Hex.LocationsbysectionsRoute = Ember.Route.extend({
model: function(params) {
return $.getJSON("/arc/v1/api/all-locations").then( function(response){
return response.map(function (child) {
return Hex.Location.create(child);
});
});
}
});
I would like to add a search button at the bottom to add locations to a specific section. I understand that I could use transitionTo but I'd just like to place this into the DOM somehow - this seems really simple but having a hard time finding a working example online.
Something like:
<script type="text/x-handlebars" id="locationsbysections">
<input id='hex-ember-location-val' /><button {{action 'searchForLocations'}}>search</button>
</script>
But I'm not really sure how to handle the searchForLocations action and get the results into the UI. Would I use the model on the Route? I was thinking something like this but how would I deliver the Promise to the template?
Hex.LocationsbysectionsController = Ember.ArrayController.extend({
actions: {
searchForLocations: function() {
var query=$('#hex-ember-location-val').val();
$.getJSON("/arc/v1/api/locations/query_by_sections/" + query).then( function(response){
var items=[];
$.each(response, function(idx, val){
var location=Hex.Location.create(val);
items.push(location);
console.log(location);
});
});
}
}
});
I'm able to put this into the items array but how would I render that into the original locationsbysections template? It doesn't seem like the model method of the Router is the place to do this but how would I get this to work?
I have tried something like this:
{{#if hasSearchItems}}
<div>there are {{items.length}} search resutls!!!</div>
{{#each items}}
<div>{{name}} <button {{action "addToSection" this}}>add to section</button></div>
{{/each}}
{{else}}
<div>there are no search results</div>
{{/if}}
and then manage the hasSearchItems variable in the Controller, but no luck.
If you don't use real ember-data model you can eventually leave model empty and set your property in setupController:
Hex.LocationsbysectionsRoute = Ember.Route.extend({
model: function(params) {return null},
setupController: function(controller, model) {
$.getJSON("/arc/v1/api/all-locations").then(function(response) {
var locations = response.map(function (child) {
return Hex.Location.create(child);
});
controller.set("locations", locations)
});
}
}
<script type="text/x-handlebars" id="locationsbysections">
{{#each location in locations}}
<div>{{location.name}}</div>
etc...
{{/each}}
</script>
In this manner you can overwrite your locations property without problems.
<script type="text/x-handlebars" id="locationsbysections">
...
{{input type='text' value=searchInput}}
<button {{action 'searchForLocations'}}>search</button>
</script>
Hex.LocationsbysectionsController = Ember.ArrayController.extend({
searchInput: "",
actions: {
searchForLocations: function() {
var that = this;
$.getJSON("/arc/v1/api/locations/query_by_sections/" + that.get("searchInput")).then(function(response) {
var locations = response.map(function (child) {
return Hex.Location.create(child);
});
that.set("locations", locations)
});
});
}
});
I have a collection view like this (CoffeeScript):
App.SearchSuggestionsList = Ember.CollectionView.extend
tagName: 'ul'
contentBinding: 'controller.controllers.searchSuggestionsController.content'
itemViewClass: Ember.View.extend
template: Ember.Handlebars.compile('{{view.content.title}}')
isSelected: (->
/* This is where I don't know what to do */
this.index == controller's selected index
).property('controller.controllers.searchSuggestionsController.selectedIndex')
emptyView: Ember.View.extend
template: Ember.Handlerbars.compile('<em>No results</em>')
As you can see there's some pseudo-code inside the isSelected method. My goal is to define the concept of the currently selected item via this yet-to-be-implemented isSelected property. This will allow me to apply a conditional className to the item that is currently selected.
Is this the way to go? If it is, then how can this isSelected method be implemented? If not, what's another way around this to achieve the same thing?
I think it solves the case that you are looking for, with a changing list.
What we do is similar to the solution above, but the selected flag is based on the collection's controller, not the selected flag. That lets us change the "selected" piece via click, url, keypress etc as it only cares about what is in the itemController.
So the SearchListController references the items and item controllers (remember to call connectController in the router)
App.SearchListController = Em.ObjectController.extend
itemsController: null
itemController: null
App.SearchListView = Em.View.extend
templateName: "templates/search_list"
The individual items need their own view. They get selected added as a class if their context (which is an item) matches the item in the itemController.
App.SearchListItemView = Em.View.extend
classNameBindings: ['selected']
tagName: 'li'
template: Ember.Handlebars.compile('<a {{action showItem this href="true" }}>{{name}}</a>')
selected:(->
true if #get('context.id') is #get('controller.itemController.id')
).property('controller.itemController.id')
the SearchList template then just loops through all the items in the itemsController and as them be the context for the single item view.
<ul>
{{each itemsController itemViewClass="App.SearchListItemView"}}
</ul>
Is that close to what you're looking for?
The trivial (or the most known) way to do this is to have your child view (navbar item) observe a "selected" property in the parent view (navbar) which is bound to a controller, so in your route you tell the controller which item is selected. Check this fiddle for the whole thing.
Example:
Handlebars template of the navbar
<script type="text/x-handlebars" data-template-name="navbar">
<ul class="nav nav-list">
<li class="nav-header">MENU</li>
{{#each item in controller}}
{{#view view.NavItemView
itemBinding="item"}}
<a {{action goto item target="view"}}>
<i {{bindAttr class="item.className"}}></i>
{{item.displayText}}
</a>
{{/view}}
{{/each}}
</ul>
</script>
your navbar controller should have a "selected" property which you'll also bind in your view
App.NavbarController = Em.ArrayController.extend({
content: [
App.NavModel.create({
displayText: 'Home',
className: 'icon-home',
routeName: 'home',
routePath: 'root.index.index'
}),
App.NavModel.create({
displayText: 'Tasks',
className: 'icon-list',
routeName: 'tasks',
routePath: 'root.index.tasks'
})
],
selected: 'home'
});
Then you have a view structure similar to this, where the child view checks if the parent view "selected" has the same name of the child
App.NavbarView = Em.View.extend({
controllerBinding: 'controller.controllers.navbarController',
selectedBinding: 'controller.selected',
templateName: 'navbar',
NavItemView: Em.View.extend({
tagName: 'li',
// this will add the "active" css class to this particular child
// view based on the result of "isActive"
classNameBindings: 'isActive:active',
isActive: function() {
// the "routeName" comes from the nav item model, which I'm filling the
// controller's content with. The "item" is being bound through the
// handlebars template above
return this.get('item.routeName') === this.get('parentView.selected');
}.property('item', 'parentView.selected'),
goto: function(e) {
App.router.transitionTo(this.get('item.routePath'), e.context.get('routeName'));
}
})
});
Then, you set it in your route like this:
App.Router = Em.Router.extend({
enableLogging: true,
location: 'hash',
root: Em.Route.extend({
index: Em.Route.extend({
route: '/',
connectOutlets: function(r, c) {
r.get('applicationController').connectOutlet('navbar', 'navbar');
},
index: Em.Route.extend({
route: '/',
connectOutlets: function (r, c) {
// Here I tell my navigation controller which
// item is selected
r.set('navbarController.selected', 'home');
r.get('applicationController').connectOutlet('home');
}
}),
// other routes....
})
})
})
Hope this helps
I have a list of object, stored in an arrayController and rendered on the view using the #each macro
{{#each item in controller}}
{{view App.ItemView}}
{{/each}}
Each item view has class name binding that depends on the user action. For exemple :
App.ItemView = Ember.View.extend {
classNameBindings: ['isSelected:selected']
}
isSelecteddepends on the state of each Item : I have to store the selected item somewhere, and compare it to the new selected item if a click event is triggered.
The question is: where should I compute this isSelectedproperty ? In the itemsController ? In an itemController? Directly in each itemView ?
To me, it does make sense to put it into the view as, moreover, it is really a display concern.
You've got an example here: http://jsfiddle.net/MikeAski/r6xcA/
Handlebars:
<script type="text/x-handlebars" data-template-name="items">
{{#each item in controller}}
{{view App.ItemView contentBinding="item"}}
{{/each}}
</script>
<script type="text/x-handlebars" data-template-name="item">
Item: {{item.label}}
</script>
JavaScript:
App.ItemsController = Ember.ArrayController.extend({
selected: null
});
App.ItemsView = Ember.View.extend({
templateName: 'items'
});
App.ItemView = Ember.View.extend({
templateName: 'item',
classNameBindings: ['isSelected:selected'],
isSelected: function() {
var item = this.get('content'),
selected = this.getPath('controller.selected');
return item === selected;
}.property('item', 'controller.selected'),
click: function() {
var controller = this.get('controller'),
item = this.get('content');
controller.set('selected', item);
}
});
App.ItemsView.create({
controller: App.ItemsController.create({
content: [{ label: 'My first item' },
{ label: 'My second item' },
{ label: 'My third item' }]
})
}).append();
It sounds like you need two things - an isSelected property on the item itself (the model) which answers the question, "Is this item selected?", and a selectedItem property on the itemsController, which answers the question, "Which item is selected?" The property on the model is just a get/set; you could compute itemsController.selectedItem by filtering the list of items for one where isSelected is true, or you could set it explicitly with some code to un-select previously unselected items.
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