How to bind value form input select to attribute in controller - ember.js

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.

Related

Ember.js link-to inside #each block does not render

<script type="text/x-handlebars" data-template-name="application">
<ul>
{{#each person in controller}}
<li>{{#link-to 'foo' person}}{{person.firstName}}{{/link-to}}</li>
{{/each}}
</ul>
</script>
<script type="text/x-handlebars" data-template-name="foo">
<h5>Foo route</h5>
{{name}}: converted to {{fullName}}
</script>
javascript:
App = Ember.Application.create({});
App.ApplicationRoute = Ember.Route.extend({
model: function(){
return [
{firstName: 'Kris', lastName: 'Selden'},
{firstName: 'Luke', lastName: 'Melia'},
{firstName: 'Formerly Alex', lastName: 'Matchneer'}
];
}
});
App.Router.map(function() {
this.route('foo');
});
App.FooRoute = Ember.Route.extend({
model: function(params) {
// just ignore the params
return Ember.Object.create({
name: 'something'
});
}
});
App.FooController = Ember.ObjectController.extend({
fullName: function() {
return this.get('name') + ' Jr.';
}
});
I must be doing something wrong, because these {{#link-to}}'s are failing. Here's a JSBin. Hesitating to file an issue because this seems like such a simple thing:
http://jsbin.com/ucanam/4777/edit?html,js,output
Your route does not allow parameter. Change it to
this.resource('foo', { path: '/:person_id'});

Ember.js: Upload file component

I need to create an Ember component to select a file.
My page will include multiple "upload component"
I have read a post trying to implement that: (https://stackoverflow.com/questions/9200000/file-upload-with-ember-data) BUT the UploadFileView is directly linked to the controller.
I would like to have something more generic...
I would like to remove the App.StoreCardController.set('logoFile'..) dependency from the view or pass the field (logoFile) from the template...
Any idea to improve this code ?
App.UploadFileView = Ember.TextField.extend({
type: 'file',
attributeBindings: ['name'],
change: function(evt) {
var self = this;
var input = evt.target;
if (input.files && input.files[0]) {
App.StoreCardController.set('logoFile', input.files[0]);
}
}
});
and the template:
{{view App.UploadFileView name="icon_image"}}
{{view App.UploadFileView name="logo_image"}}
I completed a full blown example to show this in action
https://github.com/toranb/ember-file-upload
Here is the basic handlebars template
<script type="text/x-handlebars" data-template-name="person">
{{view PersonApp.UploadFileView name="logo" contentBinding="content"}}
{{view PersonApp.UploadFileView name="other" contentBinding="content"}}
<a {{action submitFileUpload content target="parentView"}}>Save</a>
</script>
Here is the custom file view object
PersonApp.UploadFileView = Ember.TextField.extend({
type: 'file',
attributeBindings: ['name'],
change: function(evt) {
var self = this;
var input = evt.target;
if (input.files && input.files[0]) {
var reader = new FileReader();
var that = this;
reader.onload = function(e) {
var fileToUpload = reader.result;
self.get('controller').set(self.get('name'), fileToUpload);
}
reader.readAsDataURL(input.files[0]);
}
}
});
Here is the controller
PersonApp.PersonController = Ember.ObjectController.extend({
content: null,
logo: null,
other: null
});
And finally here is the view w/ submit event
PersonApp.PersonView = Ember.View.extend({
templateName: 'person',
submitFileUpload: function(event) {
event.preventDefault();
var person = PersonApp.Person.createRecord({ username: 'heyo', attachment: this.get('controller').get('logo'), other: this.get('controller').get('other') });
this.get('controller.target').get('store').commit();
}
});
This will drop 2 files on the file system if you spin up the django app
EDIT (2015.06): Just created a new solution based on a component.
This solution provides an upload button with a preview and remove icon.
P.S. The fa classes are Font Awesome
Component handlebars
<script type="text/x-handlebars" data-template-name='components/avatar-picker'>
{{#if content}}
<img src={{content}}/> <a {{action 'remove'}}><i class="fa fa-close"></i></a>
{{else}}
<i class="fa fa-picture-o"></i>
{{/if}}
{{input-image fdata=content}}
</script>
Component JavaScript
App.AvatarPickerComponent = Ember.Component.extend({
actions: {
remove: function() {
this.set("content", null);
}
}
});
App.InputImageComponent = Ember.TextField.extend({
type: 'file',
change: function (evt) {
var input = evt.target;
if (input.files && input.files[0]) {
var that = this;
var reader = new FileReader();
reader.onload = function (e) {
var data = e.target.result;
that.set('fdata', data);
};
reader.readAsDataURL(input.files[0]);
}
}
});
Usage example
{{avatar-picker content=model.avatar}}
Old Answer
I took Chris Meyers example, and I made it small.
Template
{{#view Ember.View contentBinding="foto"}}
{{view App.FotoUp}}
{{view App.FotoPreview width="200" srcBinding="foto"}}
{{/view}}
JavaScript
App.FotoPreview= Ember.View.extend({
attributeBindings: ['src'],
tagName: 'img',
});
App.FotoUp= Ember.TextField.extend({
type: 'file',
change: function(evt) {
var input = evt.target;
if (input.files && input.files[0]) {
var that = this;
var reader = new FileReader();
reader.onload = function(e) {
var data = e.target.result;
that.set('parentView.content', data);
}
reader.readAsDataURL(input.files[0]);
}
},
});
Marek Fajkus you cannot use JQuery's .serialize, it makes no mention of file uploads in the documentation at JQuery UI docs
However, you could use JQuery Upload Plugin
Actually it does mention it, it says:
". Data from file select elements is not serialized."
In case of uploading multiple files, you may want to use
{{input type='file' multiple='true' valueBinding='file'}}
^^^^
This is a solution that you would use in normal HTML upload.
Additionally, you can use 'valueBinding' which will allow you to set up an observer against that value in your component.

{{action}} link with transitionTo using relationship id

Given a view with a context like { id: 1, form_id: 5}, I want to create an {{action}} link to the form using the form_id.
My view code looks like:
<script type="text/x-handlebars" data-template-name="group">
{{action showForm form_id href=true}}
</script>
And the action in my router looks like:
showForm: function(router, event) {
var form_id = event.context;
router.transitionTo('root.form', { id: form_id });
},
I get an error that reads:
Uncaught Error: assertion failed: You must specify a target state for event 'showForm' in order to link to it in the current state 'root.index'.
I'm guessing that the problem is with the way I'm setting up the context for transitionTo, but I haven't been able to figure out the correct solution.
Here is the full code to reproduce the problem:
<script type="text/x-handlebars" data-template-name="application">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="group">
{{action showForm form_id href=true}}
</script>
MyApp = Ember.Application.create({
autoinit: false
});
MyApp.router = Ember.Router.create({
root: Ember.Route.extend({
index: Ember.Route.extend({
route: '/',
// Throws error:
// You must specify a target state for event 'showForm' in
// order to link to it in the current state 'root.index'
//
showForm: function(router, event) {
var form_id = event.context;
router.transitionTo('root.form', { id: form_id });
},
// Won't work because form deserialize finds id, not form_id
//showForm: Em.Route.transitionTo('root.form'),
// This won't work either
// showForm: Em.Route.transitionTo('root.form', { id: this.form_id }),
connectOutlets: function( router, context ){
var group = Em.Object.create({ id:1, form_id: 5 });
router.get( 'applicationController' ).connectOutlet( 'group', group );
}
}),
form: Ember.Route.extend({
route: '/form/:id',
serialize: function( router, context ){
return { id: context.id }
},
deserialize: function( router, context ){
var form = Em.Object.create({ id: 5, name: 'my form' });
return MyApp.Form.find( context.id );
},
connectOutlets: function( router, context ){
// left out for fiddle example
}
})
})
});
MyApp.ApplicationController = Ember.Controller.extend({});
MyApp.GroupController = Em.ObjectController.extend({});
MyApp.GroupView = Em.View.extend({ templateName: 'group' });
MyApp.initialize(MyApp.router);​
And the cooresponding fiddle:
http://jsfiddle.net/jefflab/LJGCz/
I was able to come up with an ugly solution to this problem using a computed property as the context of my action. The key snippets are:
<script type="text/x-handlebars" data-template-name="group">
<a {{action showForm view.formHash href=true}}>go to form</a>
</script>
MyApp.GroupView = Em.View.extend({
templateName: 'group',
formHash: function() {
return { id: this.get('controller').get('form_id') };
}.property('form_id')
});
And the working fiddle is here:
http://jsfiddle.net/jefflab/pGv8u/
However, after talking to Tom Dale, it is clear that the "right way" to solve to solve this problem is by using materialized objects instead of id values. If you are using Ember data, this is a great use case for the "sideloading" belongsTo feature.

ember.js how to set title of option for Ember.Select

I am a starter of Ember and I try to use Ember.js(1.0.0.pre) in my app.
I am trying to set title for my Ember.Select's options to show tips when mouseover.
But, I can't find any information about the option's title in API.
Do I have to write a function myself to populate the "title" attribute?
Is there any way like "optionLabelPath" to bind "title" attribute for options?
To achieve this we need to reopen the Ember.SelectOption
here is the fiddle for the following example
MyApp = Ember.Application.create();
Ember.SelectOption.reopen({
attributeBindings: ['title'],
title: function() {
var titlePath = this.getPath('parentView.optionTitlePath');
return this.getPath(titlePath);
}.property('parentView.optionTitlePath')
});
MyApp.selectArray = [{
label: "A",
id: "1",
title: "for Apple"
},
{
label: "B",
id: "2",
title: "for Ball"
}];
Handlebars
<script type="text/x-handlebars" >
{{view Ember.Select
contentBinding="MyApp.selectArray"
optionLabelPath="content.label"
optionValuePath="content.id"
optionClassPath="content.title"
}}
</script>​
​
Here is the simplest I could come up with:
http://jsfiddle.net/aK8JH/1/
Template:
{{view MyApp.Select contentBinding="content"}}
View:
MyApp.Select = Ember.Select.extend({
attributeBindings: ['title'],
title: 'myTitle'
});
Read this: http://emberjs.com/documentation/#toc_attribute-bindings-on-a-view
Below is what I've got after observing source code in Ember:
Ember.SelectOption.reopen({
attributeBindings: ['title'],
init: function() {
this.titlePathDidChange();
this._super();
},
titlePathDidChange: function() {
var titlePath = this.get('parentView.optionTitlePath');
if (!titlePath) { return; }
Ember.defineProperty(this, 'title', Ember.computed(function() {
return this.get(titlePath);
}).property(titlePath));
}.observes('parentView.optionTitlePath')
});

Ember.js view for object

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/