Ember: Update ObjectController property from ArrayController action? - ember.js

Disclaimer: I'm quite new to Ember. Very open to any advice anyone may have.
I have a action in a ArrayController that should set an ObjectController property. How I can access the right context to set that property when creating a new Object?
Here is abbreviated app code show my most recent attempt:
ChatApp.ConversationsController = Ember.ArrayController.extend({
itemController: 'conversation',
actions: {
openChat: function(user_id, profile_id){
if(this.existingChat(profile_id)){
new_chat = this.findBy('profile_id', profile_id).get('firstObject');
}else{
new_chat = this.store.createRecord('conversation', {
profile_id: profile_id,
});
new_chat.save();
}
var flashTargets = this.filterBy('profile_id', profile_id);
flashTargets.setEach('isFlashed', true);
}
},
existingChat: function(profile_id){
return this.filterBy('profile_id', profile_id).get('length') > 0;
}
});
ChatApp.ConversationController = Ember.ObjectController.extend({
isFlashed: false
});
The relevant template code:
{{#each conversation in controller itemController="conversation"}}
<li {{bind-attr class="conversation.isFlashed:flashed "}}>
<h3>Profile: {{conversation.profile}} Conversation: {{conversation.id}}</h3>
other stuff
</li>
{{/each}}

I don't see why you need an object that handles setting a property for all the elements in your list. Have each item take care of itself, this means components time.
Controllers and Views will be deprecated anyway, so you would do something like:
App.IndexRoute = Ember.Route.extend({
model: function() {
return [...];
}
});
App.ConversationComponent = Ember.Component.extend({
isFlashed: false,
actions: {
// handle my own events and properties
}
});
and in your template
{{#each item in model}}
{{conversation content=item}}
{{/each}}
So, whenever you add an item to the model a new component is created and you avoid having to perform the existingChat logic.

ArrayController and ItemController are going to be depreciated. As you are new to Ember I think that it would be better for you not to use them and focus on applying to coming changes.
What I can advice you is to create some kind of proxy object that will handle your additional properties (as isFlashed, but also like isChecked or isActive, etc.). This proxy object (actually an array of proxy objects) can look like this (and be a computed property):
proxiedCollection: Ember.computed.map("yourModelArray", function(item) {
return Object.create({
content: item,
isFlashed: false
});
});
And now, your template can look like:
{{#each conversation in yourModelArray}}
<li {{bind-attr class="conversation.isFlashed:flashed "}}>
<h3>Profile: {{conversation.content.profile}} Conversation: {{conversation.content.id}}</h3>
other stuff
</li>
{{/each}}
Last, but not least you get rid of ArrayController. However, you would not use filterBy method (as it allows only one-level deep, and you would have the array of proxy objects, that each of them handles some properties you filtered by - e.g. id). You can still use explicit forEach and provide a function that handles setting:
this.get("proxiedCollection").forEach((function(_this) {
return function(proxiedItem) {
if (proxiedItem.get("content.profile_id") === profile_id) {
return proxiedItem.set("isFlashed", true);
}
};
})(this));

Related

How to iterate over class variables from Handlebars in ember-cli?

I'd like to iterate over a class variable array in Handlebars in ember-cli to generate a list of checkboxes (categories), with the appropriate ones checked off based on which categories the model belongs to.
I have several issues:
- I can't figure out how to access the class variable in ember-cli. I've seen tutorials showing that in ember.js, it's just App.Listing.CATEGORIES, but I'm not getting any passes through my each loop.
- How to check off the appropriate box? I have some janky code below that probably doesn't work.
listing.js:
import DS from "ember-data";
var Listing = DS.Model.extend({
categories: DS.attr(), // string array
});
Listing.reopenClass({
CATEGORIES: ['park', 'outdoors']
});
export default Listing;
show.hbs:
<ul>
{{#each category in CATEGORIES}}
<li>{{input type="checkbox" name=category checked=categories.contains(category)}} {{category}}</li>
{{/each}}
</ul>
Handlebars templates can’t look up classes like that, nor does complex logic like categories.contains(category) work. You’ll need to add a computed property to the controller or component to supply proxy objects to the template. Assuming it’s a controller, here’s a rough example:
export default Ember.Controller.extend({
selectableCategories: function() {
var model = this.get('model');
return model.constructor.CATEGORIES.map(function(category) {
var categoryProxy = Ember.Object.create({
model: model,
name: category,
checked: function(key, value) {
var model = this.get('model');
// setter; the checkbox value has changed
if (arguments.length > 1) {
if (model.get('categories').contains(this.get('name'))) {
model.get('categories').removeObject(this.get('name'));
}
else {
model.get('categories').addObject(this.get('name'));
}
}
// getter; the template is checking whether the checkbox should be checked
return model.get('categories').contains(this.get('name'));
}.property('model.categories')
});
return categoryProxy;
});
}.property('model.categories')
});
The selectableCategories computed property returns an array of objects that observe the model’s categories attribute and represent whether or not each category is found within it.
Then in your template, you can use the proxy objects like this:
{{#each category in selectableCategories}}
{{input type="checkbox" name=category.name checked=category.checked}} {{category.name}}
{{/each}}

Creating a map property and enumerating its properties in a template?

I have poured through a ton of documentation and I can't seem to find an answer to a very basic question. I have a component that needs to store a map (key/value) as a property:
App.SimpleTestComponent = Ember.Component.extend({
data: Ember.A(),
actions: {
add: function() {
this.get('data').set('test', 'value');
}
}
});
The template for the component looks like this:
<script type="text/x-handlebars" data-template-name="components/simple-test">
{{#each item in data}}
<p>
<strong>{{item.key}}:</strong>
{{ item.value}}
</p>
{{/each}}
<button {{action 'add'}}>Add</button>
</script>
However, this doesn't work. No items are displayed after clicking the button and the problem seems to be with the {{#each}} block. How do I correctly enumerate over the data property?
Ember.A() is shorthand for Ember.NativeArray. This is why your
code isn't working, you're also calling .set which is a method
inherited from Ember.Observable. So what you're really doing is
just setting an object property on the array rather than its
content.
What you probably want is Ember.Map which is an internal class
but many developers use it anyways. You will still need to return an
array of objects as the following example:
App.SimpleTestComponent = Ember.Component.extend({
// Shared map among all SimpleTestComponents
map: Ember.Map.create(),
// Or per component map
init: function() {
this.set('map', Ember.Map.create());
},
data: function() {
// this doesn't have to be locally scoped...
var arr = Ember.A();
this.get('map').forEach(function(key, value) {
arr.addObject({key: key, value: value});
});
return arr;
}.property('map'),
actions: {
add: function() {
this.get('map').set('test', 'value');
}
}
});
This doesn't really work when you have keys that have multiples
values simply becauses Ember.Map always overwrites on .set
If performance is of concern and you would like to have multiple
values per key then you will need to implement your own
map class with and a handlebars helper to display it.

Setting itemController on a filtered subset of an ArrayController's model

Problem Summary: While I can get the children of a collection (defined on an ArrayController) to use a specific object controller for the individuals, this doesn't work on filtered subsets of the children.
Short context: I've got Subcriptions, which have Items. I'd like to filter the subscriptions in my view by type, and have the items within those subscriptions sort by timestamp. Here's the SubscriptionsController:
Social.SubscriptionsController = Ember.ArrayController.extend({
itemController: 'subscription',
announcements: function() {
return this.get('model').filterBy('kind', 'announcement');
}.property('model.#each.kind'),
user_sites: function() {
return this.get('model').filterBy('kind', 'user');
}.property('model.#each.kind')
});
I've defined SubscriptionController thusly:
Social.SubscriptionController = Ember.ObjectController.extend({
items: function() {
return Ember.ArrayProxy.createWithMixins(Ember.SortableMixin, {
sortProperties: ['post_timestamp'],
sortAscending: false,
content: this.get('content.items')
});
}.property('content.items'),
});
And here's the relevant bit of my handlebars template:
{{#each controller}}
<li>{{controller.description}} {{controller.kind}} {{controller.feed_url}} {{controller.base_url}}</li>
<ul>
{{#each item in controller.items}}
<li>{{item.post_timestamp}}: {{{item.summary}}}</li>
{{/each}}
</ul>
{{/each}}
That code more-or-less does what I want: it renders the items, sorted by item.post_timestamp, as SubscriptionController defines it.
The problem is if I change {{#each controller}} to {{#each site in user_sites}}, the itemController property doesn't seem to magically apply to the sublist. Is there some kind of Sorcery I should use to inform Ember in my filters that I'd rather return the controller for the objects rather than the objects themselves?
EDITed to add: I know I can just add a new property like sorted_items on the Subscription model itself, but this feels wrong, design-wise. The model holds the data, the view shows the data, and the controller deals with sorting / filtering and all that jazz. Or at least that's part of how I think about MVC separation.
You can manually set the itemController for loops. You might try this in your template:
{{#each site in user_sites itemController="subscription"}}
...
{{/each}}

Ember.js arraycontroller call from view

I might be using this all wrong, but:
I've got an ArrayController representing a collection of products. Each product gets rendered and there are several actions a user could take, for example edit the product title or copy the description from a different product.
Question is: how do you interact with the controller for the specific product you're working with? How would the controller know which product was being edited?
I also tried to create an Ember.Select with selectionBinding set to "controller.somevar" but that also failed.
I think the most important thing you need to do, is first move as much logic as you can away from the views, and into controllers.
Another thing that would be useful in your case, is to have an itemController for each product in the list. That way, you can handle item specific logic in this item controller.
I don't have enough information to understand your architecture, so I will make a few assumptions.
Given you have the following ProductsController:
App.ProductsController = Ember.ArrayController.extend();
You need to create a ProductController that will be created to wrap every single product on its own.
App.ProductController = Ember.ObjectController.extend();
You need to modify your template as follows:
{{#each controller itemController="product"}}
<li>name</li>
{{/each}}
Now every product in your list will have its own ProductController, which can handle one product's events and will act as the context for every list item.
Another option:
If you will only be handling one product at a time, you can use routes to describe which product you are working with:
App.Router.map(function() {
this.resource('products', { path: '/products' }, function() {
this.resource('product', { path: '/:product_id' }, function() {
this.route('edit');
});
});
});
And create a controller for editing a product:
App.ProductEditController = Ember.ObjectController.extend();
And your list items would link to that product route:
{{#each controller}}
<li>{{#linkTo "product.edit" this}}name{{/linkTo}}</li>
{{/each}}
If you define itemController on your ProductsController you don't need to specify that detail in your template:
App.ProductsController = Em.ArrayController.extend({
itemController: 'product',
needs: ['suppliers'],
actions: {
add: function() {
// do something to add an item to content collection
}
}
});
App.ProductController = Em.ObjectController.extend({
actions: {
remove: function() {
// do something to remove the item
}
}
});
Use a collection template like this:
<button {{action="add"}}>Add Item</button>
<ul>
{{#each controller}}
<li>{{name}} <button {{action="remove"}}>x</button></li>
{{/each}}
</ul>
The Ember documentation describesitemController here:
You can even define a function lookupItemController which can dynamically decide the item controller (eg based on model type perhaps).
The thing I found when rendering a collection wrapped in an ArrayController within another template/view is the way #each is used. Make sure you use {{#each controller}} as Teddy Zeeny shows otherwise you end up using the content model items and NOT the item controller wrapped items. You may not notice this until you try using actions which are intended to be handled by the item controller or other controller based content decoration.
When I need to nest an entire collection in another view I use the view helper as follows to set the context correctly so that any collection level actions (eg an add item button action) get handled by the array controller and not by the main controller setup by the route.
So in my products template I would do something like this to list the nested suppliers (assuming your route for 'product' has properly the 'suppliers' controller):
{{view controller=controllers.suppliers templateName="products/suppliers"}}
The suppliers template just follows the same pattern as the template I show above.

What's the best idiom for creating an EmberJS view that can show all its child-views, or just one?

Say I have a model App.Page, an ArrayController of App.Page, and an App.PageView to render each App.Page.
I'm trying to figure out how to best implement App.MyPagesView so it works like so:
if App.showAllPages is true: I want MyPagesView to contain an App.PageView(s) for displaying each of the App.Page in App.pages
Else: I want MyPagesView only show one App.PageView, bound to App.pages.currentPage.
The most straightforward implementation that occurs to me is using a template like so:
// MyPagesViewApproach1
{{#unless App.showAllPages}}
{{view App.PageView pageBinding="pages.currentPage"}}
{{else}}
{{#each pages}}
{{view App.PageView pageBinding="this"}}
{{/each}}
{{/unless}}
But won't this create new views for the existing models every time the user toggles showAllPages on and off? Also, I get emberJS warnings about performance issues when I try to use this template.
The PageView(s) could be quite complex and expensive to render. I'd really like to create a PageView once for each Page, and just remove/hide the irrelevant PageViews from the DOM when they're not in use.
App = Ember.Application.create({
showAllPages: false,
pages: Ember.ArrayController.create({
content: []
currentPage: null
}),
ready: function () {
this.pages.pushObject(App.Page.create({title: 'Page One'});
this.pages.pushObject(App.Page.create({title: 'Some Other Page'});
this.pages.pushObject(App.Page.create({title: 'Grrreatest Page Evar'});
this.pagesController.set('currentPage',
this.pagesController.get('firstObject'));
}
});
App.Page = Ember.Object.extend({
title: null
// etc, etc...
});
App.PageView = Ember.View.extend({
templateName: 'page',
page: null // should be bound to an App.Page
});
App.MyPagesView_Approach1 = Ember.View.extend({
pagesBinding: 'Elicitation.pages'
// ???
});
App.MyPagesView_Approach2 = Ember.ContainerView.extend({
// ???
});
And my HTML:
<script type="text/x-handlebars" data-template-name="page">
The title of this page is {{ page.title }}
</script>
{{view App.MyPagesView }}
To recap, what's the proper EmberJS-y way to implement MyPagesView so it responds to App.showAllPages without re-creating all the views each time its toggled?
Should it be some sort of ContainerView? Or should I use the unless/else template shown at the top of the question? Or something entirely different? I feel like a really simple solution exists in EmberJS, but its elluding me.
Here's the best I've come up with, encapsulated as a re-usable View class called "CurrentCollectionView". I'm using CollectionView, and using view.set('isVisible') to hide/show appropriate child views. Basically use it like a CollectionView, but you can set currentContent to hide all but one element of content, or use showAllContent to override currentContent.
App.CurrentCollectionView = Ember.CollectionView.extend({
showAllContent: false,
currentContent: null,
currentContentChanged: function () {
console.log("Elicitation.PagesView.currentContentChanged()");
var showAllContent = this.get('showAllContent');
if (Ember.none(showAllContent) || !showAllContent) {
var contents = this.get('content');
var currentContent = this.get('currentContent');
this.get('childViews').forEach(function (view, i) {
var isVisible = contents.objectAt(i) == currentContent;
view.set('isVisible', isVisible);
});
} else {
this.get('childViews').forEach(function (view) {
view.set('isVisible', true);
});
}
}.observes('currentContent', 'showAllContent', 'childViews')
});
An example of using CurrentCollectionView to implement MyPagesView:
App.MyPagesView = App.CurrentCollectionView.extend({
itemViewClass: App.PageView,
contentBinding: 'App.pages',
currentContentBinding: 'App.pages.currentPage',
showAllContentBinding: 'App.showAllPages',
});
or as using it inline as a template:
{{view App.CurrentCollectionView itemViewClass="App.PageView" contentBinding="App.pages" currentContentBinding="App.pages.currentPage" showAllContentBinding="App.showAllPages"}}
Hope somebody else finds this useful and/or can improve on it (please!)