Rendering a side by side comparison using knockout - javascript

I'm working on a project where I would like to be able to display several items side by side for comparison.
I'd like to have the comparison displayed similarly to the way in which amazon does, with each item being represented by a vertical column with all of the info about the item in that column:
I'm currently using Knockout, and have an Observable array with the items I would like to compare. I would like to use the foreach binding to do the job and my problem stems from the fact that table's have all cells for a specific row contained in the same element which makes it difficult to use:
<tr>
<td>Rating</td>
<td> 4.5 (data from item 1)</td>
<td> 3.5 (data from item 2)</td>
</tr>
<tr>
<td>price</td>
<td>$2.3 (data from item 1) </td>
<td>$3.5 (data from item 2) </td>
</td>
If I wanted each item to be contained in a row this would not be a problem.
Anyone have any recommendations for how to tackle this issue since the data for different items has to exists in different (overlapping) parts of the document.

I have bad news for you. Html tables are strictly ordered in rows, and this kind of layout for your data can be a bit frustrating.
You have two options, basically:
Option A: Transpose your data
You could introduce a specific View Model for a "Feature-Comparison" and transpose your data to that. You can then do a foreach on your tbody iterating through those "featureComparisons". In code, here's what I mean:
var inputData = [
{ name: "First product", rating: 4.5, price: "$3.50", color: "Gray" },
{ name: "Another product", rating: 3.5, price: "$2.95", color: "Yellow" }
];
function FeatComparison(comparisonName, comparedValues) {
this.comparisonName = comparisonName;
this.comparedValues = comparedValues;
}
var vm = { comparisons: ko.observableArray([]) };
// You could also explicitly send a list of comparable properties from back-end.
// For this example we iterate the first product.
for (var prop in inputData[0]) {
if (inputData[0].hasOwnProperty(prop)) {
var vals = inputData.map(function(i) { return i[prop]; });
vm.comparisons.push(new FeatComparison(prop, vals));
}
}
ko.applyBindings(vm);
td { background: #eee; padding: 5px 10px; }
tr:first-child, td:first-child { font-weight: bold; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<table><tbody data-bind="foreach: comparisons">
<tr>
<td data-bind="text: comparisonName"></td>
<!-- ko foreach: comparedValues -->
<td data-bind="text: $data"></td>
<!-- /ko -->
</tr>
</tbody></table>
This option is best suited for situations where you want to make an extended comparison feature with things like comparing multiple products, saving user preferences on what features to include or not, etc.
Option B: Deal with it (for lack of a better title)
Just leave your data as is, and iterate through both objects with foreach at the same time. In code:
var inputData = [
{ name: "First product", rating: 4.5, price: "$3.50", color: "Gray" },
{ name: "Another product", rating: 3.5, price: "$2.95", color: "Yellow" }
];
function RootVm(data) {
var self = this;
this.products = ko.observableArray(data);
this.keys = ko.computed(function() {
if (self.products().length === 0) return [];
return Object.keys(self.products()[0]);
});
}
ko.applyBindings(new RootVm(inputData));
td { background: #eee; padding: 5px 10px; }
tr:first-child, td:first-child { font-weight: bold; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<table><tbody data-bind="foreach: keys">
<tr>
<td data-bind="text: $data"></td>
<!-- ko foreach: $root.products -->
<td data-bind="text: $data[$parent]"></td>
<!-- /ko -->
</tr>
</tbody></table>

Related

Logic or design pattern help required - react

This is for a react project I am working on.
need help with table.
I have a table for employees where each row has a different employee and I input the days for each employee and the other values gets calculated based on the number of days. so, let's say pf, esi, amount payable gets calculated based on that.
Now, there is a way to do those calculations.
for each row we could setup different variables
like for row1, employee1, we could do
days1 and the pf1, esi1, amtPayable1 would get calculated accordingly
and then I could do the same for row2.
so it would be days2 and the pf2, esi2, amtPayable2 would get calculated accordingly.
but I dont think this is the best way to do this.
what if there are a 100 rows? would I create these variables 100 times?? and the code wont be dynamic anymore.
so, what is the best way to do this?
I havent written the full code yet, because I was going to write it like this but saw the problem with this approach and therefore am asking about the correct and better way to do this.
But still, people want to see some relevant code to better understand what I mean, so here's the relevant code
This code is for 3 rows i.e. 3 employees, which means I may have to create days100, pf100, esi100, amtPayable100 if there are a 100 employees.
this made me think that it was not the best way and therfore I thought there's got to be a better way.
anyways, here's the code
let days1 = e.target.value;
let pf1 = days1*(12/100);
let esi1 = pf1*0.25;
let amtPayable1 = pf1 + es1 + 100;
let days2 = e.target.value;
let pf2 = days2*(12/100);
let esi2 = pf2*0.25;
let amtPayable2 = pf2 + es2 + 100;
let days3 = e.target.value;
let pf3 = days3*(12/100);
let esi3 = pf3*0.25;
let amtPayable3 = pf3 + es3 + 100;
after getting the values for all the variables above, I will be using it as data for a different table.
something like this:
const data = [{
key: '1',
name: 'Jerry gold',
days: days1,
amtpayable:amtPayable1,
pf:pf1,
esi:esi1,
}, {
key: '2',
name: 'Arnold Smith',
days: days2,
amtpayable:amtPayable2,
pf:pf2,
esi:esi2,
},
{
key: '3',
name: 'Baker',
days: days3,
amtpayable:amtPayable3,
pf:pf3,
esi:esi3,
}
];
so, you see there is a lot of duplication going on here and I want a way to avoid that.
#Rafael , it's not an ajax call.
everything is getting calculated based on number of days.
so, someone, say the employer, inputs the number of days each employee was present and based on that input the other values are calculated and the data is provided to a different table.
Here's the basics of how I would go about it. Use classes with properties instead of raw data objects. Create properties for the calculated fields of a person. Pass the objects to React components that know how to display the fields of the Person nicely. A Table component can create a Row for each person, for example.
class Person {
constructor(properties) {
Object.assign(this, properties);
}
get pf() {
return this.days * 12 / 100;
}
get esi() {
return this.pf * 0.25;
}
get amtPayable() {
return this.pf + this.esi + 100;
}
}
let MyRow = (props) => (
<tr>
<td>{props.item.name}</td>
<td>{props.item.days}</td>
<td>{props.item.pf}</td>
<td>{props.item.esi}</td>
<td>{props.item.amtPayable}</td>
</tr>
);
let MyTable = (props) => (
<table>
<tr>
<th>Name</th>
<th>Days</th>
<th>pf</th>
<th>esi</th>
<th>amtPayable</th>
</tr>
{props.data.map(item => <MyRow item={item} />)}
</table>
);
const data = [
new Person({
key: '1',
name: 'Jerry gold',
days: 101
}),
new Person({
key: '2',
name: 'Arnold Smith',
days: 102
}),
new Person({
key: '3',
name: 'Baker',
days: 103
})
];
// Render it
ReactDOM.render(
<MyTable data={data} />,
document.getElementById("react")
);
table {
border-collapse: collapse;
}
table, th, td {
border: 1px solid black;
padding: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="react"></div>
This question is quite broad, but after chat discussion, it sounds like you're unsure how to reduce duplication using hard-coded employee names that will eventually be retrieved from a database.
Here is an example of using an employee array that populates a table and updates the pf, esi, and amount payable according to changes per employee days:
let employees = [ 'Jerry gold', 'Arnold Smith', 'Baker' ];
let tbody = document.getElementById('list');
let tbodyHTML = '';
employees.forEach(insertRow);
tbody.innerHTML = tbodyHTML;
function insertRow(employeeName) {
tbodyHTML += `
<tr class="employee">
<th scope="row" class="name" style="text-align: left">${employeeName}</th>
<td class="pf">--</td>
<td class="esi">--</td>
<td class="payable">--</td>
<td class="days">
<input type="number" min="0" onchange="updateEmployeeData(this)">
</td>
</tr>
`;
}
function updateEmployeeData(aNumberInput) {
let days = parseInt(aNumberInput.value);
let tr = aNumberInput.parentNode.parentNode;
let pf = days * 12 / 100;
let esi = pf * 0.25;
let payable = pf + esi + 100;
const HUNDREDTHS_PLACE = 2;
let payable_td = tr.querySelector('.payable');
tr.querySelector('.pf').innerHTML = pf.toFixed(HUNDREDTHS_PLACE);
tr.querySelector('.esi').innerHTML = esi.toFixed(HUNDREDTHS_PLACE);
payable_td.innerHTML = '$' + payable.toFixed(HUNDREDTHS_PLACE);
if (payable <= 100.15) {
payable_td.dataset.range = 'low';
} else if (payable > 100.15 && payable < 100.50) {
payable_td.dataset.range = 'med';
} else if (payable > 100.50) {
payable_td.dataset.range = 'high';
} else {
delete payable_td.dataset.range;
}
}
#employeeTable tbody>tr:nth-child(odd) {
background-color: lightgrey;
}
#employeeTable th,
td {
padding: 0.5em;
text-align: center;
}
#employeeTable th {
text-transform: uppercase;
}
#employeeTable caption {
font-style: italic;
color: grey;
}
#employeeTable td.payable {
transition: background-color 1s;
}
#employeeTable td.payable[data-range="low"] {
background-color: red;
}
#employeeTable td.payable[data-range="med"] {
background-color: yellow;
}
#employeeTable td.payable[data-range="high"] {
background-color: lightgreen;
}
<table id="employeeTable">
<colgroup class="name_col"></colgroup>
<caption>employee data</caption>
<thead>
<tr>
<th scope="col">name</th>
<th scope="col">pf</th>
<th scope="col">esi</th>
<th scope="col">payable</th>
<th scope="col">days</th>
</tr>
</thead>
<tbody id="list">
</tbody>
</table>

How to add every 12 items of an array into one <tr> set?

I have an array that every 12 items belongs to one set of data (i want to print that set into one <tr>). For example if my array got 24 items. I want to print first 12th items in one <tr> and remaining in second <tr> and so on. Could any one tell me how this can be done.Thanks
example array :
var myArray = ["1","first","Mixed","http://www.somesite.com/12.jpg","chicago","http://www.somesite.com/countries/1.jpg","false","http://www.somesite.com/logos/87.jpg","http://www.somesite.com/logos/87.jpg","winter","summer","http://somesite.com/list/thumb.jpg"];
This is the way i construct the array:
$.each($xml.find('dict>string'), function (i) {
var keyName = $(this).text();
myArray.push(keyName);
}
example <tr> to be printed via javascript:
<tr id="1">
<td>1</td>
<td><img src="http://somesite.com/list/thumb.jpg" height="42" width="42"></td>
<td>
chicago<br>
<br></td></tr>
What you really need to do is restructure your data to use an array of objects where each object contains all the key value pairs for a single table row.
Now building the html is trivial.
While we're at it, let's get rid of that inline javascript on the link and use an event handler instead:
var myArray = [{
id: 1,
name: 'first',
type: 'Mixed',
season: 'winter',
season2: 'summer',
logo1: 'http://www.somesite.com/12.jpg&city=chicago',
logo2: 'http://www.somesite.com/countries/1.jpg',
logo3: 'http://www.somesite.com/logos/87.jpg',
logo4: 'http://www.somesite.com/logos/87.jpg',
order: false,
thumb: 'http://somesite.com/list/thumb.jpg',
city: 'chicago'
}, {
id: 1,
name: 'last',
type: 'someType',
season: 'fall',
season2: 'spring',
logo1: 'http://www.somesite.com/12.jpg&city=chicago',
logo2: 'http://www.somesite.com/countries/1.jpg',
logo3: 'http://www.somesite.com/logos/87.jpg',
logo4: 'http://www.somesite.com/logos/87.jpg',
order: true,
thumb: 'http://somesite.com/list/thumb.jpg',
city: 'new york'
}, ];
$.each(myArray,function(i, object){
var $row = $('<tr>');
$row.append( $('<td></td>', {text:object.id}));
$row.append( $('<td></td>').append( $('<img>', {src:object.thumb, height:42, width:42}))) ;
$row.append( $('<td></td>').append( $('<a></a>', {href:'#', text:object.city, class:'doIt'}).data(object)));
$('#myTable').append($row);
});
$('#myTable').on('click','.doIt',function(e){
// use this handler to access the data on the link
// put your logic from "doIt()" in here instead
e.preventDefault();
var $this=$(this);
console.log($this.data('name'));
console.log($this.data('type'));
console.log($this.data('season'));
// etc....
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table id="myTable">
</table>
you can do it easily by spliting string. put your all data into a string, divide each 12 by special character like |, then divide inside strings from , and split it twice. below is a example and I used example data to understand. hope this will help to your. refere the working demo.
<html>
<head></head>
<title></title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<style>
#mytable
{
border-collapse: collapse;
}
table td{
border:1px solid black;
}
</style>
<body>
<table id='mytable'>
</table>
<script type="text/javascript">
var the_data = "1,first,mixed,http://www.first.com,chicago,http://www.somefirst.com/contry.jpg,http://www.somitemsone.com/logos/87.jpg,http://another.jpg,winter,summer,http://someitmeone/logo/one.jpg,http://oneone.com|2,22,2222,22222,22222,222222,22222222,22222222,222222222,222222222,22222222,22222222222";
var splited = the_data.split('|');
var create;
for (var i =0;i<splited.length;i++)
{
create = "<tr>";
var myarray = splited[i].split(',');
for(var j=0;j<myarray.length;j++)
{
var value = myarray[j];
create += "<td>"+value+"</td>";
}
create += "</tr>";
$("#mytable").append(create);
}
</script>
</body>
</html>
like above you can print your other data also. have a nice day

Knockout matrix

I am trying to make a matrix matching table using knockout, but I'm stuck regarding the compare between the current data-bind and the parent data-bind.
Here is the jsfiddle.net/wup9rxeu/4/
<script type="text/html" id="cubeheader-template">
<th data-bind="text: $data.Name"></th>
</script>
<script type="text/html" id="body-template">
<!-- ko foreach: $parent.modeldim -->
<td>x</td>
<!-- /ko -->
</script>
What I want to accomplished is that when the table is populated with x and - for each td, based on the modelcubdim data.
I need some pointer on comparing the ID against the parent ID and if it's a match, then X or else -
Thanks
You can expand your model with transformed data to represent every cell in the table.
// just for easy searching items by its ID
data.itemById = function(arr, id){
return ko.utils.arrayFirst(arr, function(item){
return item.ID == id;
});
};
// the property that will hold actual data for *every* table row
// in the format { Name: [Cub Name], Data [array of "x" and "-"] }
data.series = ko.utils.arrayMap(data.modelcub, function(cub){
var cubdim = data.itemById(data.modelcubdim, cub.ID);
return {
Name: cub.Name,
Data: ko.utils.arrayMap(data.modeldim, function(dim){
var item = cubdim && data.itemById(cubdim.CubeDimension, dim.ID);
return item ? "x" : "-";
})
};
});
Then slightly change your markup:
<tbody data-bind="foreach: series">
<tr>
<th data-bind="text: Name"></th>
<!-- ko foreach: Data -->
<td data-bind="text: $data"></td>
<!-- /ko -->
</tr>
</tbody>
And you will get it working like here: http://jsfiddle.net/wup9rxeu/5/

Iterate properties of an object knockoutjs

From what I've been able to find online I don't think it's possible to use the foreach data-bind to iterate through the properties of an observable object in knockout at this time.
If someone could help me with a solution to what I'm trying to do I'd be very thankful.
Let's say I have an array of movies objects:
var movies = [{
title: 'My First Movie',
genre: 'comedy',
year: '1984'
},
{
title: 'My Next Movie',
genre: 'horror',
year: '1988'
},
];
And what I would like to do is display this data in a table, but a different table for each genre of movie.
So I attempted something like this:
<div data-bind="foreach: movieGenre">
<table>
<tr>
<td data-bind="year"></td>
<td data-bind="title"></td>
<td data-bind="genre"></td>
</tr>
</table>
</div>
and my data source changed to look like this:
for (var i = 0; i < movies.length; ++i) {
if (typeof moviesGenres[movies.genre] === 'undefined')
moviesGenres[movies.genre] = [];
moviesGenres[movies.genre].push(movie);
}
I've tried about a dozen other solutions, and I'm starting to wonder if it's my lack of knowledge of knockout(I'm pretty green on it still), or it's just not possible the way I'd like it to be.
You can make your array "movies" an KO observable array and the array "movieGenre" a KO computed property. Have a look at this fiddle.
The code in the fiddle is given below for reader convenience;
KO View Model
function MoviesViewModel() {
var self = this;
self.movies = ko.observableArray([
{
title: 'My First Movie',
genre: 'comedy',
year: '1984'
},
{
title: 'My Next Movie',
genre: 'horror',
year: '1988'
},
{
title: 'My Other Movie',
genre: 'horror',
year: '1986'
}
]);
self.movieGenre = ko.computed(function() {
var genres = new Array();
var moviesArray = self.movies();
for (var i = 0; i < moviesArray.length; i++) {
if (genres.indexOf(moviesArray[i].genre) < 0) {
genres.push(moviesArray[i].genre);
}
}
return genres;
});
};
HTML
<div data-bind="foreach: movieGenre()">
<h3 data-bind="text: 'Genere : ' + $data"></h3>
<table border="solid">
<thead>
<tr>
<th>Title</th>
<th>Genre</th>
<th>Year</th>
</tr>
</thead>
<tbody data-bind="foreach: $parent.movies">
<!-- ko if: $data.genre === $parent -->
<tr>
<td data-bind="text: $data.title"></td>
<td data-bind="text: $data.genre"></td>
<td data-bind="text: $data.year"></td>
</tr>
<!-- /ko -->
</tbody>
</table>
</div>
As you can see "movieGenre" is made a computed property. Whenever the observable array "movies" changes, the moveGenre is calculated and cached. However, since this is not declared as a writable computed property, you cannot bind this to your view. Hence, it's value is used in the data binding.
The approach for rendering is simply looping through the calculated "movieGenre", and nest another loop for movies. Before adding a row to the table, for the corresponding table, the movie object is evaluated with the current movieGenre. Here, the container-less control flow syntax is used. We can use the "if" binding, but that would leave an empty table row per each movie object where the genre is otherwise.
The $parent binding context is used to access the parent contexts in the nested loop.
Hope this helps.

How do I render X <option>s in a <select>using jQuery.tmpl?

I'm inserting rows in a <table> that are bound to a JSON object. A cell in the table contains a <select> that needs to contain the number of objects, so 7 objects would produce 7 <option>s, 1-7.
Can this be done in the template definition?
Edit: I've added sample data and markup. From this data, I would want to add 3 <option>s (1-3) to the DisplayOrder <select> and have the appropriate <option> selected for each <select>.
Example: http://jsfiddle.net/4SxTS/
var data = [{
"Id": 1,
"DisplayOrder": 1,
"LabelText": "Unit On-Line Status:"},
{
"Id": 2,
"DisplayOrder": 2,
"LabelText": "AGC Pct:"},
{
"Id": 3,
"DisplayOrder": 3,
"LabelText": "Main Steam Heat Rate
}];
<table border="1" cellpadding="0" cellspacing="0">
<thead><tr><th>Display Order</th><th>Label Text</th></tr></thead>
<tbody></tbody>
</table>
<script id="attributeTemplate" type="text/x-jquery-tmpl">
<tr>
<td><select name="DisplayOrder"></select></td>
<td><input type="text" name="LabelText" value="${LabelText}" /></td>
</tr>
</script>
I'm not sure exactly what you mean without some sample data and markup, but I think the main part of your question deals with looping through numbers and rendering an option tag for each number. You could do this by creating an array containing "N" elements and then using {{each}}, but I think that using a custom template tag works well here:
Define a {{for}} tag that iterates through numbers 0 to n:
$.extend(jQuery.tmpl.tag, {
'for': {
open: "for(var $i = 0; $i < $1; $i++) {",
close: "}"
}
});
Use the new tag in a template:
<script type="text/html" id="row-tmpl">
{{each Rows}}
<tr>
<td>${Title}</td>
<td>${Description}</td>
<td>
<select>
{{for $data.TotalObjects}}
<option>${$i}</option>
{{/for}}
</select>
</td>
</tr>
{{/each}}
</script>
Called with:
var rows = {
TotalObjects: 5,
Rows: [{
Title: 'Test Row',
Description: 'Test Row Description'
},
{
Title: 'Another Row',
Description: 'Another Row Description'
}]
};
Best demonstrated in a working example: http://jsfiddle.net/xgE3Y/
Your situation might be even simpler, but my answer should enable you to at least use a simple for loop in a template and act on each iteration's value.
Maybe something like this:
$(function () {
var thisSelectObjects = 8;
var thatSelectObjects = 2;
$.fn.setOptions = function (options) {
var settings = {
'selectNum': 1
};
return this.each(function () {
if (options) {
$.extend(settings, options);
}
for (i = 0; i < settings.selectNum; i++) {
$(this).append('<option>' + i + '<option/>');
}
});
};
$('#thisSelect').setOptions({ 'selectNum': thisSelectObjects });
$('#thatSelect').setOptions({ 'selectNum': thatSelectObjects });
});
Html:
<body>
<select id='thisSelect'></select>
<select id='thatSelect'></select>
</body>

Categories