Display map from promise with emberJS and handlebars - ember.js

I am currently struggling to display maps in emberJS/handlebars (which is new for me).
Server side, I have a command.go file with:
var Actions = map[string]string{
"EAT": "EAT.",
"DRINK": "DRNK",
"SLEEP": "SLP."
}
var Keys = map[string]int{
"KEY_q": 0,
"KEY_w": 1,
"KEY_e": 2,
...
}
Each action and key have a string constant identifier and are associated to a string or int code.
I would like to display a 2 columns table in which:
- column 1 shows actions (like eat, drink, sleep, ...)
- column 2 shows a dropdown list with available keyboard keys (like Q, W, E, ...), their int code being the id of the tag
I have a controller returning these maps as JSON object:
ctx.JSON(http.StatusOK, gin.H{
"actions": models.Actions,
"keys": models.Keys,
})
Then I a an emberJS service, config.js, as follows:
commands: computed(function () {
return this.get('ajax').request('<address>/command').then(result => {
return result;
});
}),
commandActions: computed('commands', function() {
return this.get('commands').then((commands) => {
return commands.actions;
});
}),
commandKeys: computed('commands', function() {
return this.get('commands').then((commands) => {
return commands.keys;
});
}),
The controller commands.js is as follows:
import Ember from 'ember';
const { computed, inject: { service } } = Ember;
export default Ember.Controller.extend({
config: service(),
selectedKey: '',
actions: {
selectKey(value) {
},
}
});
And finally in commands.hbs I have
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Actions</th>
<th>Associated key</th>
</tr>
</thead>
<tbody>
{{#each-in config.commandActions as |key value|}}
<tr>
<td>{{command}}</td>
<td>
{{#power-select
options=config.commandKeys
selected=selectedKey
allowClear=false
searchEnabled=false
onchange=(action "selectKey")
as |key|
}}
{{key}}
{{/power-select}}
</td>
</tr>
{{/each-in}}
</tbody>
</table>
</div>
But nothing is displayed :(.
The service works well but then in the hbs file nothing appears. I have tried different combinations of each or each-in with no success.
Could someone please help?
Do I need to set variables in the controller somehow then use those variables in the hbs?
I'm using ember 2.5.
Thanks in advance
EDIT
The problem may come from the fact that I am trying to display a promise object before it is resolved. Any idea about that?

I think with Ember Concurrency, your service will pause until your promise resolves, then return the results you want, rather than a promise object that #each doesn't know how to iterate over.
Your service code would end up looking like this:
commands: task(function*() {
const allCommands = yield this.get('ajax').request('<address>/command');
return allCommands;
}),
commandActions: computed.alias('commands.actions'),
commandKeys: computed.alias('commands.commandKeys')
and your template would be happy again.

Thank you all for the help you provided.
We finally managed to get what we wanted by doing the following:
In config.js service:
commands: computed(function () {
return this.get('ajax').request('<address>/command').then(result => {
return result;
});
}),
In command.js controller:
import DS from 'ember-data';
actions: computed(function() {
return DS.PromiseArray.create({
promise: this.get('config.commands').then((allCommands) => {
let result = [];
let actions = allCommands['actions'];
for (var name in actions) {
let cmd = Ember.Object.create({
'name': name,
'code': actions[name],
});
result.pushObject(cmd);
}
return result;
})
});
}),
Same code to get keys.
And in commands.hbs:
<tbody>
{{#each actions as |action|}}
<tr>
<td>{{action.name}}</td>
<td>
{{#power-select
options=keys
selected=selectedKey
allowClear=false
onchange=(action 'selectKey' action)
searchEnabled=false
as |key|
}}
{{key.name}}
{{/power-select}}
</td>
</tr>
{{/each}}
</tbody>
Thanks again

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.

filtering results need updating when query param changes

With the code below, I want the status th to toggle showReturned and update the filteredResults on the page.
This doesn't seem to be happening however as a change in the query param doesn't automatically trigger callbacks like refreshing the model or in this case, updating the filteredResults.
How do I get a click on the status th to update the filteredResults on the page?
export default Ember.ArrayController.extend({
showReturned: false,
queryParams: ['showReturned'],
filteredResults: function() {
var articles = this.get('model');
var showReturned = this.get('showReturned');
if (showReturned) {
return articles;
} else {
return articles.filterBy('state', 'borrowed');
}
}.property('model.#each.state'),
actions: {
setShowReturned: function() {
this.toggleProperty('showReturned');
return false;
}
}
});
<thead>
<tr>·
<th>Description</th>·
<th>Notes</th>·
<th>Borrowed since</th>
<th {{action "setShowReturned"}}> Status</th>
<th></th>
</tr>
</thead>
FYI there's a standard property for what you call filtered results--arrangedContent if you want to use that.
You did not include the queryParam as a dependency:
filteredResults: function() {
....
}.property('model.#each.state', 'showReturned'),

How to add record to the Store after it is saved

Using the following syntax to create a record, adds it to the Store immidiatly:
store.createRecord('post', {title: "Rails is omakase"});
What is the way to successfully store the record on the server and only then add it to the Store?
I would expect store.createRecord to return a promise. Very confusing.
One could argue that having the "potential" records in your local store isn't the problem, but having them show up in a list that is intended for showing saved records.
So, I made a component to use when I only want to show saved records:
app/components/each-saved.js
import Ember from 'ember';
const EachSavedComponent = Ember.Component.extend({
tagName: '',
savedRecords: Ember.computed.filterBy('records', 'isNew', false)
});
EachSavedComponent.reopenClass({
positionalParams: ['records']
});
export default EachSavedComponent;
app/templates/components/each-saved.hbs
{{#each savedRecords as |record|}}
{{yield record}}
{{/each}}
Usage
<div class="CARD">
<table class="LIST">
{{#each-saved model.users as |user|}}
<tr>
<td class="LIST-cell">{{user.name}}</td>
<td class="LIST-cell--right">Remove</td>
</tr>
{{/each-saved}}
</table>
</div>
I don't need it everywhere and there are a few cases where I do want unsaved records to show up in a list.
For what you mentioned I guess you're calling the server directly (instead of through the REST Adapter). If that is the case, you can:
var store = store;
$.ajax(url, {type: 'POST', success: function (savedRecord) {
var pushedRecord = store.push(App.MyModel, savedRecord);
pushedRecord.save();
});

Nested View Not Updating Ember.js

Some background before I get right to the problem. I've got some data (random numbers in this case), and I will need to be able to visualize this data in multiple ways. I've only implemented a table and line view in the fiddle, in prod I will have more ways to visualize the data (pie, bar, etc...), and there will be multiple sections.
Here is the fiddle.
I can correctly change the type I want to display, but I can't seem to get the view to update whenever I update the nested view. I'm probably missing something really easy, so the title of this question maybe loaded. If that's the case I apologize, but I'd greatly appreciate any help.
Handlebars:
<script type="text/x-handlebars" data-template-name="index">
{{#each App.Controller.content}}
{{#view App.ChartTypeContainer contentBinding="this"}}
{{#each chartTypesWithSelected}}
<a href="#" {{action switchChartType this target="view"}}>
{{#if selected}}
<strong>{{display}}</strong>
{{else}}
{{display}}
{{/if}}
</a>
{{/each}}
{{/view}}
{{#if currentView}}
{{view currentView}}
{{/if}}
{{/each}}
</script>
<script type="text/x-handlebars" data-template-name="table">
<table>
<thead>
<tr>
{{#each view.data.headings}}
<th>{{name}}</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each view.data.data}}
<tr>
{{#each values}}
<td>{{this}}</td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
</script>
<script type="text/x-handlebars" data-template-name="line">
</script>
js:
App = Em.Application.create({
getRandomData: function(){
// Generate between 1-3 random headings
var headings=[],
headingCount = (Math.floor(Math.random() * 5) + 1),
data=[];
for(var i = 0; i < headingCount; i++){
headings.push({name: 'heading ' + i});
}
// Generate between 5 and 10 rows of random data
for(var i = 0; i< (Math.floor(Math.random() * 10) + 5);i++){
var values = [];
for(var j=0; j< headingCount;j++){
values.push((Math.floor(Math.random() * 100) + 1));
}
data.push({values: values});
}
return {headings: headings, data: data};
},
ready: function(){
Em.View.create({templateName:'index'}).appendTo('body');
}
});
App.chartFactory = Em.Object.create({
create: function(key, data){
switch(key){
case 'table':
return App.TableView.create({data: data || App.getRandomData()});
case 'line':
return App.LineView.create();
default:
return;
}
}
});
/* MODELS */
App.ChartType = Em.Object.extend({
key: '',
display: ''
});
App.Section = Em.Object.extend({
title: '',
chartTypes: [],
chartTypesWithSelected: function(){
var currentSelected = this.get('selectedChartType');
var types = this.get('chartTypes');
var thing = types.map(function(item){
item.set('selected', item.key === currentSelected);
return item;
});
return thing;
}.property('chartTypes.#each', 'selectedChartType'),
data: {},
selectedChartType: '',
selectedChartTypeObserver: function() {
var selectedChartType = this.get('selectedChartType');
alert('changin chart type to: ' + selectedChartType);
App.chartFactory.create(selectedChartType);
}.observes('selectedChartType'),
currentView: null
});
/* VIEWS */
App.ChartTypeContainer = Em.View.extend({
switchChartType: function(chartType) {
this.get('content').set('selectedChartType', chartType.key);
}
})
App.TableView = Em.View.extend({
templateName: 'table',
data: {}
});
App.LineView = Em.View.extend({
templateName:'line',
data: {},
didInsertElement: function(){
var data = App.getRandomData();
var headings = data.headings.map(function(item){
return item.name;
});
var series = data.data.map(function(item){
return {data: item.values};
});
this.$().highcharts({
title: null,
series: series,
xAxis: {categories: headings},
yAxis: {min: 0, max: 100, title: {text: 'Random Numbers'}}
});
}
})
/* CONTROLLER */
App.Controller = Em.Object.create({
content: [
App.Section.create({
title: 'First Section',
chartTypes: [
App.ChartType.create({key: 'table', display: 'Table Display'}),
App.ChartType.create({key: 'line', display: 'Line Display'})
],
selectedChartType: 'table', // CHANGE HERE TO SEE THE OTHER VIEW, use 'line' or 'table'
currentView: App.chartFactory.create('table') // CHANGE HERE TO SEE THE OTHER VIEW, use 'line' or 'table'
})
]
});
UPDATE:
Setting the newly created view on the next run cycle using Ember.run.next seems to produce the required behavior correctly. Updated Fiddle
I suggest taking a look at the official ember guides. They've gotten really good lately and can shed some light on best practices and ways to not "fight" the framework.
I forked your fiddle and provided something that is closer to "the true ember way" when it comes to showing multiple views for the same underlying data as you are trying to do. In my opinion, manually creating and/or attaching views to the DOM is an anti-pattern in ember.
The basic approach is having a resource represent your data, and then routes under that resource that map to the templates that you want to render. What I've done is provide a template for the resource that merely has links to the two routes beneath it and an {{outlet}} into which those templates will get rendered. I removed your manual creation and swapping of views. By default I have it transition to the chart view so that you don't just see a blank page with links.
As with any framework, if you find yourself writing a ton of boilerplate code and things aren't working properly, that's probably a good sign that you're fighting the framework. Or the framework is bad. :P