Setting itemController on a filtered subset of an ArrayController's model - ember.js

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

Related

Display a subset of model data in emberjs

I have the following in my routes
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.query('post',{published:true});
}
});
and a post-viewer component that renders the model. the problem that i am facing with is filter. How can i implement the same without loading the models each time. Currently i am just passing the models in the component and using the following
{{#each posts as |item|}}
{{/each}}
To render the elements. What is the proper way by which lets say i can filter them based on title containing some specific keyword. I tried using this.store.query inside the each loop but that did not work out.
If you use this.store.query ember does not cache the result. So probably you should use a .findAll() and then filter the data on the client side. A simple way to do so is inside your model() hook:
return this.store.findAll('post')
.then(posts => posts.filter(post => get(post, 'published') === true));
This will work because ember-data does cache the result of the findAll() and the filter executes on the client. You can do the same with a computed property. This has some benefits, as, for example, you can filter based on another property. A computed property in your controller for example:
filteredModel: Ember.computed('model.#each.name', 'searchName', {
get() {
return get(this, 'model').filter(record => get(record, 'name') === get(this, 'searchName'));
}
});
You can use
https://github.com/DockYard/ember-composable-helpers
and filter/filterBy
filter -Filters an array by a callback.
{{#each (filter (action "isActive") users) as |user|}}
{{user.name}} is active!
{{/each}}
filter-by Filters an array by a property.
{{#each (filter-by "isActive" true users) as |user|}}
{{user.name}} is active!
{{/each}}
If you omit the second argument it will test if the property is truthy.
{{#each (filter-by "address" users) as |user|}}
{{user.name}} has an address specified!
{{/each}}
You can also pass an action as second argument:
{{#each (filter-by "age" (action "olderThan" 18) users) as |user|}}
{{user.name}} is older than eighteen!
{{/each}}
P.S.
Another variant ( pure Ember.js ) - create computed property and iterate over it. And inside computed property filter items

Ember: Update ObjectController property from ArrayController action?

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

Using Ember.js, how do I get a template to show dynamically all of the properties of a model? [duplicate]

Is there a way to iterate over a view's context's attributes in EmberJS? I am using Ember-Data (https://github.com/emberjs/data) for ORM.
Lets say I use connectOutlets to register a UserView with a user that has attributes such as email, name, etc. In the connected Handlebars template, is there anyway that I can iterate over those attributes?
I basically need to build a generic view that can be reused with different models...
Ryan is right about the attributes, but it takes some doing to actually get where you're going. My examples here are using the latest RC1 Ember.
Here is an editor template that is model agnostic:
<script type="text/x-handlebars" data-template-name="edit_monster">
{{#if clientId}}
<h1>Edit Monster: {{name}}</h1>
<div>
{{#each metadata}}
<span class="edit-label">{{name}}</span>
<span class="edit-field">
{{view App.AutoTextField typeBinding="type" nameBinding="name" }}
</span>
{{/each}}
</div>
{{else}}
No monster selected.
{{/if}}
</script>
To make that work, we need a couple of pieces of magic-magic. This controller is a good start:
App.EditMonsterController = Em.ObjectController.extend({
metadata: function() {
var vals = [];
var attributeMap = this.get('content.constructor.attributes');
attributeMap.forEach(function(name, value) {
vals.push(value);
});
return vals;
}.property('content')
});
That uses that "attributes" property that Ryan mentioned to provide the metadata that we are feeding into our #each up there in the template!
Now, here is a view that we can use to provide the text input. There's an outer container view that is needed to feed the valueBinding in to the actual textfield.
App.AutoTextField = Ember.ContainerView.extend({
type: null,
name: null,
init: function() {
this._super();
this.createChildView();
},
createChildView: function() {
this.set('currentView', Ember.TextField.create({
valueBinding: 'controller.' + this.get('name'),
type: this.get('type')
}));
}.observes('name', 'type')
});
Here is a fiddle demonstrating the whole crazy thing: http://jsfiddle.net/Malkyne/m4bu6/
The Ember Data objects that represent your models have an attributes property that contains all of the attributes for the given model. This is what Ember Data's toJSON uses to convert your models into Javascript objects.
You can use this attributes property to read a models attributes and then pull those specific attributes out of an instance. Here is an example.
http://jsfiddle.net/BdUyU/1/
Just to reiterate what's going on here. We are reading the attributes from App.User and then pulling the values out of App.ryan and App.steve. Hope this makes sense.

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.

Iterating over a model's attributes in EmberJS handlebars template

Is there a way to iterate over a view's context's attributes in EmberJS? I am using Ember-Data (https://github.com/emberjs/data) for ORM.
Lets say I use connectOutlets to register a UserView with a user that has attributes such as email, name, etc. In the connected Handlebars template, is there anyway that I can iterate over those attributes?
I basically need to build a generic view that can be reused with different models...
Ryan is right about the attributes, but it takes some doing to actually get where you're going. My examples here are using the latest RC1 Ember.
Here is an editor template that is model agnostic:
<script type="text/x-handlebars" data-template-name="edit_monster">
{{#if clientId}}
<h1>Edit Monster: {{name}}</h1>
<div>
{{#each metadata}}
<span class="edit-label">{{name}}</span>
<span class="edit-field">
{{view App.AutoTextField typeBinding="type" nameBinding="name" }}
</span>
{{/each}}
</div>
{{else}}
No monster selected.
{{/if}}
</script>
To make that work, we need a couple of pieces of magic-magic. This controller is a good start:
App.EditMonsterController = Em.ObjectController.extend({
metadata: function() {
var vals = [];
var attributeMap = this.get('content.constructor.attributes');
attributeMap.forEach(function(name, value) {
vals.push(value);
});
return vals;
}.property('content')
});
That uses that "attributes" property that Ryan mentioned to provide the metadata that we are feeding into our #each up there in the template!
Now, here is a view that we can use to provide the text input. There's an outer container view that is needed to feed the valueBinding in to the actual textfield.
App.AutoTextField = Ember.ContainerView.extend({
type: null,
name: null,
init: function() {
this._super();
this.createChildView();
},
createChildView: function() {
this.set('currentView', Ember.TextField.create({
valueBinding: 'controller.' + this.get('name'),
type: this.get('type')
}));
}.observes('name', 'type')
});
Here is a fiddle demonstrating the whole crazy thing: http://jsfiddle.net/Malkyne/m4bu6/
The Ember Data objects that represent your models have an attributes property that contains all of the attributes for the given model. This is what Ember Data's toJSON uses to convert your models into Javascript objects.
You can use this attributes property to read a models attributes and then pull those specific attributes out of an instance. Here is an example.
http://jsfiddle.net/BdUyU/1/
Just to reiterate what's going on here. We are reading the attributes from App.User and then pulling the values out of App.ryan and App.steve. Hope this makes sense.