Got stuck with Ember.js handing over computed property to template - javascript

I´m pretty new to ember development and need help in handling this kind of task:
Currently I am working with Fixtures in an ember-cli app.
The two models concerned are:
var Recipe = DS.Model.extend({
title: DS.attr('string'),
body: DS.attr('string'),
ingredients: DS.hasMany('ingredients',{async: true}),
recipeCategory: DS.belongsTo('recipeCategory', {async: true})
});
var Ingredient = DS.Model.extend({
title: DS.attr('string'),
portion: DS.attr('string'),
groupTag: DS.attr('string'),
recipe: DS.belongsTo('recipe')
});
While there are no problems in listing all ingredients - also sorted - for a specific recipe called via nested routes,
this.resource('recipes',function(){
this.resource('recipe', {path: '/:recipe_id'});
});
I am encountering big problems while grouping ingredients by groupTag. The logic for grouping is not the problem, but I either run into race conditions accessing the models in controller for computed properties or getting framework errors when trying to handle promises in templates.
Here are the concerned templates:
//recipe.hbs
<strong>{{recipeCategory.title}}</strong>
<h3>{{title}}</h3>
{{render 'ingredients' ingredients}}
//ingredients.hbs
<strong>Zutaten</strong>
<ul>
{{#each groupedIngredients}}
<li>{{group}}
<ul>
{{#each items}}
<li>{{portion}} {{title}}</li>
{{/each}}
</ul>
</li>
{{/each}}
</ul>
My Ingredients-Controller looks like this:
var IngredientsController = Ember.ArrayController.extend({
sortProperties: ['title'],
sortedIngredients: Ember.computed.sort('model', 'sortProperties'),
groupedIngredients: function(){
return this.get('model').then(function(ingredients){
var groupTags = ingredients.mapBy('groupTag').uniq();
var groupedIngredients = groupTags.map(function(gtag){
return {
group: gtag,
items: ingredients.map(function(item){
if ( item.get('groupTag') == gtag){
return item;
}
}).compact()
};
});
console.log(groupedIngredients);
return groupedIngredients;
});
}.property('model')
});
The console log inside the promise is fine, but I can not return the promise for evaluation to the template:
Uncaught Error: Assertion Failed: The value that #each loops over must be an Array. You passed {_id: 158, _label: undefined, _state: undefined, _result: undefined, _subscribers: }
When I remove the promise and just work on this.get('model'), the computed array is full of undefined values, cause the model seems not to be fully loaded.
How can I fix this issue to work on async model data in this way?
Thanks!

You don't need to do your computation in a then hanging off of get('model'). By the time you have reached this point in your code, the model is already resolved and ready to go. The router has already ensured that the model promise is resolved before proceeding.
Therefore:
groupedIngredients: function(){
var ingredients = this.get('model');
var groupTags = ingredients.mapBy('groupTag').uniq();
var groupedIngredients = groupTags.map(function(gtag){
return {
group: gtag,
items: ingredients.map(function(item){
if ( item.get('groupTag') == gtag){
return item;
}
}).compact()
};
});
console.log(groupedIngredients);
return groupedIngredients;
}.property('#each.groupTag')
To avoid having to do compact, just switch to using filter:
items: ingredients.filter(function(item){
return item.get('groupTag') === gtag;
}
which is the same as
items: ingredients.filterBy('groupTag', gtag)
Here's an implementation of groupBy as a computed property, which you might be able to adapt, and if it works would let you simply do
groupedIngredients: Ember.computed.groupBy('groupTag')

I had similar issues with my code and it usually dealt with setting the wrong dependency for the computed property.
Based on your code I would say your groupedIngredients: property should probably be along the lines of:
.property('#each.groupTag')
Once set correctly, you should be able to remove the promises from your controller, since it should automatically update once the promise is fulfilled.

Related

Ember.js: Where does this method go?

Coming to Ember from Rails, one of the places I'm struggling is trying to figure out Ember's definitions of models, views, and controllers.
I'm just testing out some sample Ember code. I'm getting my user events via the GitHub API, and I want to change the type name into something readable.
I have a jsbin here, but here's the gist:
App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
model: function(){
return Ember.$.getJSON('https://api.github.com/users/thenickcox/events').then(function(data){
return data.splice(0,7);
});
}
});
I have a method that types a type and returns a string:
interpretType: function(type){
if (type === 'PushEvent') {
return 'Pushed';
}
return name;
}
In Rails, this would go on the model. But the only model here is the one that Ember created in memory by default (right?). So then I thought, it's something that each member of the array needs, because here's the view:
<h3> Some events</h3>
<ul>
{{#each}}
<li>I {{interpretType(type)}} to {{repo.name}}</li>
{{/each}}
</ul>
So is that something that goes on Ember.ArrayController? I tried that, like this:
App.IndexController = Ember.ArrayController.extend({
interpretType: function(type){
if (type === 'PushEvent') {
return 'Pushed';
}
return name;
}.property()
});
That just gave me an error. Where do I put this?
PS. So you don't have to look at the GitHub API, here's an example JSON object:
{
id: "1890853674",
type: "CreateEvent",
actor: {
id: 702327,
login: "thenickcox",
gravatar_id: "63f35d9e50dfd73281126b051a51668a",
url: "https://api.github.com/users/thenickcox",
avatar_url: "https://2.gravatar.com/avatar/63f35d9e50dfd73281126b051a51668a?d=https%3A%2F%2Fa248.e.akamai.net%2Fassets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png&r=x"
},
repo: {
id: 14463966,
name: "thenickcox/whiskey_taster",
url: "https://api.github.com/repos/thenickcox/whiskey_taster"
},
payload: {
ref: "master",
ref_type: "branch",
master_branch: "master",
description: "My first ember/rails app"
},
public: true,
created_at: "2013-11-17T09:00:17Z"
},
Here is an updated JSBin
Basically, the each can specify an itemController to decorate the model.
App.EventController = Ember.ObjectController.extend({
interpretType: function(){
var type = this.get('model.type');
if (type === 'PushEvent') {
type = 'Pushed';
}
return type;
}.property('model.type')
});
Handlebars doesn't have functions as you've written it, but since we are now using the event controller which wraps the single model, we just refer to interpretType to do the translation:
{{#each itemController='event'}}
<li>{{interpretType}} to {{repo.name}}</li>
{{/each}}
Put it inside an Ember.ObjectController
ArrayController's deal with methods related to the collection of data from the model, whereas ObjectController deals with methods related to the specific object.
I'm also learning Ember from a Rails background.
If you haven't already come across this, you will definetely want to check out ember-tools, it's a command line generator very similar to what we've got in rails. I cant imagine building an Ember app without something like it..

Ember 1.0, Ember Data beta 3: getting related model data with DS.FixturesAdapter

JSBin here: http://jsbin.com/IYiqifO/16/edit?html,js,output
I have three related models set up with fixtures from DS.FixturesAdapter and I'm trying to figure out how to access a related model's attributes from the parent model in a component. My template looks like this:
{{#each data}}
<li>
<h3>{{name}}</h3>
{{#each responses}}
{{text}}<br />
{{/each}}
</li>
{{/each}}
And I also have some code in the component like this:
data = #get('data').map (respondent) ->
{
name: respondent.get('name')
responses: respondent.get('responses').map (r) -> r.get('text')
}
but the data[n].responses is always an empty array. What do I need to do to a) get my handlebars template to populate with the response data; and b) get my data object to have correctly-filled responses arrays?
You need to pass the responses ids in the Respondent fixture:
App.Respondent.FIXTURES = [
id: 1
name: 'Ada Lovelace'
responses: [1,2]
,
id: 2
name: 'Grace Hopper'
responses: [3,4]
]
After this you will receive a error:
Assertion failed: You looked up the 'responses' relationship on
'' but some of the associated records were
not loaded. Either make sure they are all loaded together with the
parent record, or specify that the relationship is async
(DS.hasMany({ async: true }))
Like the message describe you need to use the async: true in your responses property:
App.Respondent = DS.Model.extend
name: DS.attr('string')
responses: DS.hasMany('response', async: true )
In your template you will see the data, but in the didInsertElement when it's called, the responses association isn't loaded. This happen because the template is binding aware but didInsertElement isn't. So when the responses is loaded the template update but the didInsertElement isn't called. Because the responses association return a promise, you can get all the responses in a array, and use Ember.RSVP.all to know when all the responses is loaded:
App.ShowRespondentsComponent = Ember.Component.extend
didInsertElement: ->
allResponses = []
#get('data').forEach (respondent) =>
allResponses.push respondent.get('responses')
Ember.RSVP.all(allResponses).then =>
#respondentLoaded()
respondentLoaded: ->
thisIsWhatIWant = #get('data').map (respondent) ->
{
name: respondent.get('name')
responses: respondent.get('responses').map (r) -> r.get('text')
}
console.log thisIsWhatIWant
This is a jsbin with this working http://jsbin.com/IYiqifO/19/edit

Ember.js: Proper way to iterate through object in model?

I'm developing my first EmberJS app after following some tutorials as practice. It simply contains a list of 'tables', 'columns', and 'rows' similar to a database.
Link to the problematic page: http://www.kangarooelectronics.com/fakeDB/#/tables/edit/2
My issue is that when I go to remove a column I get:
Object # has no method 'deleteRecord'
As I understand this is due to the object I'm iterating through having no references to the controller because of the way I am constructing the array that I use to create my list.
Removing tables works fine, which are listed in the following fashion:
{{#each model itemController='TableList'}}
<a {{action removeTable this}}>Delete</a>
{{/each}}
I'm iterating through the columns via:
{{#each column in currentColumns itemController='TablesEdit'}}
<a {{action removeColumn column}}>Drop</a>
{{/each}}
Snippet from FIXTURES object:
FakeDB.Table.FIXTURES = [
{
id: 1,
name: 'Users',
columns: {
1:{'colId':1, 'name':'name'},
2:{'colId':2, 'name':'favorite color'},
3:{'colId':3, 'name':'phone number'}
},
// ...snip... //
I am getting 'currentColumns' via:
FakeDB.Table = DS.Model.extend({
name: DS.attr('string'),
columns: DS.attr('object'),
rows: DS.attr('object'),
currentColumns: function() {
var newColumns = $.map(this.get('columns'), function(k, v) {
return [k];
});
return newColumns;
}.property('columns'),
// ..snip.. //
Here you can see my problem... it's obvious that my 'column' isn't going to have any methods from my controller. I tried something like this:
FakeDB.Adapter = DS.FixtureAdapter.extend();
FakeDB.Adapter.map('FakeDB.Table', {
columns: {embedded: 'load'},
rows: {embedded: 'load'}
});
FakeDB.Columns = DS.Model.extend({
colId: DS.attr('integer'),
name: DS.attr('string')
});
FakeDB.Rows = DS.Model.extend({
colId: DS.attr('integer'),
name: DS.attr('string')
});
But I couldn't get {{#each column in columns}} to work with that.
Any suggestions? I'm going to read the docs again and will post back if I find a solution.
Thanks!
EDIT:
So I think I found another solution, but I'm still running into a little issue.
FakeDB.Table = DS.Model.extend({
name: DS.attr('string'),
columns: FakeDB.Columns.find().filter(function(item, index, self) {
if(item.tableID == 1) { return true; }
})
});
Still not sure what to replace 'item.tableID == 1' with so that I get items with the tableID referencing to the current page...
Columns are structured as...
FakeDB.Columns.FIXTURES = [
{
id: 1,
tableID: 1,
name: 'name'
},
// ...snip... //
But now I get:
assertion failed: Your application does not have a 'Store' property defined. Attempts to call 'find' on model classes will fail. Please provide one as with 'YourAppName.Store = DS.Store.extend()'
I am in fact defining a 'Store' property...
I'm developing my first EmberJS app after following some tutorials as practice. It simply contains a list of 'tables', 'columns', and 'rows' similar to a database.
Most databases do contain a list of tables, rows and columns. Most web applications contain a fixed set of tables with pre-defined columns and a dynamic list of rows. If this is your first ember app i would recommend starting with something that keeps you on the happy path.
I am in fact defining a 'Store' property...
True but ember is complaining because store is not available before ember app is initialized. Anything that accesses the store should be in a framework hook of some kind. It can't be used when defining your objects, which wouldn't make a lot of sense anyway.
Probably what you meant to do was make a computed property called columns like this:
FakeDB.Table = DS.Model.extend({
name: DS.attr('string'),
columns: function() {
FakeDB.Columns.find().filter(function(item, index, self) {
if(item.tableID == 1) { return true; }
})
}.property('')
});

Ember Data not mapping relations with Padrino API

This is driving me nuts. I have a simple data model set up (using Padrino); I'm long past the stage of actually getting any error messages but adding 'App.Repo' models to an 'App.Stack' model just…doesn't work.
App.Store = DS.Store.extend({
revision: 10
adapter: DS.RESTAdapter.create({
bulkCommits: false,
mappings: {
stars: App.Stars,
stacks: App.Stacks
}
})
});
App.Stack = DS.Model.extend({
url: DS.attr('string'),
repos: DS.hasMany('App.Repo')
});
App.Repo = DS.Model.extend({
name: DS.attr('string'),
url: DS.attr('string'),
description: DS.attr('string'),
language: DS.attr('string'),
watchers: DS.attr('number'),
stack: DS.belongsTo('App.Stack'),
stackId: DS.attr('number')
});
var store = App.get('router.store');
newStack = store.createRecord(App.Stack);
console.log(newStack.serialize())
-> Object {url: null} // no mention of a repos array like I was expecting?
newStack.set('url', 'http://google.com');
console.log(newStack.serialize());
-> Object {url: "http://google.com"} // this works though
var repo = App.Repo.find().objectAt(0);
console.log(repo.serialize());
-> Object {name: "floere/james", url: "https://github.com/floere/james", description: "Voice commanded servant for OSX", language: "Ruby", watchers: 97…}
// so this exists too…
repos = newStack.get('repos');
repos.pushObject(repo);
newStack.get('repos.length'); // 1 (repos.toArray() etc etc all work too)
// but then…
console.log(newStack.serialize())
-> Object {url: null}
// and so then I try to save the relationship on the server anyway…
store.commit()
=> {"stack"=>{"url"=>nil}} // in my Ruby server logos
The store is all set up fine talking to my back end (for example submitting a POST to /repo.json sends the correct request); it just doesn't recognise that App.Stack has any relation.
No idea what's going wrong or what to look at for help :(
Also
I tried making the relations in my Ruby console and then accessing them in a view. This is what happens
// in the router
router.get('applicationController').connectOutlet('body', 'stacks', router.get('store').findAll(App.Stack));
// in the view
<script type="text/x-handlebars" data-template-name="stacks">
{{#each stack in controller }}
{{stack.id}} // this works
{{stack.url}} // this works
{{stack.repos.length}} // this returns the correct count
{{#each repo in stack.repos}}
// this loops the right number of times. so there *is* something there. somehow.
{{repo}} // prints out <App.Repo:ember490>
{{repo.id}} // prints out [object Object]
{{/each}}
{{/each}}
On that last note - maybe a clue in the [object Object]?
I'm so lost :(
More Info:
I'm using Padrino with Mongoid, using RABL to give me JSON. As I said, I can query for & template out my Stack & Repo records. Here's a JSON sample for the /stacks.json endpoint
{
"stacks": [
{
"account_id": null,
"id": "50c127ff6f094144ed000001",
"stars": [
{
"description": "Voice commanded servant for OSX",
"id": "50c128996f0941cfe8000001",
"name": "floere/james"
}
]
}
]
}
I think you'll have to add hasMany relationships to your json object manually by looping through the repos array. I'm doing this in my adapter's createRecord method.
createRecord: (store, type, record) ->
data = {}
data[root] = #toData(record, { includeId: true })
repos = []
stack.get("repos").forEach (repo) ->
repos.pushObject repo.serialize()
data[root]["repos"] = repos
...
I've found a way to get embedded related objects in the JSON to load properly. Basically you have to subclass the serializer and then in its initializer you tell it to register a map for the relationship. Here's an example for a model class called Category that has a to-many relationship 'resourceTypes':
App.WOSerializer = DS.Serializer.extend({
init: function(){
this._super();
this.map(App.Category, {
resourceTypes: { embedded: 'load' }
});
}
});
My solution is further explained here.

Binding to a nested model in Ember.js

I have the following model:
App.Checklist = DS.Model.extend({
name: DS.attr('string'),
checkitems: DS.hasMany('App.Checkitem', { embedded: true }),
remainingItemsCount: function() {
var checkitemsToCount = this.get('checkitems');
return checkitemsToCount.filterProperty('isDone', false).get('length');
}.property()
});
I want to display a list of checklists, with a count of the current checkitems remaining open for each list.
If I drop the following into a template, I get the correct output:
{{#each checklists}}
{{this.name}}
{{this.remainingItemsCount}}
{{/each}}
However, if a new checkitem is added to a checklist, the count does not go up.
BUT, if I change the remainingItemsCount computed property in the Checklist model so that it depends on checkitems.#each.done, then the count increments as new checkitems are added.
The problem is that once this dependency is added, the collection of child checkitems is wrong - it keeps repeating the first checkitem for the number of total checkitems (i.e,. if there are five items for which 'isDone' is false, and four for which 'isDone' is true, then the list count will appear as 9, and the first checkitem will be repeated 9 times).
What am I doing wrong?
UPDATE:
It turns out that adding the dependency to the remainingItemsCount property is causing ember-data to make a new call to the server.
Without the dependency, the following XHR requests are made upon page load:
GET http://localhost:3000/checklists
With the dependency, the following XHR requests are made upon page load:
GET http://localhost:3000/checklists
GET http://localhost:3000/checkitems
The last request comes with the following parameters, which seem to be a representation of the first checkitem, wrapped in an "ids" hash:
{"ids"=>
{"0"=>
{"id"=>"182",
"checklist_id"=>"4",
"title"=>
"Make sure list count automatically increments",
"is_done"=>"false"}},
"action"=>"index",
"controller"=>"checkitems"}
I wonder if this is because the checkitem model is defined with a belongsTo attribute?
App.Checkitem = DS.Model.extend({
title: DS.attr('string'),
isDone: DS.attr('boolean'),
checklist: DS.belongsTo('App.Checklist')
});
UPDATE 2
I'm still not certain why, but it's clear that adding the dependency to the property as follows...
remainingItemsCount: function() {
var checkitemsToCount = this.get('checkitems');
return checkitemsToCount.filterProperty('isDone', false).length;
}.property('checkitems.#each.isDone').cacheable()
...causes ember-data's built-in DS.RESTAdapter to call findMany. The findMany request should take an array of ids, but instead an array containing one entire checkitem object nested inside a hash with the key 0 is being passed to it.
SOLUTION
In the end, I traced the problem to the following observer deep inside ember-data:
dataDidChange: Ember.observer(function() {
var associations = get(this.constructor, 'associationsByName'),
data = get(this, 'data'), store = get(this, 'store'),
idToClientId = store.idToClientId,
cachedValue;
associations.forEach(function(name, association) {
if (association.kind === 'hasMany') {
cachedValue = this.cacheFor(name);
if (cachedValue) {
var ids = data.get(name) || [];
var clientIds = Ember.ArrayUtils.map(ids, function(id) {
return store.clientIdForId(association.type, id);
});
set(cachedValue, 'content', Ember.A(clientIds));
cachedValue.fetch();
}
}
}, this);
}, 'data')
By the time that observer got to the line return store.clientIdForId(association.type, id), the array ids was an array of checkitem objects, not an array of id integers. The fix was pretty simple: return store.clientIdForId(association.type, id.id) returns an array of id integers.
I created a JSFiddle from your description and couldn't reproduce your problem. I'm using Ember.js 0.9.6 and the latest build of ember-data, see http://jsfiddle.net/pangratz666/dGjyR/
Handlebars:
<script type="text/x-handlebars" data-template-name="checklist" >
{{#each checklists}}
{{this.name}}
remaining: {{this.remainingItemsCount}}
{{#each checkitems}}
{{view Ember.Checkbox valueBinding="isDone"}}
{{/each}}
<a {{action "addCheckitem"}} class="clickable">add item</a>
<hr/>
{{/each}}
</script>​
JavaScript:
App = Ember.Application.create({});
App.Checkitem = DS.Model.extend({
isDone: DS.attr('boolean')
});
App.Checklist = DS.Model.extend({
name: DS.attr('string'),
checkitems: DS.hasMany('App.Checkitem', {
embedded: true
}),
remainingItemsCount: function() {
var checkitemsToCount = this.get('checkitems');
return checkitemsToCount.filterProperty('isDone', false).get('length');
}.property('checkitems.#each.isDone').cacheable()
});
App.store = DS.Store.create({
revision: 4
});
App.checklistsController = Ember.ArrayProxy.create({
content: App.store.find(App.Checklist)
});
Ember.View.create({
templateName: 'checklist',
checklistsBinding: 'App.checklistsController',
addCheckitem: function(evt) {
var checklist = evt.context;
checklist.get('checkitems').addObject(App.Checkitem.createRecord({
isDone: false
}));
}
}).append();
var checklist = App.Checklist.createRecord({
name: 'firstChecklist'
});
App.Checklist.createRecord({
name: 'secondChecklist'
});
checklist.get('checkitems').addObject(App.Checkitem.createRecord({
isDone: false
}));
checklist.get('checkitems').addObject(App.Checkitem.createRecord({
isDone: true
}));
checklist.get('checkitems').addObject(App.Checkitem.createRecord({
isDone: true
}));​

Categories