Ember ArrayController this.get doesn't work - ember.js

I'm having hard time to understand arrayController and ObjectController in Ember (at least I think this is the point.)
I'm working with an ArrayController and I need to get a model and modify it. (take today model and make in order to figured out how many days are in a month) but every time I do:
this.get("today")
nothing happen. Which from the documentation, that is how it should be call.
If I look at other example, most of the people use ObjectController, so i try it with that one too but I got an error complaining the #each loop i'm using need an ArrayController
Here is my code so far:
//Router
WebCalendar.Router.map(function() {
this.resource('index', {path: '/'}, function() {
this.resource("cal", {path: '/'});
this.resource("location", {path: '/location'});
this.resource("organization", {path: '/organization'});
});
});
WebCalendar.CalRoute = Ember.Route.extend({
model: function(){
return this.store.find('dates');
}
});
//Model
WebCalendar.Dates = DS.Model.extend({
today: DS.attr('date'),
month: DS.attr('date'),
year: DS.attr('date'),
daysName: DS.attr('array'),
daysInMonth: DS.attr('array')
});
WebCalendar.Dates.FIXTURES = [
{
id: 1,
today: moment().format('MMMM Do YYYY, h:mm:ss a'),
month: moment().format('MMM'),
year: moment().format('YYYY'),
daysName: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
daysInMonth: [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]
}
];
//CalController
WebCalendar.CalController = Ember.ArrayController.extend({
getMonthDays: function(){
return this.get("today");
}.property('today')
});
//Cal Handlebars
<table>
{{#each date in controller}}
<tbody id="table">
<tr id="year">
<td>{{date.year}}</td>
</tr>
<tr>
<td id="prev-month"> Prev </td>
<td id="month">{{date.month}}</td>
<td id="next-month"> Next </td>
</tr>
<tr id="days-of-week">
{{#each date.daysName}}
<td>{{this}}</td>
{{/each}}
</tr class="days">
<tr>{{getMonthDays}}</tr>
</tbody>
{{/each}}
</table>
My questions are:
Why this.get method doesn't work? Documentation here: http://emberjs.com/api/classes/Ember.ArrayController.html#method_get
Is it correct that i'm using ArrayController in this specific situation?
Why seems i cannot use #each loop with ObjectController?

{{getMonthDays}} is being invoked within the {{each}}, which means it is being called in the context of individual Dates objects, but you are defining it on the ArrayController--where Ember won't even look. You are confused between the ArrayController managing the collection of model instances, and individual model instances (or controllers therefor, which you haven't defined).
You need an itemController. I refer you to the documentation rather than summarizing it here. getMonthDays would be a method on the item controller.
By the way,
getMonthDays: function(){
return this.get("today");
}.property('today')
is often better written as
getMonthDays: Ember.computed.alias('today')
or
getMonthDaysBinding: 'today'

Related

#each not updating computed sum value

In a route with an array model, I need a couple of summary statistics available. These summary statistics need to be updated based on values typed into numeric input fields. I have attempted to implement this by setting these as computed properties using #each in a controller.
The properties (creditTotal and costTotal) compute on load, but fail to update when values are updated through the input fields. Unfortunately, they need to be updating, and I am at a loss how to make this happen.
Admittedly I am not a full time developer, so I am grateful for any assistance and insight you may be able to offer.
0640PST 03Jan2018: I also put this in a GitHub repo (https://github.com/knu2xs/arcgis-credit-calculator) to hopefully make it a little easier for anybody generous enough with their time to take a closer look at it.
Here are the relevant files, starting with the controller.
// ./app/controllers/index.js
import Controller from '#ember/controller';
import { computed } from '#ember/object';
export default Controller.extend({
creditTotal: computed.sum('model.#each.creditCost', function(){
return this.get('model').mapBy('creditCost');
}),
costTotal: computed.sum('model.#each.cost', function(){
return this.get('model').mapBy('cost');
})
});
Next, the model being referenced.
// ./app/models/credit-object.js
import DS from 'ember-data';
import { computed } from '#ember/object';
const _creditCost = 0.1;
export default DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
creditRate: DS.attr('number'),
unitRate: DS.attr('number'),
units: DS.attr('number', { defaultValue: 0 }),
rate: computed('creditRate', 'unitRate', function(){
return Number(this.get('creditRate')) / Number(this.get('unitRate'));
}),
creditCost: computed('rate', 'units', function(){
return this.get('rate') * this.get('units');
}),
cost: computed('creditCost', function(){
return this.get('creditCost') * _creditCost;
}),
});
And the route.
// ./app/routes/index.js
import Route from '#ember/routing/route';
export default Route.extend({
model() {
return this.get('store').findAll('credit-object');
}
});
Finally, the template, so it hopefully makes some sense.
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Credit Rate</th>
<th scope="col">Unit Count</th>
<th scope="col">Credit Count</th>
<th scope="col">Cost</th>
</tr>
</thead>
<tbody>
{{#each model as |creditObject|}}
<tr>
<td>{{creditObject.name}}</td>
<td>{{creditObject.rate}}</td>
<td>{{input type='number' value=creditObject.units}}</td>
<td>{{format-floating-point creditObject.creditCost}}</td>
<td>{{format-currency creditObject.cost}}</td>
</tr>
{{/each}}
<tr class="table-primary">
<td>Total</td>
<td></td>
<td></td>
<td>{{format-floating-point creditTotal}}</td>
<td>{{format-currency costTotal}}</td>
</tr>
</tbody>
</table>
I eventually figured out the solution through a lot of trial and error. While not exactly the most elegant, this is what eventually worked with Ember.js version 2.18.
creditArray: computed('model.#each.creditCost', function(){
return this.get('model').mapBy('creditCost');
}),
creditTotal: computed.sum('creditArray')
I did stumble across an enhancement request discussing chaining of these types of functions so it could become something like this.
this.get('model').mapBy('creditCost').sum()
Currently this does not work, but I definitely hope it will in the future!
creditArray: computed('model.#each.creditCost', function(){
return this.get('model').mapBy('creditCost');
}),
creditTotal: computed.sum('creditArray')
I did stumble across an enhancement request discussing chaining of
these types of functions so it could become something like this.
this.get('model').mapBy('creditCost').sum()
Currently this does not work, but I definitely hope it will in the
future!
You have to differentiate between computed property macros (e.g. computed.sum) and native javascript array functions (e.g. mapBy).
The above is not possible because there is no sum function available in javascript, but it can be easily implemented with reduce.
this.get('model').mapBy('creditCost').reduce((res, val) => res + val)
Try this:
// ./app/controllers/index.js
import Controller from '#ember/controller';
import { computed } from '#ember/object';
export default Controller.extend({
creditTotal: computed.sum('model.#each.{units}', function(){
return this.get('model').mapBy('creditCost');
}),
costTotal: computed.sum('model.#each.{units}', function(){
return this.get('model').mapBy('cost');
})
});
With the '{' it should work just fine

Ember display data in table format

I have various coffee drinks from a coffee store. There are various drinks (Mocha, Drip, CustomDrink1, etc) that have different sizes. I need to make a table that displays those drinks with the drink on the y-axis and size as the x-axis.
So for example I have Mocha 12oz, 16oz, 20oz; Drip 12oz, 16oz, 20oz; My Custom Drink 12oz, 16oz, 20oz.
This project is using Ember 1.13
// models/store-drink.js
export default DS.Model.extend({
_store: DS.belongsTo('store', {async: true}),
type: DS.attr('string'),
size: DS.attr('number'),
price: DS.attr('number'),
active: DS.attr('boolean'),
custom: DS.attr('boolean'),
});
My general idea is to get the data in the route and then loop through it somehow in the template. The important attributes for the problem are type and size because I need to dispaly a row with a drink type (Mocha) and then all the sizes (12oz, 16oz, 20oz)
// routes/menu.js
export default Ember.Route.extend({
model: function() {
let myStoreId = this.controllerFor('application').get('myStore.id');
return Ember.RSVP.hash({
myStore: this.store.find('store', myStoreId),
storeMilk: this.store.find('storeMilk', {'store':myStoreId}),
milk: this.store.find('milk', {'store':myStoreId}),
storeDrinks: this.store.find('store-drink', {'store':myStoreId})
});
},
setupController: function(controller, model) {
controller.setProperties({
'storeMilk': model.storeMilk,
'storeDrinks': model.storeDrinks,
'milk': model.milk,
'myStore': model.myStore,
});
}
}
In the template I run into problems because I can't figure out how to split this data by drink type.
<table class="table table-striped">
<thead>
<tr>
<th>Drink</th>
<th>12oz</th>
<th>16oz</th>
<th>20oz</th>
<th>24oz</th>
<th>32oz</th>
</tr>
</thead>
<tbody>
/* all of this is here is wrong. I believe I would need to do 2
loops. the first one would be to create rows that represent drink
types, and then the second loop would loop through the drink type
and display the sizes in the columns.
*/
{{#each storeDrink in storeDrinks}}
<tr>
<td>{{storeDrink.type}}</td>
{{#each drink in storeDrinks}}
<td class="{{unless drink.active 'disabled'}}" {{action 'setDetailDrink' drink}} id="drink-{{drink.id}}">
{{#if drink.active}}
{{decimal drink.price}}
{{else}}
<span class="fa fa-close"></span>
{{/if}}
</td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
I have been stuck on this for months, off and on (since Ember 1.13 was latest). Before I got by by splitting the drinks into different scope variables before it got to the template. It was a hacky workaround, and doesn't work anymore because now users can add custom drinks so I can't hardcode the drink types anymore.
I might be going about this completely wrong, any suggestions welcomed.
I would recommend to have a computed property that calculates the data in a way you can consume it in your template.
types: Ember.computed('drinks', {
get() {
return get(this, 'drinks').reduce((total, curr) => {
if(!total.findBy('type', get(curr, 'type'))) {
total.push(get(curr, 'type'));
}
return total;
}, []).map(type => ({
type,
sizes: get(this, 'drinks').filterBy('type', type)
}));
}
})
Then you can loop through it with
{{#each types as |type|}}
type {{type}} has following sizes:
{{#each type.sizes as |size|}}
{{size.size}}
{{/each}}
{{/each}}

Getting length of hasMany association

I have the following Models defined in my Ember project (project created with ember-cli 0.2.3)
import DS from 'ember-data';
var Game = DS.Model.extend({
title: DS.attr('string'),
description: DS.attr('string'),
geekRating: DS.attr('number')
});
Game.reopenClass({
FIXTURES: [
{ id: "1", title: "Catan", description: "Catan the game", geekRating: "5.65"}
]
});
export default Game;
import DS from 'ember-data';
var OwnedGame = DS.Model.extend({
rating: DS.attr('number'),
plays: DS.hasMany('gamePlay'),
game: DS.belongsTo('game', {async: true}),
playCount: function() {
return this.get('plays.length')
}.property('plays')
});
OwnedGame.reopenClass({
FIXTURES: [
{ id: "1", rating: "8.25", game: "1"}
]
});
export default OwnedGame;
import DS from 'ember-data';
var GamePlay = DS.Model.extend({
date: DS.attr('date'),
ownedGame: DS.belongsTo('ownedGame', {async: true})
});
GamePlay.reopenClass({
FIXTURES: [
{id: "1", date: "2015-01-01", ownedGame: "1"},
{id: "2", date: "2015-02-01", ownedGame: "1"}
]
});
export default GamePlay;
And then in my owned-games/index.hbs
<h1>My Games</h1>
<table class="table table-striped table-condensed table-hover">
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Your Rating</th>
<th>Geek Rating</th>
<th>Plays</th>
</tr>
</thead>
<tbody>
{{#each game in this}}
<tr>
<td>{{game.game.title}}</td>
<td>{{game.game.description}}</td>
<td>{{game.rating}}</td>
<td>{{game.game.geekRating}}</td>
<td>{{game.playCount}}</td>
</tr>
{{/each}}
</tbody>
</table>
I'm trying to display the length of the plays for an OwnedGame with a computed property. Note that as you can see I'm using Fixture Data so not sure if this might be a limitation. I'm not getting any errors, and the list of OwnedGames displays just fine (just 1 of them for now). I'm getting 0 for the size of plays.
I've read other similar threads about this issue, and I believe I have the code correct, but not sure why it isn't working.
UPDATE:
I also tried doing this on each iteration of game:
{{#each play in game.plays}}
{{play.date}}
{{/each}}
And I get no output.
Update #2:
I created all the necessary code to view a list of GamePlay's. The size is 0 until I visit the list of GamePlay's. Then if I go back to my list of OwnedGames, the length of GamePlay's is now 2, which is accurate. So I think I just need to figure out how to get the computed property to query for the data?
With a hasMany relationship, you have two options on how to go about fetching the side loaded data.
First option is to specify what plays records you want by specifying an array with game-play ids on the owned-game fixture. This will make another request for game-play automatically for you and populate your model asynchronously.
App.OwnedGame.reopenClass({
FIXTURES: [
{id: 1, rating: 8.25, game: 1, plays: [1,2]}
]
});
Second option is to manually make a second request to fetch game-play data, which will populate the plays property of owned-game asynchronously.
App.IndexRoute = Ember.Route.extend({
model: function () {
return new Ember.RSVP.hash({
gamePlay: this.store.find('game-play'),
ownedGame: this.store.find('owned-game')
});
},
setupController: function(controller, model) {
controller.set('model', model.ownedGame);
}
});
http://emberjs.jsbin.com/woruqugida/1/edit?html,js,output

Ember binding model to template

I'm having hard time to understand the different ways to bind models to templates in Ember and when use one or the others.
Following different tutorial my code should work but it doesn't.
cal.hbs
<div class="calendar">
{{#each cal}}
{{#if days_label.length}}
{{days_label.[1]}}
{{/if}}
{{test}}
{{/each}}
</div>
cal_model.js
WebCalendar.Cal = DS.Model.extend({
today: DS.attr('date'),
test: DS.attr('number'),
days_label: DS.attr('array'),
months_label: DS.attr('array'),
days_per_month: DS.attr('array')
});
WebCalendar.Cal.FIXTURES = [{
id: 1,
test: 2,
today: new Date(),
days_label: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
months_label: ['January', 'February', 'March', 'April',
'May', 'June', 'July', 'August', 'September',
'October', 'November', 'December'],
days_per_month: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
}];
Router.js
WebCalendar.CalRoute = Ember.Route.extend({
model: function(){
return this.store.find('cal');
}
});
cal_controller.js
WebCalendar.CalController = Ember.ArrayController.extend({
month: function(){
var today = this.get('test');
return today + 1;
}.property('model'),
monthCal: function(year, month){
var today = this.store.find('cal', { today: new Date() });
this.year = today.getFullYear();
this.month = today.getMonth();
},
updateContent: function() { //doesn't work if is called arrangedContent
return this.get('model').content;
}.property('model')
});
For what I understood there are these ways to hook the model:
First
<div class="calendar">
{{#each cal}}
{{#if days_label.length}}
{{days_label.[1]}}
{{/if}}
{{test}}
{{/each}}
</div>
Second
<div class="calendar">
{{#each cal in controller}}
{{#if days_label.length}}
{{days_label.[1]}}
{{/if}}
{{test}}
{{/each}}
</div>
Third
<div class="calendar">
{{#each item in arrangedContent}} //this work if I don't specify arrangedContent in my controller
{{#if item.days_label.length}}//this doesn't work
{{item.days_label.[1]}}//this doesn't work
{{/if}}
{{item.test}}// this works
{{/each}}
</div>
What I want to understand and I can't find good explanation around is: what's the difference between those ways, when use what and what exactly is and does arrangedContent
Thank you very much for any explanation!
ArrangedContent ,if Im not wrong , is an array generated by
EMBER.SORTABLEMIXIN CLASS that is nowday implemented by default to EMBER.ARRAYPROXY CLASS , so your arraycontrollers inherit the arrangedcontent property.
What it really is , an array that is your controller's content , sorted by the properties you'll define in your controller (sortProperties, sortAscending).
So if I have a controller content(that is an array of objects that have the property name) , I can choose to sort them by the name property (sortProperties: ['name'] , thus rendering them in alphabetical order ascending (sortAscending: true).
The difference between your first and second question, is what is passed inside the loop as the current object.
In first you will call {{days_label}} to get the days_label that is equivalent as saying this.days_label.
In your second example though you can call {{days_label}}, but also call cal.days_label that in both cases is the same but it's needed if you were let's say go deeper inside an iteration, as:
{{#each cal in controller}}
{{#if days_label.length}}
{{#each day in days_label}}
{{day.someProperty}}
{{days_label.someOtherProperty}}
{{/each}}
{{/if}}
{{test}}
{{/each}}
In this example I could just do {{#each controller}} ,inside it {{#each days_label}} and then do {{someProperty}} inside it , but then I wouldn't be able to do the same for {{days_label.someOtherProperty}} because this would have changed.
One more thing to consider (very usefull when you are using ember data), is the difference between the model and the content of the controller.
The content is set inside route's setupController (controller, model) but can be overwritten.
UPDATE
Thank you again for your answer :) Right, the code is bit messy now but what i'm try to achieve now is very simple. With the model that I have, I want to display the current month. I try to take 'today' from my model, in my controller, do today.getMonth() and save as property(month), so i should be able to display in the hbs with {{month}}. – Giorgia Sambrotta
What I would do then is move the logic from the controller inside the model.
Some basic things to understand about ember data is that calling this.store.find('resourceName'), makes a request to your api's resoure index route thus returning an array of objects.
This call will be triggered each time you access this route because ember is agnostic of whereas your records have been changed at your server.
So given that you get an array of objects that all have some sort of date in them and you just want to get their month from it , you can define a computed property inside your WebCalendar.Cal model.
Inside WebCalendar.Cal
month: (function() {
var today = this.get('today');
//do something to get the value you want here
}).property('today')
Another thing to consider is that since months_label, days_label and days_per_month are static arrays you really don't need to pass them in each record but instead move them inside your model too, as properties.
So now that we've moved logic to the model you can just call {{month}} inside itteration and this will give you the result of the model's computed property. Also if a cal record happens to change today you will get that change imediately.
The only reason you wouldn't want to do it this way is if you wouldn't want this month property anywhere else except this specific controller.
In this case you would go
Inside WebCalendar.CalController
setMonthToEachCal: function(){
this.get('content').forEach(function(cal) {
today = cal.get('today');
//prepare the month value here and set it in month var
cal.set('month', month);
});
}.observes('content.#each.today'),

Assertion failed: an Ember.CollectionView's content must implement Ember.Array

Very new to ember and trying to mess around with my first test script. I keep getting the error in the title and it occurs when I try to use handlebars to loop through a data fixture. Any help would be appreciated, thank you!
Link to fiddle
Loop statement that is giving error
{{#each player in controller}}
<tr> <!-- foreach statement here order by wins>loss>ties -->
<td>{{ name }}</td>
</tr>
{{/each}}
App.JS
var App = Ember.Application.create({
LOG_TRANSITIONS: true,
});
// Router
App.Router.map(function(){});
App.PlayersRoute = Ember.Route.extend({
model: function(){
return App.Player.find();
}
});
// Controllers
App.PlayersController = Ember.ArrayController.extend();
// View Helpers
// Models
App.Store = DS.Store.extend({
revision: 11,
adapter: 'DS.FixtureAdapter'
});
App.Player = DS.Model.extend({
name: DS.attr('string'),
wins: DS.attr('number'),
losses: DS.attr('number'),
ties: DS.attr('number'),
drop: DS.attr('boolean')
});
App.Player.FIXTURES = [{
id: 1,
name: 'Joe Jame',
wins: 2,
losses: 0,
ties: 0,
drop: 'False'
}, {
id: 2,
name: 'Froggy Bottom',
wins: 2,
losses: 0,
ties: 0,
drop: 'False'
}];
One more answer, first I've ported your example to jsbin IMO it's more reliable than jsfiddle, for the kind of setup you have I've changed the render statement to partial and renamed the playerlist template to _playerlist. additionally I've defined in the IndexRoute a model hook to get your fixture data, and this are the results: http://jsbin.com/iceraz/2/edit. It seams to be working now as expected. Since we are using the partial helper there is no need for an extra Playerlist controller nor route, it will be all handled in the IndexRoute.
Hope it helps.
The render helper creates a new template/view/controller so in your case you will need to pass in the array of Player objects to allow the playerlist template to have the correct context for rendering.
{{ render "playerlist" content }}
You also need to change PlayersRoute to IndexRoute otherwise the model hook won't get called and your data won't be loaded.
App.IndexRoute = Ember.Route.extend({
model: function(){
return App.Player.find();
}
});
JSFiddle example