I'm quite new to EmberJS. I am trying to render a simple hasMany modelisation.
My app is supposed to handle multiple tasks for multiple persons.
First, I have models:
App.Task = Em.Object.extend({
name: null
});
App.Person = Em.Object.extend({
firstname: null,
lastname: null,
avatar: null,
tasks:null,
});
A person can have multiple tasks. So my personsController works like this:
App.personsController = Em.ArrayController.create({
content: [],
tasks:[],
addPerson: function(){
var aPerson = App.Person.create({
firstname: this.firstname,
lastname : this.lastname,
avatar : this.avatar,
});
this.pushObject( aPerson );
}
My tasksController, wich will handle tasks jobs:
App.tasksController = Em.ArrayController.create({
content:[],
contentBinding: "App.personsController.tasks",
name:'',
removeTask: function(e){
this.removeObject( e.context );
},
addTask: function(e){
this.pushObject( App.Task.create({"name":this.name}) );
}
});
contentBiding seems to be one of the keys here. I want it to auto-bind to the tasks of the person I'm working on.
On the views side, just working on my Handlebars templates:
<script type="text/x-handlebars">
{{view Em.TextField valueBinding="App.personsController.firstname" placeholder="firstname" }}
{{view Em.TextField valueBinding="App.personsController.lastname" placeholder="lastname" }}
<button {{action "addPerson" target="App.personsController"}} class="btn btn-primary" >Ajouter un developpeur</button>
{{#each App.personsController}}
<div>
<h3>{{firstname}}{{lastname}}</h3>
{{view Em.TextField valueBinding="App.tasksController.name"}}
<button {{action "addTask" target="App.tasksController"}} >Add a task</button>
{{#each App.tasksController}}
{{view Em.TextField valueBinding="name" }}
<button {{action "removeTask" target="App.tasksController"}} >x</button>
{{/each}}
</div>
{{/each}}
</script>
So, when I'm adding a new person, everything works fine. But if I add a task, the task is added on each of the person's tasks in the view.
I'm afraid I'm making a conceptual mistake, and I can't find a good documentation about this kind of visualisation. I've seen examples of hasMany relations with ember.data, but I'd like first to understand properly what is going on here.
Your addTask function adds a new task to the array maintained within App.tasksController, but there is nothing binding these tasks to any instance of App.Person.tasks.
I suggest creating new View class with a binding to your tasksController and to a single App.Person instance. The personBinding of this view would be set inside of your outer {{#each}} loop. Have this view be responsible for rendering App.Person.tasks for a single person.
Change addTask in your controller to accept a reference to an App.Person as an argument, then add the task explicitly to that person's tasks array in the body of the function.
Related
I'm trying to create a manually saved form with a moderate number of fields (let's say 20) in Ember.js (not using live bindings) and so far am confused about the correct way / best practice for doing so. I've found the following methods:
http://www.solitr.com/blog/2012/06/ember-input-field-with-save-button/
How to use one-way binding on emberjs?
https://stackoverflow.com/a/16473186/1248965
All of the above methods seem hacky to a degree; they either extend the text field or use a per-field observer, requiring you to list out each one. Is there some other way? Something like the 'unbound' helper, but allowing the auto-model updating magic / validation (via ember-data) on some action (like an 'unbound-until' or 'conditional-bind' or something)? I've gone through all the docs, SO, the github issues, the Ember forum, and the links above, and still feel like I must have missed something.
Basically, a way to say "do everything you would do with a normally bound form/fields, but only on a certain action, rather than in real time."
What you want is a "Buffered Proxy", where you temporarily store all changes to the model (you can catch those using setUnkownProperty) in a proxy object. Once you are happy with the changes, you'd copy all of the proxy data over into the actual object ("flush the data").
App.Heisenberg = {
firstName: 'Walter',
lastName: 'White',
};
App.IndexRoute = Ember.Route.extend({
model: function() {
return App.Heisenberg;
},
setupController: function(controller, model) {
controller.set('content', model);
}
});
App.IndexController = Ember.ObjectController.extend({
proxy: {},
setUnknownProperty: function(key, value) {
console.log("Set the unknown property: " + key + " to: " + value);
this.proxy[key] = value;
console.log(this.proxy);
},
flush: function() {
for(var key in this.proxy)
this.set('model.'+key, this.proxy[key]);
}
});
Template:
<script type="text/x-handlebars" data-template-name="index">
Saved Name: {{firstName}} {{lastName}}<br />
<br />
<form {{ action "saveEdit" on="submit" }}>
First Name: {{input type="text" valueBinding="firstName"}}<br />
Last Name: {{input type="text" valueBinding="lastName"}}<br />
<br />
<button {{ action "flush" }}>Flush</button>
</form>
</script>
This would make for a nice controller Mixin.
See this jsBin for a live example.
I found a workaround, but I'm not 100% happy with it:
In my "editing" template, I have:
<form {{ action "saveEdit" on="submit" }}>
Title: {{input type="text" value=title}}
<input type="submit" value="Save">
<button {{ action "cancelEdit" }}>Cancel</button>
</form>
Then in my associated controller, I do:
cancelEdit: function() {
var entry = this.get('model');
this.set('isEditing', false);
entry.rollback();
},
saveEdit: function() {
var entry = this.get('model');
this.set('isEditing', false);
entry.save().then(
function() {
console.log('Saved!');
}
I simply hide the fields where the "live updating" would show. I still would like to find a way to temporarily turn off the binding until I trigger my "saveEdit" action, since this still seems inelegant.
I'm new to Ember and am finding some of their concepts a bit opaque. I have a app that manages inventory for a company. There is a screen that lists the entirety of their inventory and allows them to edit each inventory item. The text fields are disabled by default and I want to have an 'edit item' button that will set disabled / true to disabled / false. I have created the following which renders out correctly:
Inv.InventoryitemsRoute = Ember.Route.extend({
model: function(params) {
return Ember.$.getJSON("/arc/v1/api/inventory_items/" + params.location_id);
}
<script type="text/x-handlebars" data-template-name="inventoryitems">
{{#each}}
<div class='row'>
<p>{{input type="text" value=header disabled="true"}}</p>
<p>{{input type="text" value=detail disabled="true"}}</p>
<button {{action "editInventoryItem" data-id=id}}>edit item</button>
<button {{action "saveInventoryItem" data-id=id}}>save item</button>
</div>
{{/each}}
</script>
So this renders in the UI fine but I am not sure how to access the specific model to change the text input from disabled/true to disabled/false. If I were just doing this as normal jQuery, I would add the id value of that specific model and place an id in the text input so that I could set the textfield. Based upon reading through docs, it seems like I would want a controller - would I want an ArrayController for this model instance or could Ember figure that out on its own?
I'm thinking I want to do something like the following but alerting the id give me undefined:
Inv.InventoryitemsController=Ember.ArrayController.extend({
isEditing: false,
actions: {
editInventoryItem: function(){
var model = this.get('model');
/*
^^^^
should this be a reference to that specific instance of a single model or the list of models provided by the InventoryitemsRoute
*/
alert('you want to edit this:' + model.id); // <-undefined
}
}
});
In the Ember docs, they use a playlist example (here: http://emberjs.com/guides/controllers/representing-multiple-models-with-arraycontroller/) like this:
App.SongsRoute = Ember.Route.extend({
setupController: function(controller, playlist) {
controller.set('model', playlist.get('songs'));
}
});
But this example is a bit confusing (for a couple of reasons) but in this particular case - how would I map their concept of playlist to me trying to edit a single inventory item?
<script type="text/x-handlebars" data-template-name="inventoryitems">
{{#each}}
<div class='row'>
<p>{{input type="text" value=header disabled="true"}}</p>
<p>{{input type="text" value=detail disabled="true"}}</p>
<button {{action "editInventoryItem" this}}>edit item</button>
<button {{action "saveInventoryItem" this}}>save item</button>
</div>
{{/each}}
</script>
and
actions: {
editInventoryItem: function(object){
alert('you want to edit this:' + object.id);
}
}
Is what you need. But let me explain in a bit more detail:
First of all, terminology: Your "model" is the entire object tied to your controller. When you call this.get('model') on an action within an array controller, you will receive the entire model, in this case an array of inventory items.
The {{#each}} handlebars tag iterates through a selected array (by default it uses your entire model as the selected array). While within the {{#each}} block helper, you can reference the specific object you are currently on by saying this. You could also name the iteration object instead of relying on a this declaration by typing {{#each thing in model}}, within which each object would be referenced as thing.
Lastly, your actions are capable of taking inputs. You can declare these inputs simply by giving the variable name after the action name. Above, I demonstrated this with {{action "saveInventoryItem" this}} which will pass this to the action saveInventoryItem. You also need to add an input parameter to that action in order for it to be accepted.
Ok, that's because as you said, you're just starting with Ember. I would probably do this:
<script type="text/x-handlebars" data-template-name="inventoryitems">
{{#each}}
<div class='row'>
<p>{{input type="text" value=header disabled=headerEnabled}}</p>
<p>{{input type="text" value=detail disabled=detailEnabled}}</p>
<button {{action "editInventoryItem"}}>edit item</button>
<button {{action "saveInventoryItem"}}>save item</button>
</div>
{{/each}}
</script>
with this, you need to define a headerEnabled property in the InventoryitemController(Note that it is singular, not the one that contains all the items), and the same for detailEnabled, and the actions, you can define them also either in the same controller or in the route:
App.InventoryitemController = Ember.ObjectController.extend({
headerEnabled: false,
detailEnabled: false,
actions: {
editInventoryItem: function() {
this.set('headerEnabled', true);
this.set('detailEnabled', true);
}
}
});
that's just an example how you can access the data, in case the same property will enable both text fields, then you only need one, instead of the two that I put . In case the 'each' loop doesn't pick up the right controller, just specify itemController.
In my application I display a list of accounts like so:
<script type="text/x-handlebars" data-template-name="accounts">
{{#each account in controller}}
{{#linkTo "account" account class="item-account"}}
<div>
<p>{{account.name}}</p>
<p>#{{account.username}}</p>
<i class="settings" {{ action "openPanel" account }}></i>
</div>
{{/linkTo}}
{{/each}}
</script>
Each account has a button which allows users to open a settings panel containing settings just for that account. as you can see in this quick screencast:
http://screencast.com/t/tDlyMud7Yb7e
I'm currently triggering the opening of the panel from within a method located on the AccountsController:
Social.AccountsController = Ember.ArrayController.extend({
openPanel: function(account){
console.log('trigger the panel');
}
});
But I feel that it's more appropriate to open the panel from within a View that I've defined for this purpose. This would give me access to the View so that I can perform manipulations on the DOM contained within it.
Social.MainPanelView = Ember.View.extend({
id: 'panel-account-settings',
classNames: ['panel', 'closed'],
templateName: 'mainPanel',
openPanel: function(){
console.log('opening the panel');
}
});
<script type="text/x-handlebars" data-template-name="mainPanel">
<div id="panel-account-settings" class="panel closed">
<div class="panel-inner">
<i class="icon-cancel"></i>close
<h3>Account Settings</h3>
Disconnect Account
</div>
</div>
</script>
The problem I'm encountering is that I don't see how I can trigger a method on the Social.MainPanelView from the context of the AccountsController. Is there a better solution?
UPDATE 1
I've worked up a Fiddle to illustrate what I'm talking about:
http://jsfiddle.net/UCN6m/
You can see that when you click the button it calls the showPanel method found on App.IndexController. But I want to be able to call the showPanel method found on App.SomeView instead.
Update:
Approach One:
Simplest of all
Social.AccountsController = Ember.ArrayController.extend({
openPanel: function(account){
/* we can get the instance of a view, given it's id using Ember.View.views Hash
once we get the view instance we can call the required method as follows
*/
Ember.View.views['panel-account-settings'].openPanel();
}
});
Fiddle
Approach Two:(Associating a controller, Much Cleaner)
Using the Handlebars render helper: what this helper does is it associates a controller to the view to be displayed, so that we can handle all our logic related to the view in this controller, The difference is
{{partial "myPartial"}}
just renders the view, while
{{render "myPartial"}}
associates App.MyPartialController for the rendered view besides rendering the view, Fiddle
now you can update your code as follows
application.handlebars(The place you want to render the view)
{{render "mainPanel"}}
accounts controller
Social.AccountsController = Ember.ArrayController.extend({
openPanel: function(account){
this.controllerFor("mainPanel").openPanel();
}
});
main panel view
Social.MainPanelView = Ember.View.extend({
id: 'panel-account-settings',
classNames: ['panel', 'closed']
});
main panel controller
Social.MainPanelController = Ember.Controller.extend({
openPanel: function(){
console.log('opening the panel');
}
})
Approach Three:
This one is the manual way of accomplishing Approach Two
Social.MainPanelView = Ember.View.extend({
id: 'panel-account-settings',
controllerBinding: 'Social.MainPanelController',
classNames: ['panel', 'closed'],
templateName: 'mainPanel'
});
Social.MainPanelController = Ember.Controller.extend({
openPanel: function(){
console.log('opening the panel');
}
})
use this.controllerFor("mainPanel").openPanel()
You need to use the action helper rather than directly coding the links. The action helper targets the controller by default, but you can change it to target the view instead:
<a {{action openPanel target="view"}}></a>
Your second link should be a linkTo a route, since you are specifying a link to another resource. The whole snippet, revised:
Social.MainPanelView = Ember.View.extend({
id: 'panel-account-settings',
classNames: ['panel', 'closed'],
templateName: 'mainPanel',
openPanel: function(){
console.log('opening the panel');
}
});
<script type="text/x-handlebars" data-template-name="mainPanel">
<div id="panel-account-settings" class="panel closed">
<div class="panel-inner">
<a {{action openPanel target="view"} class="button button-close"><i class="icon-cancel"></a></i>
<h3>Account Settings</h3>
{{#linkTo "connections"}}Disconnect Account{{/linkTo}}
</div>
</div>
</script>
Is there a way to get the data of a list of checkboxes in Ember as an array of id's?
In my app I want to make a new Site. I made a route to /sites/new to show a form with fields to add a Site.
This is my Model:
App.Site = DS.Model.extend({
name : DS.attr('string'),
languages: DS.hasMany('App.Language')
});
App.Language = DS.Model.extend({
name : DS.attr('string')
});
This is my Controller:
app.SitesNewController = Em.ObjectController.extend({
needs : [ 'languages' ],
name: null,
languages: null,
createSite : function() {
// Get the site name
var name = this.get('name');
var languages = this.get('languages');
console.log(name,description,languages);
// Create the new Site model
app.Site.createRecord({
name : name,
languages : languages
});
// Save the new model
this.get('store').commit();
}
});
This is (part of) my SitesNewView:
{{#each controllers.languages}}
<label class="checkbox">
{{view Ember.Checkbox checkedBinding="languages"}}
{{ name }}
</label>
{{/each}}
<button {{ action "createSite" }}>Save</button>
In my console languages is null. How do I get an array of language-id's out of this.get('languages')?
UPDATE
I mean something like an Ember.Select with attribute multiple=true, like this: {{view Ember.Select selectionBinding="languages" contentBinding="controllers.languages" optionValuePath="content.id" optionLabelPath="content.name" multiple="true"}}
Take a look to the jsfiddle that I quickly created.
This may not be the best solution but at least it should help you.
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.