Ember display data in table format - ember.js

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}}

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

How would you bind a dynamic value to a dynamic component in Handlebars/EmberJS

I'm creating a dynamic table component (one row per model), that will include components dynamically (one column for each object in config, each object relates to a key in a model).
I'm trying to bind the model key to the dynamic model.
Any ideas on how to do that given the following?
Config object:
deployment.js (controller)
EDConfig: {
controller: this,
modelType: 'EscalationDetailModelGroup',
table: {
cols: [{
header: 'Escalation Time',
cname: 'form-input-text',
content: {
value: model.escalationTime //obviously this wont work
}
},{
header: 'Most Complex Alarm Level',
field: 'mostComplexAlarmLevelDispatched',
cname: 'form-input-text',
content: {
value: model.escalationTime //obviously this wont work
}
}]
}
};
Router Model:
deployment.js (router)
modelRange: [{
id: 1,
escalationTime: '3 hours',
mostComplexAlarmLevelDispatched: 'N/A'
}, {
id: 2,
escalationTime: '45 minutes',
mostComplexAlarmLevelDispatched: 'Level 3'
}]
Templates:
deployment.hbs
<h2>Deployments</h2>
{{table-list
config=EDConfig
data=model.escalationDetailModelGroups
}}
table-list.hbs
<table>
<thead>
<tr>
{{#each col in config.table.cols}}
<th>{{col.header}}</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each record in modelRange}}
<tr>
{{#each col in config.table.cols}}
<td>
{{component col.cname content=col.content}}
</td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
I'm still not sure how are you trying to merge/link the data, but I doesn't seem to be really important.
I don't think its necessary to pass two data sources to your table-list, the relationships between config and model are not something that you should be doing in the templates. Its more of a data-decoration process and that type of thing should be done at the controller level.
How about something like:
// controller
tableRows: function() {
var config = this.get('config');
var model = this.get('model');
config.forEach(function(col) {
// give each col a model reference
});
return config;
}.property('config', 'model')
// template
{{table-list data=tableRows}}
I just typed that off the top of my head, tweaks would be needed most likely, but the idea should be clear.

Drop-down menu which changes the content on an other drop-down menu

I'd like to create a drop-down menu from a hasMany relation which filters the values of an other drop-down menu. I have ships which belongsTo companies and which haveMany cruises. The user of the webpage should be able to select nothing (the table displays all ships) or a company (the table displays just the ships of that company) or a specific ship (the table just displays one ship). If the user selected a specific company only ships of that company should be displayed in the ship drop-down menu. The table of ships should be updated too.
How can I create a set of two drop-down menus (above or below the table) which behave like this example and filter the content of the table?
I know this question is a tall order. I tried to break it down as much as possible. Thank you for your time and effort.
The code of my example application
ember new travel_agency
cd travel_agency
ember install:addon ember-cli-scaffold
ember g scaffold ship name:string
ember g scaffold company name:string
ember g scaffold cruise starts_at:date
ember generate adapter application
ember g http-mock ships
ember g http-mock companies
ember g http-mock cruises
ember install:addon ember-moment
app/adapters/application.js
import DS from 'ember-data';
export default DS.RESTAdapter.extend({
'namespace': 'api'
});
app/models/company.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
ships: DS.hasMany('ship', { async: true })
});
app/models/ship.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
company: DS.belongsTo('company', { async: true }),
cruises: DS.hasMany('cruise', { async: true })
});
app/models/cruise.js
import DS from 'ember-data';
export default DS.Model.extend({
startsAt: DS.attr('date'),
ship: DS.belongsTo('ship', { async: true })
});
server/mocks/ships.js
[...]
var shipList = [
{"id":1,"name":"Carnival Fantasy","company":1,"cruises":[1,2]},
{"id":2,"name":"Carnival Triumph","company":1,"cruises":[3,4]},
{"id":3,"name":"Queen Mary 2","company":2,"cruises":[5]},
{"id":4,"name":"Queen Elizabeth","company":2,"cruises":[6]},
{"id":5,"name":"Norwegian Jewel","company":3,"cruises":[7,8]}
]
[...]
server/mocks/companies.js
[...]
var companyList = [
{"id":1,"name":"Carnival"},
{"id":2,"name":"Cunard"},
{"id":3,"name":"Norwegian Cruise Line"}
]
[...]
server/mocks/cruises.js
[...]
var cruiseList = [
{"id":1,"startsAt":"2014-10-01","ship":1},
{"id":2,"startsAt":"2014-10-15","ship":1},
{"id":3,"startsAt":"2014-10-30","ship":2},
{"id":4,"startsAt":"2014-11-10","ship":2},
{"id":5,"startsAt":"2014-11-20","ship":3},
{"id":6,"startsAt":"2014-11-20","ship":4},
{"id":7,"startsAt":"2014-10-20","ship":5},
{"id":8,"startsAt":"2014-11-20","ship":5}
]
[...]
app/templates/ships/index.hbs
[...]
<tbody>
{{#each ship in model}}
<tr>
<td>
{{ship.name}}
</td>
<td>
{{ship.company.name}}
</td>
<td>
{{#each cruise in ship.cruises}}
{{moment date "L" cruise.startsAt}},
{{/each}}
</td>
<td>
{{link-to "Edit" "ships.edit" ship}}
</td>
<td>
{{link-to "Show" "ships.show" ship}}
</td>
<td>
<a href="#" {{action "remove" ship}}>Remove</a>
</td>
</tr>
{{/each}}
</tbody>
[...]
I'm not going to dive into your whole application. Because it doesn't have much to do with the functionality you want. If I understand correctly you want to filter the second option-list based on the value of the first.
You'd need a template containing two option-lists
{{view "select" content=model.colors value=selectedItem}}
color: {{selectedItem}}
{{view "select" content=filteredBoats optionLabelPath="content.boat"}}
You'd need a route providing a model:
App.IndexRoute = Ember.Route.extend({
model: function() {
return {
colors: [],
boats: []
};
}
});
And then you need a controller wiring things up:
App.IndexController = Ember.Controller.extend({
filteredBoats: function(){
var selI = this.get('selectedItem');
return this.get('model.boats').filter(
function(boat) {
return boat.color == selI;
});
}.property('selectedItem')
});
Lets recap and see what happens.
In the view we bind the result of the first option-list to selectedItem in the controller
In the controller we create a computed property and filter model.boats based on it's value
In the view we bind the second option-list to the computed property.
See this jsbin.

Ember ArrayController this.get doesn't work

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'

Ember Ember.Select binding

I am trying to create a simple page where I loop through a list of phone numbers associated with a Contact. Each phone number has a "number" and a "phone_type".
I've created a View that extends Ember.Select that populates itself with a the list of phone_types. Other than that, is's just a plain Ember.Select:
export default Ember.Select.extend({
thestore: '',
optionLabelPath: 'content.code',
optionValuePath : 'content.code',
didInsertElement: function() {
var vtype = this.get("valuetype");
var vls = this.get("thestore").filter('valuelist', { type: 'phone_type' }, function(vv) {
return vv.get("type") == vtype;
});
this.set("content",vls);
}
});
Here is my code in the template using the "valuelist" view defined above.
{{#each phonenumber in model}}
<tr>
<td> {{phonenumber.number}}</td>
<td>{{phonenumber.phone_type}}</td>
<td>{{view 'valuelist' thestore=store valuetype='phone_type'
selection="{{phonenumber.phone_type}}"
value="phonenumber.phone_type" }}</td>
</tr>
{{/each}}
What I cannot figure out is how to bind the value in the dropdown to the field in each model record I am iterating through in the template. You can see I've tried various things in the code above without any luck.
The property you need is value. However, in your attempts above, you were filling it with literal strings. This happens when you provide a value wrapped in quotes ('...' or "..."). What you need is to give it an identifier, which is value without quotes. So, try this:
{{#each phonenumber in model}}
<tr>
<td>{{phonenumber.number}}</td>
<td>{{phonenumber.phone_type}}</td>
<td>{{view 'valuelist' thestore=store valuetype='phone_type'
value=phonenumber.phone_type }}</td>
</tr>
{{/each}}
As an aside, this is a very unortodox way of doing things. A view shouldn't be tied to a store. Also, I think this will cause your select to be unusable while the values load asynchronously (and potentially crash your app if there is an error).
A conventional way to do this would be to load the list of all phone_types in your setupController hook and then provide it as an argument to Select view.
Controller:
App.MyRoute = Ember.Route.extend({
//...
setupController: function (c, m) {
c.set("model", m);
c.set("phoneTypes", [
"home", "office"
// or whatever, load it from store in model hook and setup here
]);
}
});
Template:
{{#each phonenumber in model}}
<tr>
<td>{{phonenumber.number}}</td>
<td>{{phonenumber.phone_type}}</td>
<td>{{view Ember.Select
content=phoneTypes
value=phonenumber.phone_type }}</td>
</tr>
{{/each}}