Ember CollectionView - Removing an ItemView - ember.js

I'm having a hard time finding an answer to this, so perhaps I'm doing it all wrong. I have a collection view with an item view class. Each item has a delete button, which refers to the action "removeItem." This does remove the item, but I need to refer back to the collection view content in order to post the updated array with an API call.
App.WatchlistTilesView.Collection = Ember.CollectionView.extend({
tagName: "ul",
itemViewClass: Ember.View.extend({
attributeBindings: "role",
role: "tile",
classNames: ["tile"],
removeItem: function() {
this.remove();
},
templateStock: Ember.Handlebars.compile("
<button {{action removeItem}}>Delete</button>
...
"),
})
});
EDIT:
I found the answer to this particular problem. There was a property on the collection view that needed to be accessed by the item class. I was able to access it with this.get("parentView"). Also, I had to add the view as the target for the removeItem action.
It looks something like this:
App.WatchlistTilesView.Collection = Ember.CollectionView.extend({
tagName: "ul",
itemViewClass: Ember.View.extend({
attributeBindings: "role",
role: "tile",
classNames: ["tile"],
watchlistBinding: "parentView.watchlist"
removeItem: ->
#remove()
watchlist = #get("watchlist")
#get("controller").removeSymbol(#content, watchlist)
templateStock: Ember.Handlebars.compile("
<button {{action removeItem target="view"}}>Delete</button>
...
"),
})
});
App.WatchlistTilesController = Ember.Controller.extend({
account: null,
watchlists: null,
removeSymbol: function(entry, watchlist) {
watchlist.entries.removeObject(entry);
return WatchlistService.edit(this.account, watchlist);
}
});

Related

Ember.js persist classNameBindings on transition to different routes

I'm fairly new to ember and I've been trying to tackle this problem for a couple of days but I can't seem to find a solution anywhere online.
I have a page with a list of all posts, each post has one tag (like a hashtag), either 'Fitness', 'Knowledge' or 'Social'. At the top of the page I have 3 view helpers and each view helper represents a tag (fitness, knowledge or social). These will be used to filter out the posts with that particular tag name.
My problem is that when I click on a view helper I toggle the "isSelected" property to true, which adds the "isSelected" class via classNameBindings. But when I transition to a different route on the site and come back, the "isSelected" property is reset back to false and the "isSelected" class has been removed. How do I keep these values persistent and in-tact for when I revisit the route?
Here's my code:
<script type="text/x-handlebars" data-template-name="global">
<ul class="categories">
<li>{{view App.Tag class="label fitness" text="fitness"}}</li>
<li>{{view App.Tag class="label knowledge" text="knowledge"}}</li>
<li>{{view App.Tag class="label social" text="social"}}</li>
</ul>
</script>
View:
"use strict";
App.Tag = Ember.View.extend({
tagName: 'span',
template: Ember.Handlebars.compile('{{view.text}}'),
classNames: ['label'],
classNameBindings: ['isSelected'],
isSelected: false,
click: function () {
this.toggleProperty('isSelected');
}
});
I have also tried using a controller with actions but that way persisted the "isSelected" property but didn't preserve the addition of the class when I revisited the route.
This may not be ideal, but to save the state of the application, you can put the state in the controller. You probably had a simple implementation, but maybe did not specify the isSelected as a property. The below works and you can view the jsbin here
App = Ember.Application.create();
App.Router.map(function() {
this.route('global');
});
App.IndexRoute = Ember.Route.extend({
model: function() {
return ['red', 'yellow', 'blue'];
}
});
App.GlobalController = Ember.Controller.extend({
activeTags: Ember.A()
})
App.Tag = Ember.View.extend({
tagName: 'span',
template: Ember.Handlebars.compile('{{view.text}}'),
classNames: ['label'],
classNameBindings: ['isSelected'],
isSelected: function () {
console.log("ON CHANGE", this.get('controller.activeTags'));
return this.get('controller.activeTags').contains(this.text);
}.property('controller.activeTags.#each'),
click: function () {
var tagArray = this.get('controller.activeTags');
if (tagArray.contains(this.text))
this.set('controller.activeTags', tagArray.without(this.text))
else
tagArray.pushObject(this.text);
}
});

Sibling attribute binding in Ember.js

I am a relative Ember newbie, so I may be missing something obvious here. I'm trying to build a custom view for a Bootstrap accordion. I'm using relative views for this, and while it is working, I'd like to understand if I can simplify a little bit. Specifically, I'd like to know if it's possible to make the href computed property of the embedded toggleView dependent on the elementId of the embedded bodyView. Here's the top level view, which is a collection:
App.AccordionView = Em.CollectionView.extend({
headingTemplateName: 'accordion-heading',
innerTemplateName: 'accordion-inner',
tagName: 'div',
classNames: ['accordion'],
contentBinding: 'controller.content',
The item view corresponds to the DOM element with the accordion-group css class.
itemViewClass: Em.View.extend({
tagName: 'div',
classNames: ['accordion-group'],
target: null,
template: Em.Handlebars.compile("{{view view.headingView contentBinding='view.content'}}{{view view.bodyView viewName='bodyViewInstance' contentBinding='view.content'}}"),
headingView: Em.View.extend({
tagName: 'div',
template: Em.Handlebars.compile("{{view view.toggleView contentBinding='view.content'}}"),
classNames: ['accordion-heading'],
toggleView: Em.View.extend({
tagName: 'div', // Can be an 'a'
templateNameBinding: 'parentView.parentView.parentView.headingTemplateName',
classNames: ['accordion-toggle'],
attributeBindings: ['dataToggle:data-toggle', 'dataParent:data-parent', 'href'],
dataToggle: 'collapse',
dataParent: function() {
return this.get('parentView.parentView.parentView.elementId');
}.property(),
Note that I've made the href property dependent on the target field in the item view.
href: function() {
return "#" + this.get('parentView.parentView.target');
}.property('parentView.parentView.target')
})
}),
bodyView: Em.View.extend({
tagName: 'div',
template: Em.Handlebars.compile("{{view view.innerView contentBinding='view.content'}}"),
classNames: ['accordion-body', 'collapse'],
This field is set when the bodyView embedded view instance is inserted into the DOM.
didInsertElement: function() {
this.get('parentView').set('target', this.get('elementId'));
},
innerView: Em.View.extend({
tagName: 'div',
classNames: ['accordion-inner'],
templateNameBinding: "parentView.parentView.parentView.innerTemplateName"
})
})
})
});
That all works. However, I'd like to do something like this in the toggleView:
href: function() {
return "#" + this.get('parentView.parentView.bodyViewInstance.elementId');
}.property('parentView.parentView.bodyViewInstance.elementId')
When I do, I get the following error, and I'm not sure I understand why:
Error: Something you did caused a view to re-render after it rendered but before it was inserted into the DOM.
Is there any way to accomplish what I'm looking to do?

How to access the current item of an each block in the controller for a view

Given this controller:
ItemController= Ember.Controller.extend({
subItems: Ember.ArrayController.create({
content: App.store.find(App.models.SubItem),
sortProperties: ['name']
}),
currentItemIdBinding: 'App.router.mainController.currentItemId',
item: function() {
return App.store.find(App.models.SubItem, this.get('currentItemId'));
}.property('currentItemId'),
currentSubItems: function () {
return this.get('subItems.content')
.filterProperty('item_id', this.get('item.id'));
}.property('item', 'subItems.#each')
});
and this each block in the template:
{{#each subItem in currentSubItems}}
{{view App.SubItemView}}
{{/each}}
How would I gain access to the "subItem" in the controller for the SubItemView?
Edit:
I stumbled upon a way to do this. If I change the each block slightly:
{{#each subItem in currentSubItems}}
{{view App.SubItemView subItemBinding="subItem"}}
{{/each}}
and add an init method to the SubItemView class:
init: function() {
this._super();
this.set('controller', App.SubItemController.create({
subItem: this.get('subItem')
}));
})
I can get access to the subItem in the controller. This however just feels wrong on more levels than I can count.
Interesting...while browsing ember.js, I found this: https://stackoverflow.com/a/14251255/489116 . It's a little different from what you're asking, but may solve the problem: if I'm reading it correctly, it would automatically associate a subItemController with each subItemView and subItem, without you having to pass the model around. Not released yet though. I'd still like to see other solutions!
How about using Ember.CollectionView instead of {{each}} helper see the following:
App.SubItemsView = Ember.CollectionView.extend({
contentBinding: "controller.currentSubItems",
itemViewClass: Ember.View.extend({
templateName: "theTemplateYouUsedForSubItemViewInYourQuestion",
controller: function(){
App.SubItemController.create({subItem: this.get("content")});
}.property()
})
})
Use it in handlebars as follows
{{collection App.SubItemsView}}

Set a Ember.CollectionView's selected item

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

Does it make sense to use ObjectController and ArrayController together?

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.