Iterating over a model's attributes in EmberJS handlebars template - ember.js

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.

Related

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.

Creating a new record not pulling data from template fields

I am attempting to create a new record, however none of the data from the fields is being passed automatically, as I expected Ember to (from what I've read).
My template:
<form {{action save content on="submit"}}>
{{input value=name}}
<button type="submit"}}>Next</a>
From what I've read content is an alias for model and interchanging these makes no difference.
My route:
App.CampaignsNewRoute = Ember.Route.extend({
actions: {
save: function(campaign) {
console.log(campaign.name);
}
},
model: function(controller) {
return this.store.createRecord('campaign');
}
});
And my controller:
App.CampaignsNewController = Ember.ObjectController.extend({
pageTitle: 'New Campaign Setup'
});
When I hit 'Next' it logs undefined. Logging just the campaign shows it's an Ember model, but without the name attribute. name is defined on the campaign model. Setting the input to {{input value=content.name}} places the name attribute within the model returned, but it's still undefined. Am I missing anything in this process? The EmberJS site doesn't show how to do this, from what I can find.
--
As a side note: I was originally using App.CampaignsNewController = Ember.Controller.extend as my model was returning a hash of promises, one of which is an array and Ember didn't like me using either array or object controller. I simplified it to the above to verify it wasn't that which was causing the issue. So any solution taking this into account would be wonderful.
Edit: I can access the template fields by doing this.get('controller').get('name') but surely that is not necessary? Changing my controller to a Ember.Controller.extend also stops that from working, would love to know why. Clarification on best practice here would still be wonderful!
Edit2: this.get('controller.content').get('name') works if the controller is simply an Ember.Controller as opposed to Ember.ObjectController and the template has {{input value=content.name}}. I'll work with but hopefully someone can clarify this is the correct way.
ObjectController is the way to go here. You would have it backed by one particular model, your new model, and you would add additional properties to the controller for use in the template.
Code
App.IndexRoute = Ember.Route.extend({
actions: {
save: function(campaign) {
console.log(campaign.get('color'));
}
},
model: function() {
return Ember.RSVP.hash({
record: this.store.createRecord('color'),
all: this.store.find('color')
});
},
setupController: function(controller, model){
this._super(controller, model.record);
controller.set('allColors', model.all);
}
});
App.IndexController = Em.ObjectController.extend({
});
Template
In the template any time you want to access anything on the model backing the template, you can just access it as if the model is the current scope.
{{name}}
if you want to access any of the properties that exist on the controller you would use the property name that it is on the controller.
{{allColors.length}}
Here's an example:
<form {{action save model on="submit"}}>
Color:{{input value=color}}<br/>
<button type="submit">Next</button>
</form>
<ul>
{{#each item in allColors}}
{{#unless item.isNew}}
<li>{{item.color}}</li>
{{/unless}}
{{/each}}
</ul>
One last tip, always use getters and setters ;)
Ember Data hides the properties, they don't live right on the object, so campaign.name will return undefined forever and ever. If you do campaign.get('name') you'll get a real response.
With the example: http://emberjs.jsbin.com/OxIDiVU/792/edit

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.

text field filtering a list using ember + ember data

I'm new at using ember, but already familiar with it, basically following some tutorials here and there and reading the api docs. But tutorials don't go too deep into more complex topics.
These are the details: I already implemented a web page that shows a list of items. The following are the relevant code excerpts from different parts of the app.
// the data model, the view and the controller
App.Item = DS.Model.extend({
name: DS.attr('string')
});
App.ItemsController = Ember.ArrayController.extend();
App.ItemsView = Ember.View.extend({ templateName: 'items' })
// in the router's corresponding route
connectOutlets: function(router) {
router.get('applicationController').connectOutlet('items', App.Item.find())
}
// in the handlebars template
<ul class="items">
{{#each content}}
<li>{{name}}</li>
{{/each}}
</ul>
The data for this list is loaded remotely via ember-data (notice the App.Item.find() call in the route's connectOutlet method above) and a handlebar template displays it, and dynamically updates the list as the data changes. Up to here this is basic ember.
Now I want to have a text field at the top of the list, and when the user types in this text field, the list should be updated, by filtering and showing only the items with a name that matches what the user is typing. The actual definition of what a matching name is, is irrelevant to this question. It could be those names that contain the typed string, or that start with it.
I know my next step is to include a textfield view on top of the list in the handlebars template:
<div class="search-bar">
{{view Ember.TextField}}
</div>
<ul class="items">
{{#each content}}
<li>{{name}}</li>
{{/each}}
</ul>
So my questions at this point are the following:
How do I refer to this text field in javascript code so I can attach a listener to it to detect when it changes?
And more importantly, what do I need to do inside this event listener so the list gets filtered?
I would like to know how to achieve it filtering data loaded locally, but also how to do it by loading the filtering data remotely everytime the user types.
I actually need to implement something slightly more complex than this, but knowing how to do this will help.
You can have a computed property on your controller that filters the content based on a text field.
App.ItemsController = Ember.ArrayController.extend({
// ...
searchedContent: function() {
var regexp = new RegExp(this.get('search'));
return this.get('content').filter(function(item) {
return regexp.test(item.get('name'));
});
}.property('search', 'content.#each.name')
});
Then you just bind your text field to controller.search. Example: http://www.emberplay.com#/workspace/2356272909
To filter data remotely you should have ember data load more items every time search changes. This can be done by sending an event to the router every time there is a change.