Ember.js view for object - templates

I have simple view in my Ember.js application, like this. Each "content" element consists of two objects, first and second:
{{#each App.myController.content}}
{{view for content.first}}
{{view for content.second}}
{{/each}}
I'd like to define view for each content separately (so as not to have to write it twice), in another handlebars template script. How can I pass the first and second variables to the view?
Here is a code sample, see http://jsfiddle.net/Zm4Xg/5/:
Handlebars:
<script type="text/x-handlebars" data-template-name="contact-view">
<div>{{name}}</div>
<img {{bindAttr src="avatar"}} {{bindAttr alt="name"}}>
</script>
<script type="text/x-handlebars">
{{#each App.contactsController.pair}}
<div class="menu_vertical_group">
{{#with this.first}}
{{view App.contactView}}
{{/with}}
{{#with this.second}}
{{view App.contactView}}
{{/with}}
</div>
{{/each}}
</script>
​JavaScript:
App = Ember.Application.create();
App.Contact = Em.Object.extend({
name: null,
avatar: null
});
App.contactView = Ember.View.extend({
templateName: 'contact-view'
});
App.contactsController = Em.ArrayController.create({
content: [],
initData: function(data) {
var contacts = data.map(function(contact) {
return App.Contact.create({
"name": contact.name,
"avatar": contact.avatar
});
});
this.pushObjects(contacts);
},
pair: (function() {
content = this.get('content');
var result = [];
for (ii = 0; ii < content.length; ii += 2) {
result.pushObject({
"first": content[ii],
"second": content[ii + 1] ? content[ii + 1] : null
});
}
return result;
}).property('content')
});
App.contactsController.initData([{
"name": "John Doe",
"avatar": "/john.jpg"},
{
"name": "Someone Else",
"avatar": "/else.jpg"}]);​

Something like this?
{{#each App.myController.content}}
{{view MyView contentBinding="content.first"}}
{{view MyView contentBinding="content.second"}}
{{/each}}

You can extend the View class with a templateName function that evaluates to a different view based on a property of the model, like this:
App.customView = Ember.View.extend({
templateName:function(){
if(this.get('content').get('index') === 1){
return 'view1';
}
else{
return 'view2';
}
}.property('content.index') // custom template function based on 'index' property
});
Check out this fiddle: http://jsfiddle.net/lifeinafolder/7hnc9/

Related

Ember.js: programmatically load hasmany relationship

Disclamer: I started using Ember.js few days ago
Suppose having these 2 models:
App.Question = DS.Model.extend({
text: DS.attr('string'),
answers: DS.hasMany('answer', {async: true})
});
App.Answer = DS.Model.extend({
question: DS.belongsTo('question'),
text: DS.attr('string')
});
For some reasons I got this JSON (without "answers" list)
{
...
"questions": [
{"id": 1, "text": "Foo?"}, ...
]
}
In the template I want to load and show answers only if explicitly needed
{{#each questions itemController="question"}}
<div class="answer-wrapper">
{{text}}
<button {{action "loadAnswers"}}>Load answers</button>
<ul>
{{#each answers}}
<li>{{text}}</li>
{{/each}}
</ul>
</div>
{{/each}}
How can I do this in the controller's loadAnswer action?
App.QuestionController = Ember.ObjectController.extend({
...
actions: {
loadAnswers: function() {
// programatically load answers
}
}
});
Workaround: I can do this changing the attribute in the template
{{#each loadedAnswers}}
<li>{{text}}</li>
{{/each}}
and defining correctly the action
App.QuestionController = Ember.ObjectController.extend({
...
actions: {
loadAnswers: function() {
// or loaded with ajax...
this.set("loadedAnswers", [
{id: 1, text: "foo"},
{id: 2, text: "bar"}
]);
}
}
});
You can wrap that portion of the template with an if statement. The async relationship won't be fetched unless it's requested.
Controller
App.QuestionController = Ember.ObjectController.extend({
showAnswers: false,
...
actions: {
loadAnswers: function() {
this.set('showAnswers', true);
}
}
});
Template
{{#if showAnswers}}
<ul>
{{#each answers}}
<li>{{text}}</li>
{{/each}}
</ul>
{{/if}}

Template not updating when controller property changes

Caveat: This is part of my first ember app.
I have an Ember.MutableArray on a controller. The corresponding view has an observer that attempts to rerender the template when the array changes. All the changes on the array (via user interaction) work fine. The template is just never updated. What am I doing wrong?
I'm using Ember 1.2.0 and Ember Data 1.0.0-beta.4+canary.7af6fcb0, though I guess the latter shouldn't matter for this.
Here is the code:
var ApplicationRoute = Ember.Route.extend({
renderTemplate: function() {
this._super();
var topicsController = this.controllerFor('topics');
var topicFilterController = this.controllerFor('topic_filter');
this.render('topics', {outlet: 'topics', controller: topicsController, into: 'application'});
this.render('topic_filter', {outlet: 'topic_filter', controller: topicFilterController, into: 'application'});
},
});
module.exports = ApplicationRoute;
var TopicFilterController = Ember.Controller.extend({
topicFilters: Ember.A([ ]),
areTopicFilters: function() {
console.log('topicFilters.length -> ' + this.topicFilters.length);
return this.topicFilters.length > 0;
}.property('topicFilters'),
getTopicFilters: function() {
console.log('getTopicFilters....');
return this.store.findByIds('topic', this.topicFilters);
}.property('topicFilters'),
actions: {
addTopicFilter: function(t) {
if(this.topicFilters.indexOf(parseInt(t)) == -1) {
this.topicFilters.pushObject(parseInt(t));
}
// this.topicFilters.add(parseInt(t));
console.log('topicFilters -> ' + JSON.stringify(this.topicFilters));
},
removeTopicFilter: function(t) {
this.topicFilters.removeObject(parseInt(t));
console.log('topicFilters -> ' + JSON.stringify(this.topicFilters));
}
}
});
module.exports = TopicFilterController;
var TopicFilterView = Ember.View.extend({
topicFiltersObserver: function() {
console.log('from view.... topicFilters has changed');
this.rerender();
}.observes('this.controller.topicFilters.[]')
});
module.exports = TopicFilterView;
// topic_filter.hbs
{{#if areTopicFilters}}
<strong>Topic filters:</strong>
{{#each getTopicFilters}}
<a {{bind-attr href='#'}} {{action 'removeTopicFilter' id}}>{{topic}}</a>
{{/each}}
{{/if}}
var TopicsController = Ember.ArrayController.extend({
needs: ['topicFilter'],
all_topics: function() {
return this.store.find('topic');
}.property('model', 'App.Topic.#each'),
actions: {
addTopicFilter: function(t) {
App.__container__.lookup('controller:topicFilter').send('addTopicFilter', t);
}
}
});
module.exports = TopicsController;
// topics.hbs
<ul class="list-group list-unstyled">
{{#each all_topics}}
<li class="clear list-group-item">
<span class="badge">{{entryCount}}</span>
<a {{bind-attr href="#"}} {{action 'addTopicFilter' id}}>{{topic}}</a>
</li>
{{/each}}
</ul>
your observes should just be controller.topicFilters.[]
And honestly this is a very inefficient way of doing this, rerendering your entire view because a single item changed on the array. If you show your template I can give you a much better way of handling this.
Here's an example, I've changed quite a few things, and guessed on some others since I don't know exactly how your app is.
http://emberjs.jsbin.com/uFIMekOJ/1/edit

Ember Handlebars helper view not updated

I'm trying to use Handlebars helper, but the helper view does not get updated.
The view,
<script type="text/x-handlebars">
{{#view App.NumberView}}
<button {{action changeNumber}}>Change number</button><br>
{{formatNumber view.number}} <br>
{{view.number}}
{{/view}}
</script>​
The code,
App = Ember.Application.create({});
App.NumberView = Ember.View.extend({
number: 5,
changeNumber: function(e) {
this.set('number', this.get('number') + 1);
}
});
Em.Handlebars.registerHelper('formatNumber', function(timePath, options) {
var number = Em.Handlebars.getPath(this, timePath, options);
return new Handlebars.SafeString("Formated number: " + number);
});
​
The live example in jsfiddle http://jsfiddle.net/LP7Hz/1/
So what's wrong ?
Because Handlebars helpers in Ember which aren't bound helpers. So you can instantiate an auxiliary view to do that like this fiddle:
http://jsfiddle.net/secretlm/qfNJw/2/
HTML:
<script type="text/x-handlebars">
{{#view App.NumberView}}
<button {{action changeNumber}}>Change number</button><br>
{{formatNumber view.number}} <br>
{{view.number}}
{{/view}}
</script>​
Javascript:
App = Ember.Application.create({});
App.NumberView = Ember.View.extend({
number: 5,
changeNumber: function(e) {
this.set('number', this.get('number') + 1);
}
});
App.registerViewHelper = function(name, view) {
Ember.Handlebars.registerHelper(name, function(property, options) {
options.hash.contentBinding = property;
return Ember.Handlebars.helpers.view.call(this, view, options);
});
};
inlineFormatter = function(fn) {
return Ember.View.extend({
tagName: 'span',
template: Ember.Handlebars.compile('{{view.formattedContent}}'),
formattedContent: (function() {
if (this.get('content') != null) {
return fn(this.get('content'));
}
}).property('content')
});
};
App.registerViewHelper('formatNumber', inlineFormatter(function(content) {
return new Handlebars.SafeString("Formated number: " + content);
}));
This link is useful: http://techblog.fundinggates.com/blog/2012/08/ember-handlebars-helpers-bound-and-unbound/ from #Jo Liss
​
You're looking for a bound helper, which doesn't exist just yet. There is a Pull Request and associated discussion.

Trigger an action in a text field from a button

I'm looking for advice on how to trigger this view function insertNewLine from a button (see view and template below). I'm guessing there's probably a better way to structure this code. Thanks for your help.
// view
App.SearchView = Ember.TextField.extend({
insertNewline: function() {
var value = this.get('value');
if (value) {
App.productsController.search(value);
}
}
});
// template
<script type="text/x-handlebars">
{{view App.SearchView placeholder="search"}}
<button id="search-button" class="btn primary">Search</button>
</script>
You could use the mixin Ember.TargetActionSupport on your TextField and execute triggerAction() when insertNewline is invoked. See http://jsfiddle.net/pangratz666/zc9AA/
Handlebars:
<script type="text/x-handlebars">
{{view App.SearchView placeholder="search" target="App.searchController" action="search"}}
{{#view Ember.Button target="App.searchController" action="search" }}
Search
{{/view}}
</script>
JavaScript:
App = Ember.Application.create({});
App.searchController = Ember.Object.create({
searchText: '',
search: function(){
console.log('search for %#'.fmt( this.get('searchText') ));
}
});
App.SearchView = Ember.TextField.extend(Ember.TargetActionSupport, {
valueBinding: 'App.searchController.searchText',
insertNewline: function() {
this.triggerAction();
}
});

How to bind value form input select to attribute in controller

I try to bind value from input select to attribute "selectedValue" in controller.
This is app.js
Food = Ember.Application.create();
Food.appsController = Ember.Object.create({
selectedValue: ""
});
Food.Todo = Ember.Object.extend({
title: null,
value: null
});
Food.FoodController = Ember.ArrayProxy.create({
content: []
});
Food.FoodController.pushObject(Food.Todo.create({title:"a", value:"1"}));
Food.FoodController.pushObject(Food.Todo.create({title:"b", value:"2"}));
Food.FoodController.pushObject(Food.Todo.create({title:"c", value:"3"}));
This is index.html
{{#collection
contentBinding="Todos.todosController"
tagName="select"
itemClassBinding="content.isDone"}}
{{content.title}}
{{/collection}}
Output look like this
<select id="ember180" class="ember-view">
<option id="ember192" class="ember-view">
<script id="metamorph-0-start" type="text/x-placeholder"></script>
a
<script id="metamorph-0-end" type="text/x-placeholder"></script>
</option>
<option id="ember196" class="ember-view">
<script id="metamorph-1-start" type="text/x-placeholder"></script>
b
<script id="metamorph-1-end" type="text/x-placeholder"></script>
</option>
<option id="ember200" class="ember-view">
<script id="metamorph-2-start" type="text/x-placeholder"></script>
c
<script id="metamorph-2-end" type="text/x-placeholder"></script>
</option>
</select>
I have no idea how to add value to option and how to binding selected value back to controller.
Is this possible to do in Emberjs?
Ember now has a built-in Select view.
You can find it in the latest Ember.js build here: http://cloud.github.com/downloads/emberjs/ember.js/ember-latest.js
Here's an example usage:
var App = Ember.Application.create();
App.Person = Ember.Object.extend({
id: null,
firstName: null,
lastName: null,
fullName: function() {
return this.get('firstName') + " " + this.get('lastName');
}.property('firstName', 'lastName').cacheable()
});
App.selectedPersonController = Ember.Object.create({
person: null
});
App.peopleController = Ember.ArrayController.create({
content: [
App.Person.create({id: 1, firstName: 'Yehuda', lastName: 'Katz'}),
App.Person.create({id: 2, firstName: 'Tom', lastName: 'Dale'}),
App.Person.create({id: 3, firstName: 'Peter', lastName: 'Wagenet'}),
App.Person.create({id: 4, firstName: 'Erik', lastName: 'Bryn'})
]
});
Your template would look like:
{{view Ember.Select
contentBinding="App.peopleController"
selectionBinding="App.selectedPersonController.person"
optionLabelPath="content.fullName"
optionValuePath="content.id"}}
Again, here's a jsFiddle example: http://jsfiddle.net/ebryn/zgLCr/
Jumping off from the solution for #pangrantz, this Fiddle example (http://jsfiddle.net/bsyjr/) illustrates some improvements: The Handlebars code is cleaner through the use of tagName. When tagName is set to "select", the child views automatically become "option" elements. See the Ember.CollectionView.CONTAINER_MAP in https://github.com/emberjs/ember.js/blob/master/packages/ember-views/lib/views/collection_view.js to understand why. On the Javascript side, by specifying an itemViewClass, we can add the value attribute to the option element.
<script type="text/x-handlebars" >
{{#collection Food.SelectView tagName="select" contentBinding="Food.foodController"
valueBinding="Food.appsController.selectedValue"}}
{{content.title}}
{{/collection}}
selected: {{view Ember.TextField valueBinding="Food.appsController.selectedValue"}}{{Food.appsController.selectedValue}}
</script>
Food = Ember.Application.create();
Food.SelectView = Ember.CollectionView.extend({
value: null,
itemViewClass: SC.View.extend({
attributeBindings:['value'],
valueBinding: 'content.value'
}),
valueChanged: function(){
this.$().val( this.get('value') );
}.observes('value'),
didInsertElement: function(){
var self = this;
this.$().change(function(){
var val = $('select option:selected').val();
self.set('value', val);
});
}
});
Food.appsController = Ember.Object.create({
selectedValue: ""
});
Food.Todo = Ember.Object.extend({
title: null,
value: null
});
Food.foodController = Ember.ArrayProxy.create({
content: []
});
Food.foodController.pushObject(Food.Todo.create({title:"a", value:"1"}));
Food.foodController.pushObject(Food.Todo.create({title:"b", value:"2"}));
Food.foodController.pushObject(Food.Todo.create({title:"c", value:"3"}));
There is still room for improvement in the event handling, which is not using Ember's event framework, and it would make a lot of sense to use a custom written SelectView that doesn't leverage Handlebars, since IMO, it is dubious how much value Handlebars adds in this case.
Using a custom Ember.View works for me, but I think there is a better solution...
See working example is this fiddle http://jsfiddle.net/pangratz/hcxrJ/
Handlebars:
{{#view Food.SelectView contentBinding="Food.foodController"
valueBinding="Food.appsController.selectedValue"}}
<select>
{{#each content}}
<option {{bindAttr value="value"}} >{{title}}</option>
{{/each}}
</select>
{{/view}}
app.js:
Food = Ember.Application.create();
Food.SelectView = Ember.View.extend({
value: null,
valueChanged: function(){
this.$('select').val( this.get('value') );
}.observes('value'),
didInsertElement: function(){
var self = this;
this.$('select').change(function(){
var val = $('select option:selected').val();
self.set('value', val);
});
}
});
Food.appsController = Ember.Object.create({
selectedValue: ""
});
Food.Todo = Ember.Object.extend({
title: null,
value: null
});
Food.foodController = Ember.ArrayProxy.create({
content: []
});
Food.foodController.pushObject(Food.Todo.create({title:"a", value:"1"}));
Food.foodController.pushObject(Food.Todo.create({title:"b", value:"2"}));
Food.foodController.pushObject(Food.Todo.create({title:"c", value:"3"}));
I'm not sure if this is of use to others, but I've been doing something similar based on the answers here, and have made this SelectView that should work in this context too. It binds to 'change', works out the view that is currently selected and then does something with its content.
Food.SelectView = Ember.CollectionView.extend({
change: function(e) {
var selected = this.$().find(":selected").index();
var content = this.get('childViews')[selected].content;
// Do something with the result
Food.appsController.set('selectedValue', content.title);
}
});
This way you're able to pass around an object rather than the index of the select.