I'm trying to produce a select input and pass the selected object to the change event on the view. The ember contact example uses a <ul> but with a select the view needs to be outside the each otherwise the change even isn't fired.
Here is the view js:
App.SelectView = Ember.View.extend({
change: function(e) {
//event for select
var content = this.get('content');
console.log(content);
App.selectedWidgetController.set('content', [content]);
},
click: function(e) {
//event for ul
var content = this.get('content');
console.log(content);
App.selectedWidgetController.set('content', [content]);
}
});
The ul from the example works:
<ul>
{{#each App.widgetController.content}}
{{#view App.SelectView contentBinding="this"}}
<li>{{content.name}}</li>
{{/view}}
{{/each}}
</ul>
But if I replace html directly, the change event isn't fired (which makes sense)
<select>
{{#each App.widgetController.content}}
{{#view App.SelectView contentBinding="this"}}
<option>{{content.name}}</option>
{{/view}}
{{/each}}
</select>
So I guess the select has to be wrapped in the view.. in which case how do I pass the relevant object?... This code results in the entire array being passed:
{{#view App.select_view contentBinding="App.widgetController.content"}}
<select>
{{#each App.widgetController.content}}
<option>{{name}}</option>
{{/each}}
</select>
{{/view}}
Ember now has a built-in Select view.
Here's a usage example:
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/
check out the answers to a similar question: How to bind value form input select to attribute in controller
In the examples a CollectionView is used with an tagName=select. You may find this helpful in getting it work.
EDIT: Since I was looking to implement a select myself, here is the solution I came up with:
views/form.js.hjs:
{{#view contentBinding="App.typeController" valueBinding="type" tagName="select"}}
{{#each content}}
<option {{bindAttr value="title"}}>{{title}}</option>
{{/each}}
{{/view}}
{{#view Ember.Button target="parentView" action="submitEntry"}}Save{{/view}}
The select is part of a form. I do check for the submit event and in there read the value:
app.js.coffee
# provides the select, add value: 'my_id' if you need differentiation
# between display name (title) and value
app.typeController = Ember.ArrayProxy.create
content: [{title:'Energy'}, {title:'Gas'}, {title:'Water'}]
# simplified version, but should prove the point
app.form_view = Ember.View.create
templateName: 'views_form'
type: null
submitEntry: () ->
console.log this.$().find(":selected").val()
Hope this helps.
This isn't an Answer, just a fix on the broken jsfiddle link.. Apparently jsfiddle has no love for ember :/ But JsBin does! http://jsbin.com/kuguf/1/edit
Related
I'm having a weird issue with the Ember.Select view when I try to bind its value to a model.
Here is an abstract of what I'm doing, the complete jsbin can be found here:
Using JavaScript: http://jsbin.com/jayutuzazi/1/edit?html,js,output
Using CoffeeScript: http://jsbin.com/nutoxiravi/2/edit?html,js,output
Basically what I'm trying to do is use an attribute of a model to set an attribute of another model.
I have the Age model like this
App.Age = DS.Model.extend({
label: DS.attr('string'),
base: DS.attr('number')
});
And an other model named Person like this
App.Person = DS.Model.extend({
name: DS.attr('string'),
ageBase: DS.attr('number')
});
The template looks like this:
<!-- person/edit.hbs -->
<form>
<p>Name {{input value=model.name}}</p>
<p>
Age
{{view "select" value=model.ageBase
content=ages
optionValuePath="content.base"
optionLabelPath="content.label"}}
</p>
</form>
What I am trying to do is have a select in the Person edit form that lists the ages using base as value and label as label.
I expect the correct value to be selected when loading and to change when the selected option changes.
Has can be seen in the jsbin output, the selected is correctly populated but it sets the ageBase value of the edited person to undefined and does not select any option. The model value is correctly set when an option is selected though.
Am I doing something wrong ? Is it a bug ? What am I supposed to do to make this work ?
Thank you :)
You can conditionally render based on fulfilment of the ages as follows, since select doesn't handle promises (more on that below):
{{#if ages.isFulfilled}}
{{view "select" value=ageBase
content=ages
optionValuePath="content.base"
optionLabelPath="content.label"}}
{{/if}}
I updated your JsBin demonstrating it working.
I also illustrate in the JsBin how you don't have to qualify with model. in your templates since object controllers are proxies to the models they decorate. This means your view doesn't have to be concerned with if a property comes from the model or some computed property on the controller.
There is currently a PR #9468 for select views which I made a case for getting merged into Ember which addresses some issues with selection and option paths. There is also meta issue #5259 to deal with a number of select view issues including working with promises.
From issue #5259 you will find that Ember core developer, Robert Jackson, has some candidate select replacements. I cloned one into this JsBin running against latest production release version of Ember.
There is nothing at all preventing you using Roberts code as a select view replacement in your app. Asynchronous collections/promises will just work (and it is MUCH faster rendering from the benchmarks I have seen).
The template for that component is just:
{{#if prompt}}
<option disabled>{{prompt}}</option>
{{/if}}
{{#each option in resolvedOptions}}
<option {{bind-attr value=option.id}}>{{option.name}}</option>
{{/each}}
The js of the component is:
App.AsyncSelectComponent = Ember.Component.extend({
tagName: 'select',
prompt: null,
options: null,
initialValue: null,
resolvedOptions: null,
resolvedInitialValue: null,
init: function() {
var component = this;
this._super.apply(this, arguments);
Ember.RSVP.hash({
resolvedOptions: this.options,
resolvedInitialValue: this.initialValue
})
.then(function(resolvedHash){
Ember.setProperties(component, resolvedHash);
//Run after render to ensure the <option>s have rendered
Ember.run.schedule('afterRender', function() {
component.updateSelection();
});
});
},
updateSelection: function() {
var initialValue = Ember.get(this, 'resolvedInitialValue');
var options = Ember.get(this, 'resolvedOptions') || [];
var initialValueIndex = options.indexOf(initialValue);
var prompt = Ember.get(this, 'prompt');
this.$('option').prop('selected', false);
if (initialValueIndex > -1) {
this.$('option:eq(' + initialValueIndex + ')').prop('selected', true);
} else if (prompt) {
this.$('option:eq(0)').prop('selected', true);
}
},
change: function() {
this._changeSelection();
},
_changeSelection: function() {
var value = this._selectedValue();
this.sendAction('on-change', value);
},
_selectedValue: function() {
var offset = 0;
var selectedIndex = this.$()[0].selectedIndex;
if (this.prompt) { offset = 1; }
return Ember.get(this, 'resolvedOptions')[selectedIndex - offset];
}
});
The problem is that in:
{{view "select" value=model.ageBase
When the app starts, value is undefined and model.ageBase gets synchronized to that before value is synchronized to model.ageBase. So, the workaround is to skip that initial undefined value.
See: http://jsbin.com/rimuku/1/edit?html,js,console,output
The relevant parts are:
template
{{view "select" value=selectValue }}
controller
App.IndexController = Ember.Controller.extend({
updateModel: function() {
var value = this.get('selectValue');
var person = this.get('model');
if ( value ) { // skip initial undefined value
person.set('ageBase', value);
}
}.observes('selectValue'),
selectValue: function() {
// randomly used this one
return this.store.find('age', 3);
}.property()
});
givanse's answer should work.
I don't think it's because value is undefined, but because value is just an integer (42) and not equal to any of the selects content, which are Person objects ({ id: 2, label: 'middle', base: 42 }).
You could do something similar to what givens suggests or use relationships.
Models
//
// Models
//
App.Person = DS.Model.extend({
name: DS.attr('string'),
ageBase: DS.belongsTo('age', { async: true })
});
App.Age = DS.Model.extend({
label: DS.attr('string'),
base: DS.attr('number')
});
//
// Fixtures
//
App.Person.reopenClass({
FIXTURES: [
{ id: 1, name: 'John Doe', ageBase: 2 }
]
});
App.Age.reopenClass({
FIXTURES: [
{ id: 1, label: 'young', base: 2 },
{ id: 2, label: 'middle', base: 42 },
{ id: 3, label: 'old', base: 82 }
]
});
Template:
<h1>Edit</h1>
<pre>
name: {{model.name}}
age: {{model.ageBase.base}}
</pre>
<form>
<p>Name {{input value=model.name}}</p>
<p>
Age
{{view "select" value=model.ageBase
content=ages
optionValuePath="content"
optionLabelPath="content.label"}}
</p>
</form>
Ok, I found a solution that I think is more satisfying. As I thought the issue was coming from ages being a promise. The solution was to ensure that the ages list was loaded before the page was rendered.
Here is how I did it:
App.IndexRoute = Ember.Route.extend({
model: function() {
return Ember.RSVP.hash({
person: this.store.find('person', 1),
ages: this.store.findAll('age')
});
}
});
That's it! All I need from there is to change the view according the new model:
{{#with model}}
<form>
<p>Name {{input value=person.name}}</p>
<p>
Age
{{view "select" value=person.ageBase
content=ages
optionValuePath="content.base"
optionLabelPath="content.label"}}
</p>
</form>
{{/with}}
The complete working solution can be found here: http://jsbin.com/qeriyohacu/1/edit?html,js,output
Thanks again to #givanse and #Larsaronen for your answers :)
I've got an app with basic functionality built out. I'm not going through and adding additional features. In this case I need to convert a simple button, currently using linkTo, to a View. Problem is that I'm not sure how to convert one to the other and still keep the link intact.
How do I do this conversion? Here's the code I have now:
<script type="text/x-handlebars" data-template-name="accountItem">
{{#each account in controller}}
{{#linkTo "account" account}}
<img {{bindAttr src="account.icon"}} />
{{/linkTo}}
{{/each}}
</script>
and here's the code I'm going to have:
<script type="text/x-handlebars" data-template-name="accountItem">
{{#each account in controller}}
{{#view "Social.AccountButtonView"}}
<img {{bindAttr src="account.icon"}} />
{{/view}}
{{/each}}
</script>
Social.AccountButtonView = Ember.View.extend({
tagName: 'a',
classNames: ['item-account'],
click: function(){
// do something
}
});
I would assume that I'd be building on top of the click handler in the View, but I'm not sure how to pass the reference to item being iterated over, nor how to reference the correct route within the View.
Assistance please?
Update 1
The first version renders an href attribute with a value of #/accounts/4 based on the Router I have set up:
Social.Router.map(function() {
this.resource('accounts', function(){
this.resource('account', { path: ':account_id'});
});
});
When I convert the current code to a view, how do I mimic the functionality that linkTo provides?
You can define a property binding for account in your handlebars template.
This binding works like this:
<script type="text/x-handlebars">
<h1>App</h1>
{{#each item in controller}}
{{#view App.AccountView accountBinding="item"}}
<a {{bindAttr href="view.account.url"}} target="_blank">
{{view.account.name}}
</a>
{{/view}}
{{/each}}
</script>
Note that I added accountBinding, so the general rule is propertyName and Binding as a suffix. And remember that when you add a property to a view, you will not be able to access it directly, instead you will have to access it with view.propertyName as shown above.
Just keep in mind that you must have a View class when using the {{view}} helper:
window.App = Em.Application.create();
App.AccountView = Em.View.extend(); // this must exist
App.ApplicationRoute = Em.Route.extend({
model: function() {
return [
{id: 1, name: 'Ember.js', url: 'http://emberjs.com'},
{id: 2, name: 'Toronto Ember.js', url: 'http://torontoemberjs.com'},
{id: 3, name: 'JS Fiddle', url: 'http://jsfiddle.com'}];
}
})
Working fiddle: http://jsfiddle.net/schawaska/PFxHx/
In Response to Update 1:
I found myself in a similar scenario, and ended up creating a child view to mimic the {{linkTo}} helper. I don't really know/think it's the best implementation tho.
You can see my previous code here: http://jsfiddle.net/schawaska/SqhJB/
At that time I had created a child view within the ApplicationView:
App.ApplicationView = Em.View.extend({
templateName: 'application',
NavbarView: Em.View.extend({
init: function() {
this._super();
this.set('controller', this.get('parentView.controller').controllerFor('navbar'))
},
selectedRouteName: 'home',
gotoRoute: function(e) {
this.set('selectedRouteName', e.routeName);
this.get('controller.target.router').transitionTo(e.routePath);
},
templateName: 'navbar',
MenuItemView: Em.View.extend({
templateName:'menu-item',
tagName: 'li',
classNameBindings: 'IsActive:active'.w(),
IsActive: function() {
return this.get('item.routeName') === this.get('parentView.selectedRouteName');
}.property('item', 'parentView.selectedRouteName')
})
})
});
and my Handlebars looks like this:
<script type="text/x-handlebars" data-template-name="menu-item">
<a {{action gotoRoute item on="click" target="view.parentView"}}>
{{item.displayText}}
</a>
</script>
<script type="text/x-handlebars" data-template-name="navbar">
<ul class="left">
{{#each item in controller}}
{{view view.MenuItemView itemBinding="item"}}
{{/each}}
</ul>
</script>
I'm sorry I can't give you a better answer. This is what I could come up with at the time and haven't touched it ever since. Like I said, I don't think this is the way to handle it. If you are willing to take a look into the {{linkTo}} helper source code, you'll see a modular and elegant implementation that could be the base of your own implementation. I guess the part you're looking for is the href property which is being defined like so:
var LinkView = Em.View.extend({
...
attributeBindings: ['href', 'title'],
...
href: Ember.computed(function() {
var router = this.get('router');
return router.generate.apply(router, args(this, router));
})
...
});
So I guess, from there you can understand how it works and implement something on your own. Let me know if that helps.
example here
http://jsfiddle.net/tobikko/ysFC6/10/
how do i really find out what to put in for OptionValuePath and OptionLabelPath? as you can see the dropdown is there with a list but the values are not visible.
in this case i know that it should be OptionLabelPath: "content.firstName" but what if i didn't know?
i created the focusOut function to examine the content of the view and it looks like this
in my real app that i'm having problem with, each object in the content doesn't contain the ember1340135889380_meta object or the __proto part. is that the problem? how do i fix it?
<script type="text/x-handlebars">
{{view App.SelectView }}
<p>Selected: {{App.selectedPersonController.person.fullName}}
(ID: {{App.selectedPersonController.person.id}})</p>
</script>
window.App = Ember.Application.create();
App.SelectView = Ember.Select.extend({
contentBinding: "App.peopleController",
selectionBinding: "App.selectedPersonController.person",
optionLabelPath: "firstName",
optionValuePath: "id",
prompt: "select",
focusOut: function () {
var test = this.get('content');
}
})
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: [
{
id: 1,
firstName: 'Yehuda',
lastName: 'Katz'},
{
id: 2,
firstName: 'Tom',
lastName: 'Dale'},
{
id: 3,
firstName: 'Peter',
lastName: 'Wagenet'},
{
id: 4,
firstName: 'Erik',
lastName: 'Bryn'}
]
});
As stated in the documentation, you always have to prepend content. to the path of the property which shall be used as id and label, see http://jsfiddle.net/pangratz666/B2rJN/.
Your mentioned ember1340135889380_meta and similar properties on an object indicate that it's an instance of an Ember.Object. But the entries in the Ember.Select's content do not need to be an instance of an Ember.Object, the path also works for "normal" objects, as you use in your example.
Background:
The Ember.View is basically rendered as follows:
<select>
{{#if view.prompt}}
<option value>{{view.prompt}}</option>
{{/if}}
{{#each view.content}}
{{view Ember.SelectOption contentBinding="this"}}
{{/each}}
</select>
So in the each-iteration the current item is made available to the Ember.SelectOption view via content as well. That's why you have to prepend content. when you specify the path to the id / label property. Makes sense?
I have the following example (see below) to work with Ember.js, and everything works alright as far as I enter something in the textfield and press enter. But how can I have the same result when I press the button? How can I bind the value of the textfield when clicking the button? Do I have work with a view?
Thanks in advance!
<script type="text/x-handlebars">
{{view App.TextField}}
{{#view Ember.Button target="App.peopleController" action="addPerson"}}
Add Person
{{/view}}
<ul id='todo-list'>
{{#each App.peopleController }}
<li>{{name}}</li>
{{/each}}
</ul>
</script>
<script>
App = Em.Application.create();
App.peopleController = Em.ArrayController.create({
content: [{name: "Tom"}, {name: "Mike"}],
addPerson: function(name) {
this.unshiftObject(App.Person.create({name: name}));
}
});
App.Person = Em.Object.extend({
name: null
});
App.TextField = Em.TextField.extend({
insertNewline: function() {
App.peopleController.addPerson(this.get("value"));
this.set("value", "");
}
});
</script>
This is actually a little tricky to accomplish, but I've reworked your example in a jsFiddle: http://jsfiddle.net/ebryn/vdmrA/
I would advise against hardcoding references to controllers in your view subclasses. You can't reuse those view components elsewhere if you do that.
I think in your code for the Ember Button is just going to call the addPerson function without giving it the name parameter that it expects. You might have to write a separate view for that button in order to get the value of the input field to pass to the addPerson function.
App.MyButton = Em.Button.extend({
click: function(){
var value = App.TextField.get('value')
// etc
}
I'm just starting to play around with the ember.js library to see what it's all about. I want to display a table of data, and to the right of each row, have a delete button to delete that item from the table. I have no idea how to do this though.
Note, I also tried to create a child view (ItemView) and use it inline within the {{#each ...}}...{{/each}} section, but ember.js complains about expecting a view class instead of ItemView, even though ItemView is defined using Ember.View.create. I would also like to know why that isn't working. Even the sample code for using a child view in an #each block in the documentation doesn't work.
Even if I could declare a child view called ItemView to render each individual Item, I still wouldn't know how to get that particular view's removeItem action to know which item to remove from the itemsController collection. Is there a property of the View to get back the Item instance that the child view is bound to in a collection?
Here is the part of my view template that has the list:
{{#each App.itemsController}}
<tr>
<td>{{itemName}}</td>
<td><a href="#" {{action "removeItem" on="click"}}>Delete</a></td>
</tr>
{{/each}}
And here is my javascript:
window.App = Ember.Application.create();
window.App.Item = Ember.Object.extend({
itemName: "defaultItemName"
});
window.App.itemsController = Ember.ArrayProxy.create({
content: []
});
window.App.ListView = Ember.View.create({
templateName: 'listView',
removeItem: function (event) {
// ??? How do I figure out what item
// the user wanted to remove?
}
});
Yehuda's post Michael linked to demonstrates the correct approach, using a child ItemView inside the each. Not sure why that didn't work for you, you've removed that bit of code from your question unfortunately.
Some of the syntax in Yehuda's answer is slightly out of date so I've updated it and changed it to be a bit more like your question. You can check it out here: http://jsfiddle.net/wmarbut/67GQb/130/ (updated link to jsfiddle 1/21/12)
The thrust of it is
Handlebars
{{#each App.peopleController}}
{{#view App.PersonView personBinding="this"}}
<td>{{view.person.fullName}}</td>
<td><button {{action removeItem target="view"}}>Delete</button>
{{/view}}
{{/each}}
Javascript
App.PersonView = Ember.View.extend({
tagName: 'tr',
person: null,
removeItem: function() {
var person = this.get('person');
App.peopleController.removeObject(person);
}
});
Thanks to Tom Whatmore fiddle I found answer to the same question.
After reading trek intro, instead of personBinding="this", I'd rather use {{action removeItem person}} to explicitly indicate object on which action should be performed.
<script type="text/x-handlebars">
<table>
{{#each person in App.peopleController}}
{{#view App.PersonView}}
<td>{{person.fullName}}</td>
<td><button {{action removeItem person}}>Delete</button>
{{/view}}
{{/each}}
</table>
</script>
In the view I'd use var person = evt.context; to get person object.
App = Ember.Application.create();
App.Person = Ember.Object.extend({
fullName: function() {
return this.get('firstName') + ' ' + this.get('lastName');
}.property('firstName', 'lastName')
});
App.peopleController = Ember.ArrayProxy.create({
content: [App.Person.create({ firstName: "Yehuda", lastName: "Katz" }),
App.Person.create({ firstName: "Tom", lastName: "Dale" })]
});
App.PersonView = Ember.View.extend({
tagName: 'tr',
removeItem: function(evt) {
var person = evt.context;
App.peopleController.removeObject(person);
}
});
You can play with this fiddle jsfiddle.net/67GQb/127
Not sure if this the best way but I've put the item index or item name as a property of the tag and then use jQuery to fetch it.
// template
{{#each App.itemsController}}
<tr itemName="{{itemName}}">
<td>{{itemName}}</td>
<td><a href="#" {{action "removeItem" on="click"}}>Delete</a></td>
</tr>
{{/each}}
// Javascript
window.App.ListView = Ember.View.create({
templateName: 'listView',
removeItem: function (event) {
var id = this.$().parent().parent().attr('itemName');
...
}
});
Hope this helps.