EmberJS: Accessing child controller functionality in parent - ember.js

This may be more of a structure question but the heading is my current issue.
I have the following basic app:
var App = Ember.Application.create();
App.ApplicationAdapter = DS.FixtureAdapter.extend();
App.Router.map(function () {
this.resource('numbers', {
path: '/'
});
this.resource('users');
});
App.UsersRoute = Ember.Route.extend({
model: function (params) {
return this.store.findAll('user');
}
});
App.NumbersRoute = Ember.Route.extend({
model: function (params) {
return this.store.findAll('number');
}
});
App.User = DS.Model.extend({
name: DS.attr('string'),
numbers: DS.hasMany('number', {
async: true
})
});
App.Number = DS.Model.extend({
value: DS.attr('number'),
user: DS.belongsTo('user')
});
App.NumbersController = Ember.ArrayController.extend({
total: function () {
var total = 0;
this.forEach(function (number) {
total += parseFloat(number.get('value'));
});
return total;
}.property()
});
App.User.FIXTURES = [{
id: 1,
name: 'Bob',
numbers: [100, 101]
}, {
id: 2,
name: 'Fred',
numbers: [102]
}];
App.Number.FIXTURES = [{
id: 100,
value: 25,
user: 1
}, {
id: 101,
value: 15,
user: 1
}, {
id: 102,
value: 60,
user: 2
}];
Working example with templates is here: http://jsfiddle.net/sweetrollAU/9DuR3/
The example shows a relationship between users and numbers. The first page is a list of all numbers in the app, their related user and a total of all numbers. The Users link shows the same content but each user should show its own subtotal for the numbers it has.
My question is basically, how can I access the NumbersController method 'total' in my UsersController? Should I be accessing this method or do I have the structure incorrect?
Thanks

In your case they are similar logic, but they ultimately come from different data sources. You can still create a Mixin that can help you share the code amongst different Ember objects.
App.AddNumberMixin = Em.Mixin.create({
sumNumbers: function(arr){
var total = 0;
arr.forEach(function (number) {
total += parseFloat(number.get('value'));
});
return total;
}
});
App.UserController = Ember.ObjectController.extend(App.AddNumberMixin, {
total: function () {
return this.sumNumbers(this.get('numbers'));
}.property('numbers.#each.value')
});
App.NumbersController = Ember.ArrayController.extend(App.AddNumberMixin, {
total: function () {
return this.sumNumbers(this);
}.property('#each.value')
});
http://jsfiddle.net/9DuR3/2/

#kingpin2k's answer seems sufficient, but here's another way to do it:
http://jsfiddle.net/9DuR3/3/
What happens here is that the numbers are rendered again, for each users, but this time with a different template. Namely the one between the {{render}} tags. The NumbersController is provided with the user.numbers collection, instead of the complete numbers collection.
Also it's important to specify on which properties the total function dependent is (.property('#each.value')).

Related

Ember Data: saving polymorphic relationships

I'm having trouble saving "hasMany" polymorphic records in Ember Data (1.0.0-beta.15). It looks as if Ember Data isn't setting the "type" property of the polymorphic relationship. Relationships in serialized records look like:
"roles": ["1", "2"]
When I expect them to look more like:
"roles":[{
"id": "1",
"type": "professionalRole"
}, {
"id": "2",
"type": "personalRole"
}
];
I see the following error in the console:
TypeError: Cannot read property 'typeKey' of undefined
If the records come back from the server in the expected format, all is well. The error only occurs when Ember Data creates the relationship.
I experience this using the FixtureAdapter, LocalStorageAdapter, and the RESTAdapter. I've read every piece of documentation I can find on the subject, but I cannot see my mistake.
I've created a CodePen to demonstrate the problem, but I'll also paste that code below.
window.App = Ember.Application.create();
App.ApplicationAdapter = DS.FixtureAdapter;
App.Person = DS.Model.extend({
name: DS.attr(),
roles: DS.hasMany('role')
});
App.Role = DS.Model.extend({
title: DS.attr(),
person: DS.belongsTo('person', {
polymorphic: true
})
});
App.ProfessionalRole = App.Role.extend({
rank: DS.attr()
});
App.ApplicationRoute = Ember.Route.extend({
setupController: function(controller) {
var person = this.store.createRecord('person', {
name: 'James'
});
var role = this.store.createRecord('professionalRole', {
title: 'Code Reviewer',
rank: 'Expert'
});
var promises = Ember.RSVP.hash({
person: person.save(),
role: role.save()
});
promises.catch(function() {
controller.set('initialSaveResult', 'Failure');
});
promises.then(function(resolved) {
controller.set('initialSaveResult', 'Success!');
var resolvedPerson = resolved.person;
var resolvedRole = resolved.role;
// Either/both of these break it
//resolvedRole.set('person', resolvedPerson);
resolvedPerson.get('roles').addObject(resolvedRole);
var innerPromises = Ember.RSVP.hash({
person: resolvedPerson.save(),
role: resolvedRole.save()
});
innerPromises.catch(function() {
controller.set('secondSaveResult', 'Failure');
});
innerPromises.then(function() {
controller.set('secondSaveResult', 'Success!');
});
});
}
});
App.ApplicationController = Ember.Controller.extend({
initialSaveResult: "Loading...",
secondSaveResult: "Loading..."
});

Ember.js nested property calculations returning undefined

I'm having an issue with two levels of calculated properties. I'm a bit new to ember so would appreciate some pointers.
The basic problem is that there are two levels of calculated properties - one at the order level and one at the item level. The order level is dependent on the calculation on the item.
After binding to the form - the item level calculation works great and the form is updated as I change the quantity. The order total however does not seem to calculate at all. Am I missing something in the property dependencies?
App.Order = DS.Model.extend({
items: DS.hasMany('item', { async: true } ),
payment_cash: DS.attr('number'),
payment_card: DS.attr('number'),
payment_credit: DS.attr('number'),
balance: DS.attr('number'),
total: function() {
return this.get('items').reduce(function(value,lineItem) {
value += lineItem.get('total');
});
}.property("items.#each.total"),
itemCount: function() {
return this.get('items').reduce(function(value,lineItem) {
value += lineItem.get('quantity');
});
}.property("items.#each.quantity"),
});
App.Item = DS.Model.extend({
order: DS.belongsTo('item'),
product: DS.belongsTo('product'),
quantity: DS.attr('number'),
adjustment: DS.attr('number'),
total: function() {
return this.get('product.price') * this.get('quantity')
}.property('product.price', 'quantity' )
});
App.Product = DS.Model.extend( {
name: DS.attr('string'),
description: DS.attr('string'),
price: DS.attr('number'),
imagePath: DS.attr('string')
});
The problem is that your reduce function is not returning anything. Try this:
total: function() {
return this.get('items').reduce( function(value, lineItem) {
return value += lineItem.get('total');
}, 0 );
}.property("items.#each.total"),
itemCount: function() {
return this.get('items').reduce( function(value, lineItem) {
return value += lineItem.get('quantity');
} , 0);
}.property("items.#each.quantity"),

Filter child-records (hasMany association) with Ember.js

Is there any possibility to filter the hasMany records from a model record? I want to get the active projects, grouped by the customer.
Customer model
Docket.Customer = DS.Model.extend({
name: DS.attr('string'),
initial: DS.attr('string'),
description: DS.attr('string'),
number: DS.attr('string'),
archived: DS.attr('boolean'),
projects: DS.hasMany('project',{ async: true })
});
Project model
Docket.Project = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
number: DS.attr('string'),
archived: DS.attr('boolean'),
customer: DS.belongsTo('customer', { async: true })
});
Project route
Docket.OrganizationProjectsIndexRoute = Docket.AuthenticatedRoute.extend({
setupController: function () {
var customersWithActiveProjects = this.store.filter('customer', function(customer) {
return customer.get('id') && GET_ONLY_ACTIVE_PROJECTS_FROM_CUSTOMER?
});
this.controllerFor('organization.projects').set('filteredProjects', customersWithActiveProjects);
}
});
Update
I tried something like this but It does not work. I think this is a problem caused by asynchronous requests. But does it point in the right direction?
Docket.OrganizationProjectsIndexRoute = Docket.AuthenticatedRoute.extend({
setupController: function () {
// get customers because we group projects by customers
var customers = this.store.filter('customer', function(customer) {
return customer.get('id')
});
var sortedProjects;
// loop through each valid customer and filter the active projects
$.when(
customers.forEach(function(customer){
customer.get('projects').then(function(projects) {
var filteredProjects = projects.filter(function(project){
return !project.get('archived')
});
customer.set('projects',filteredProjects);
});
})
).then(function() {
sortedProjects = Ember.ArrayProxy.createWithMixins(Ember.SortableMixin, {
sortProperties: ["name"],
content: customers
});
});
this.controllerFor('organization.projects').set('filteredProjects', sortedProjects);
}
});
I think the following could work:
controller
Docket.OrganizationProjectsIndexRoute = Docket.AuthenticatedRoute.extend({
setupController: function () {
var projectsController = this.controllerFor('organization.projects');
this.store.find('customer').then(function(customers) {
var promises = customers.map(function(customer) {
return Ember.RSVP.hash({
customer: customer,
projects: customer.get('projects').then(function(projects) {
return projects.filter(function(project) {
return !project.get('archived');
});
});
});
});
Ember.RSVP.all(promises).then(function(filteredProjects) {
projectsController.set('filteredProjects', filteredProjects);
});
});
}
});
template
{{#each filtered in filteredProjects}}
Customer {{filtered.customer}}<br/>
{{#each project in filtered.projects}}
Project {{project.name}}<br/>
{{/each}}
{{/each}}
The trick is use Ember.RSVP.hash to group each customer by active projects.

Retriving models for HasMany relationship

I have two models
Time Entry
TimeTray.TimeEntry = DS.Model.extend({
startTime: DS.attr('date'),
endTime: DS.attr('date'),
status: DS.attr('string'),
offset: DS.attr('number'),
isDeleted: DS.attr('boolean'),
task: DS.belongsTo('task'),
duration: function () {
return TimeTray.timeController.duration(this.get('startTime'), this.get('endTime'));
}.property('startTime', 'endTime'),
title: function () {
if (this.get('task')) {
return this.get('task').get('title');
}
}.property('task')
});
Task
TimeTray.Task = DS.Model.extend({
title: DS.attr('string'),
totalTime: function () {
var timeEntries = this.get('timeEntries')
for (var entry in timeEntries) {
var duration = entry.get('duration')
}
}.property('timeEntries'),
isDeleted: DS.attr('boolean'),
isRecording: DS.attr('boolean', { defaultValue: false }),
timeEntries: DS.hasMany('TimeEntry')
});
how do i get an array of timeentry entities so that i can calculate the total time spent on a task? the above method doesnt work.
the Time Entry title property works.
You have some errors in your code:
1- In that foreach
...
for (var entry in timeEntries) {
var duration = entry.get('duration')
}
...
The for ... in not work like you expected for arrays, you need to use or for(var i; i < array.length; i++) or the array.forEach(func).
2 - In the computed property totalTime you will use the duration property of the TimeEntry, you need to specify that dependency using property('timeEntries.#each.duration').
3 - Probally your timeEntries property will be fetched from the server, so you will need to use the async: true option, in your definition:
timeEntries: DS.hasMany('TimeEntry', { async: true })
4 - If your timeEntries is always empty, even the data being saved in your database. Make sure that your returned json have the timeEntries ids. For example:
{id: 1, title: 'Task 1', timeEntries: [1,2,3] }
The changed code is the following:
TimeTray.Task = DS.Model.extend({
title: DS.attr('string'),
totalTime: function () {
var duration = 0;
this.get('timeEntries').forEach(function(entry) {
duration += entry.get('duration')
});
return duration;
}.property('timeEntries.#each.duration'),
isDeleted: DS.attr('boolean'),
isRecording: DS.attr('boolean', { defaultValue: false }),
timeEntries: DS.hasMany('TimeEntry', { async: true })
});
And this is the fiddle with this sample working http://jsfiddle.net/marciojunior/9DucM/
I hope it helps
Your totalTime method is neither summing timeEntry durations nor returning a value. Also your property is not set up correctly (use #each). The correct way of doing this is:
totalTime: function () {
var timeEntries = this.get('timeEntries')
var duration = 0;
for (var entry in timeEntries) {
duration = duration + entry.get('duration');
}
return duration;
}.property('timeEntries.#each.duration'),
Or, more elegantly using getEach() and reduce():
totalTime: function () {
return this.get('timeEntries').getEach('duration').reduce(function(accum, item) {
return accum + item;
}, 0);
}.property('timeEntries.#each.duration'),

How to correctly access a parent controllers data (model) in nested resources?

I have a little EmberJS app to test things out hot to do nested resources. Sometimes accessing a parent routes/controllers data work, other times not.
Most likely this is due to a oversight on my part with how EmberJS does its magic.
Here is the app:
window.App = Ember.Application.create();
App.Router.map(function() {
this.resource('items', function() {
this.resource('item', {path: ':item_id'}, function() {
this.resource('subitems');
});
});
});
App.ApplicationController = Ember.Controller.extend({
model: {
items: [
{
id: 1,
name: 'One',
subitems: [
{
id: 1,
name: 'One One'
}, {
id: 2,
name: 'One Two'
}
]
}, {
id: 2,
name: 'Two',
subitems: [
{
id: 3,
name: 'Two One'
}, {
id: 4,
name: 'Two Two'
}
]
}
]
}
});
App.ItemsRoute = Ember.Route.extend({
model: function() {
return this.controllerFor('Application').get('model.items')
}
});
App.ItemRoute = Ember.Route.extend({
model: function(params) {
var items = this.controllerFor('Items').get('model')
var item = items.filterBy('id', parseInt(params.item_id))[0]
return item
}
});
App.SubitemsRoute = Ember.Route.extend({
model: function(params) {
var item = this.controllerFor('Item').get('model')
var subitems = item.get('subitems')
return subitems
}
});
http://jsfiddle.net/maxigs/cCawE/
Here are my questions:
Navigating to items/1/subitems throws an error:
Error while loading route: TypeError {} ember.js:382
Uncaught TypeError: Cannot call method 'get' of undefined test:67
Which i don't really get, since apparently the ItemController loads its data correctly (it shows up) and the same construct works for the ItemsRoute as well to get its data.
Since i don't have access to the parents routes params (item_id) i have no other way of re-fetching the data, even though directly accessing the data from ApplicationController works fine.
Why do i have define the root data in a controller not route?
Moving the model definition from ApplicationController to ApplicationRoute, does not work.
Conceptually, as far as i understand it, however this should even be the correct way to do it, since everywhere else i define the mode data for the controller int he route.
Or should the whole thing be better done via the controllers needs-api? As far as i understood the needs are more for only accessing extra data within the controller (or its view) but the routers job is to provide the model.
1. Navigating to items/1/subitems throws an error:
Your model is just a javascript object so there isn't a method get to fetch the data. You can access the subitems by just executing item.subitems.
Also the argument of controllerFor() should be lower case.
For instance:
this.controllerFor('application')
2. Why do i have define the root data in a controller not route?
You can set the model from the route in the setupController method.
App.ApplicationRoute = Ember.Route.extend({
setupController: function(controller) {
controller.set('model', { ... });
}
});
http://jsfiddle.net/Y9kZP/
After some more fiddling around here is a working version of the example in the question:
window.App = Ember.Application.create();
App.Router.map(function() {
this.resource('items', function() {
this.resource('item', {path: ':item_id'}, function() {
this.resource('subitems');
});
});
});
App.ApplicationRoute = Ember.Route.extend({
model: function() {
return Ember.Object.create({
items: [
Ember.Object.create({
id: 1,
name: 'One',
subitems: [
{
id: 1,
name: 'One One'
}, {
id: 2,
name: 'One Two'
}
]
}), Ember.Object.create({
id: 2,
name: 'Two',
subitems: [
{
id: 3,
name: 'Two One'
}, {
id: 4,
name: 'Two Two'
}
]
})
]
})
}
});
App.ItemsRoute = Ember.Route.extend({
model: function() {
return this.modelFor('application').get('items')
}
});
App.ItemRoute = Ember.Route.extend({
model: function(params) {
return this.modelFor('items').findProperty('id', parseInt(params.item_id))
}
});
App.SubitemsRoute = Ember.Route.extend({
model: function(params) {
return this.modelFor('item').get('subitems')
}
});
http://jsfiddle.net/maxigs/cCawE/6/ and deep link into subitems (that did not work previously) http://fiddle.jshell.net/maxigs/cCawE/6/show/#/items/2/subitems
What changed:
root-model data moved into ApplicationRoute
root-model moved into an ember object, and sub-objects are also their own ember objects (so calling get('subitems') and other ember magic works)
changed all the controllerFor('xxx').get('model') into modelFor('xxx') (lower case!), which probably has no effect other than consistency
I'm still not sure if this is now "the ember way" of doing what i have here but its consistent and works completely as wanted.