Display a comparison table - javascript

I have a html table with each column displaying a company and each row has a feature that offers this company.
Let's say
Company A has features 1,2,3 and features 8,9 optional.
Company B has features 1,3 and features 7,8 optional.
Company C has features 3,4 and features 9,10 optional.
//A B C
//-------------
//1 | Y Y X
//2 | Y X X
//3 | Y Y Y
//4 | X X Y
//7 | X O X
//8 | O O X
//9 | O X O
//10 | X X O
I dont display features 5,6 because they are missing in all companies.
I want the table to display "fa-check"(Y) when the company contains the feature, "fa-times"(X) when is missing and input type="checkbox" when the feature is optional. Each optional feature has a price so the total price for a company is recalculated when is checked
<table>
<thead>
<tr>
<!-- ko foreach : companies -->
<th data-bind="text: name"></th>
<!-- /ko -->
</tr>
</thead>
<tbody>
<!-- ko foreach : UnionOfFeatures-->
<tr>
<!-- ko foreach : companies -->
<td data-bind="if: mandatory"><i class="fa fa-check"></i></td>
<td data-bind="ifnot: mandatory"><input type="checkbox" data-bind="checked: Checked"></td>
#*<td data-bind="when is Missing"><i class="fa fa-times"></i></td>*#
<!-- /ko -->
</tr>
<!-- /ko -->
</tbody>
<script type="text/javascript">
function feature(id, mandatory) {
var self = this;
self.id = id;
self.mandatory = ko.observable(mandatory);
}
function company(name, features) {
var self = this;
self.name = name;
self.features = ko.observableArray(features);
}
var viewModel = function () {
self.companies = ko.observableArray(
[
new company("Company 1", [
new feature(1, true),
new feature(2, true),
new feature(3, true),
new feature(8, false),
new feature(9, false)
]),
new company("Company 2", [
new feature(1, true),
new feature(3, true),
new feature(7, false),
new feature(8, false)
]),
new company("Company 3", [
new feature(3, true),
new feature(4, true),
new feature(9, false),
new feature(10, false)
]),
]);
self.UnionFeaturesIds = ko.computed(function () {
return 0; //????;
});
}
ko.applyBindings(new viewModel());

You're missing var self = this inside your viewModel.
I've had a quick play with this, and I decided to do it in slightly different way. Feel free to change any of my code, I'm just having fun ;p
I've produced an array of all features, which I can always reference. Then I created a new helper logic inside each company that will loop through all features.
If a given feature is found, it's then added, otherwise we create a dummy object that we know is a missing feature (I've added ID of -1)
var allFeatures = ko.observableArray();
function feature(id, mandatory) {
var self = this;
self.id = id;
self.mandatory = ko.observable(mandatory);
}
function company(name, features) {
var self = this;
self.name = name;
self.features = ko.observableArray(features);
}
var viewModel = function() {
var self = this;
self.companies = ko.observableArray(
[
new company("Company 1", [
new feature(1, true),
new feature(2, true),
new feature(3, true),
new feature(8, false),
new feature(9, false)
]),
new company("Company 2", [
new feature(1, true),
new feature(3, true),
new feature(7, false),
new feature(8, false)
]),
new company("Company 3", [
new feature(3, true),
new feature(4, true),
new feature(9, false),
new feature(10, false)
])
]);
self.setFeatures = function(features) {
var featuresToChange = features;
var tempFeatures = [];
// loop through all features, so we can create all rows in the HTML
for (var i = 0; i < allFeatures().length; i++) {
var currentFeature = featuresToChange()[i];
// see if current feature exists in a given company
var featureWithIdFound = ko.utils.arrayFirst(featuresToChange(), function(item) {
return item.id === allFeatures()[i];
});
// if the feature was found, and we are currently looping through its ID, push it to temporary array
if (featureWithIdFound !== null && featureWithIdFound.id === allFeatures()[i]) {
tempFeatures.push(featureWithIdFound);
} else {
// otherwise push a feature that's missing, by giving it's negative ID
tempFeatures.push(new feature(-1));
}
}
// push to existing features array in that company
featuresToChange(tempFeatures);
}
self.createAllFeaturesList = function() {
var _allFeatures = [];
// loop through all companies to get unique features
for (var i = 0; i < self.companies().length; i++) {
var curCompany = self.companies()[i];
// push all unique items to temporary array called _allFeatures
ko.utils.arrayFirst(curCompany.features(), function(item) {
if (_allFeatures.indexOf(item.id) < 0 && item.id !== -1) {
// only push items that don't exist in the array, so we don't end up with duplicated
_allFeatures.push(item.id);
}
});
}
// sort IDs
_allFeatures.sort(function(a, b) {
return a > b ? 1 : -1
});
allFeatures(_allFeatures);
ko.utils.arrayForEach(self.companies(), function(item) {
// apply them to table
self.setFeatures(item.features);
});
};
// instantiate features
self.createAllFeaturesList();
}
ko.applyBindings(new viewModel());
.fa-check {
color: green;
}
.fa-times {
color: red;
}
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<table>
<thead>
<tr data-bind="foreach: companies">
<th data-bind="text: name"></th>
</tr>
</thead>
<tbody>
<tr data-bind="foreach: companies">
<td>
<table>
<tbody data-bind="foreach: features">
<tr>
<td>
<i class="fa fa-check" data-bind="visible: mandatory"></i>
<input type="checkbox" data-bind="visible: !mandatory() && id > 0"/>
<i class="fa fa-times" data-bind="visible: id < 0"></i>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
I hope that this helps.

Related

What am I doing wrong in creating this dynamically generated table?

Apologies for any simple mistakes, this is my first Stack Overflow post and I'm relatively new to coding.
I am attempting to create a website that displays a dynamically generated table using DOM elements. This table's rows and columns have been defined using DOM elements, and it should now populate itself on page load when storeItemOutput() is called from initialize(). It should be pulling the data from a loop through the previously defined and populated array storeItems, and displaying their attributes in the table id "storeItemOutput". It should also get one of five values from a dropdown box, and display items that match the selected category whenever it is changed.
However, I can't get the table itself or it's contents to actually display on the page. I'm unsure what is preventing this, and the lack of any output has left me stumped. Am I missing some code in my function? Is the table not created properly?
I've included parts of my code below, as well as expected output and actual output to try and help you understand my issue.
<select class="categoryDropDown" style="margin: 30px;">
<p>
<option selected value="All" onload="storeItemOutput();">All</option>
<option value="Tops" onchange="storeItemOutput();">Tops</option>
<option value="Bottoms" onchange="storeItemOutput();">Bottoms</option>
<option value="Shoes" onchange="storeItemOutput();">Shoes</option>
<option value="Accessories" onchange="storeItemOutput();">Accessories</option>
</p>
</select>
<table id="storeItemOutput">
<span><strong>| ID | Product Name | Price | Qty | Max | Category | Image |</strong></span>
</br>
<tbody>
<tr>
<th>b</th>
<th>b</th>
<th>b</th>
<th>b</th>
<th>b</th>
<th>b</th>
<th>b</th>
</tr>
<tr>
<td>b</td>
<td>b</td>
<td>b</td>
<td>b</td>
<td>b</td>
<td>b</td>
<td>b</td>
</tr>
</tbody>
(Output all store items via DOM table here)
</table>
This is some of my HTML code with an ID'd dummy table, and a dropdown menu class.
var storeItems = [];
function StoreItem(id, name, price, qtyOnHand, maxPerCust, category, shipping, reviews, description, image) {
this.id = id; //String
this.name = name; //String
this.price = price; //Number
this.qtyOnHand = qtyOnHand; //Number
this.maxPerCust = maxPerCust; //Number
this.category = category; //String
this.shipping = shipping; //Number
this.reviews = reviews; //Array
this.description = description; //String
this.image = image; //String
}
storeItems.push(new StoreItem("Y2k111", "Black Hoodie", 119.99, 10, 1, "Tops", 19.99, this.reviews, "100% Cotton Hoodie in Black", "/img/home_img/link"));
Some Javascript code of creating an empty array for store items, creating an object constructor for store items, and pushing a new item to the array (normally there is more than one item being pushed, I used just one here to save space).
function storeItemOutput() {
var itemTableDiv = document.getElementById("cartItemOutput");
var table = document.createElement("table");
itemTableDiv.innerHTML = "";
document.getElementsByTagName("tbody")[0].remove();
var tBody = document.createElement("tbody");
var headerRow = document.createElement("tr");
var hC1 = document.createElement("th");
var hC2 = document.createElement("th");
var hC3 = document.createElement("th");
var hC4 = document.createElement("th");
var hC5 = document.createElement("th");
var hC6 = document.createElement("th");
var hC7 = document.createElement("th");
hC1.innerHTML = "Item ID";
hC2.innerHTML = "Item Name";
hC3.innerHTML = "Item Price";
hC4.innerHTML = "Item Quantity";
hC5.innerHTML = "Max Items Per Customer";
hC6.innerHTML = "Category";
hC7.innerHTML = "Image";
headerRow.appendChild(hC1);
headerRow.appendChild(hC2);
headerRow.appendChild(hC3);
headerRow.appendChild(hC4);
headerRow.appendChild(hC5);
headerRow.appendChild(hC6);
headerRow.appendChild(hC7);
tbody.appendChild(headerRow);
for (var index = 0; index < storeItems.length; index++) {
var products = storeItems[i];
var theRow = document.createElement("tr");
var c1 = document.createElement("td");
var c2 = document.createElement("td");
var c3 = document.createElement("td");
var c4 = document.createElement("td");
var c5 = document.createElement("td");
var c6 = document.createElement("td");
var c7 = document.createElement("td");
c1.innerHTML = products.id;
c2.innerHTML = products.name;
c3.innerHTML = "$" + products.price.toFixed(2);
c4.innerHTML = products.qtyOnHand;
c5.innerHTML = products.maxPerCust;
c6.innerHTML = products.category;
c7.innerHTML = products.image;
theRow.appendChild(c1);
theRow.appendChild(c2);
theRow.appendChild(c3);
theRow.appendChild(c4);
theRow.appendChild(c5);
theRow.appendChild(c6);
theRow.appendChild(c7);
tbody.appendChild(theRow);
}
itemTableDiv.appendChild(tbody);
var selectedCategory = document.getElementByClass("categoryDropDown").value;
var filteredItems = [];
var index = 0;
while (index < storeItems.length) {
if (storeItems[index].category == selectedCategory) {
filteredItems.push(storeItems[index]);
}
index++;
}
storeItemOutput(filteredItems);
And finally, my function that is meant to create and populate the table, before displaying the items that match the selected category.
Here is an image of what the table should look like:
working table
And the lack of output for my table:
my missing table
Any help would be appreciated.
Here's a working example. A few things worthy of mention:
I've used a template element, since it makes repeatedly creating
similar content very much faster.
Floating-point math has rounding errors. For this reason, I've stored the
prices in cents rather than dollars. Perform all math on the number
of cents, then present it as dollars & cents
A NodeList is very similar to, but slightly different than an array. It does not for instance have a forEach member function. For this reason, I used Array.from in the appendRow function. (which is actually shorter by 1 line if you use the commented code instead)
"use strict";
function newEl(tag) {
return document.createElement(tag)
}
function byId(id) {
return document.getElementById(id)
}
function qs(sel, parent = document) {
return parent.querySelector(sel)
}
function qsa(sel, parent = document) {
return parent.querySelectorAll(sel)
}
window.addEventListener('load', onLoaded, false);
function onLoaded(evt) {
var tableData = [
["PID01", "Fluffy Bear", 599, 600, 20, "Toy", "bear.png"],
["PID02", "Rubber Ducky", 1599, 40, 5, "Toy", "duck.png"],
["PID03", "Cool Handbag", 599, 1, 2, "Fashion", "bag.png"],
["PID04", "Fidget Spinner", 999, 120, 10, "Toy", "spinner.png"],
["PID05", "Lame Handbag", 4599, 60, 3, "Fashion", "bag.png"],
["PID06", "UltraMega Genuine Laptop", 170599, 20, 2, "Technology", "laptop.png"],
];
populateTable(tableData);
var categoryNames = ["All", "Fashion", "Toy", "Technology"];
populateSelect(byId('catSelector'), categoryNames);
qs('select').addEventListener('change', updateFilter, false);
}
function populateSelect(selElem, data) {
selElem.innerHTML = '';
data.forEach(txt => selElem.appendChild(new Option(txt, txt)));
}
function populateTable(data) {
data.forEach(appendRow);
function appendRow(itemData, itemIndex, items) {
let newRow = byId('productRowTemplate').content.cloneNode(true);
let cells = Array.from(newRow.firstChild.cells);
cells.forEach((cell, index) => {
if (index == 2)
cell.textContent = '$' + (itemData[index] / 100).toFixed(2);
else
cell.textContent = itemData[index];
});
// cells[0].textContent = itemData[0];
// cells[1].textContent = itemData[1];
// cells[2].textContent = '$'+(itemData[2]/100).toFixed(2);
// cells[3].textContent = itemData[3];
// cells[4].textContent = itemData[4];
// cells[5].textContent = itemData[5];
byId('storeItemOutput').tBodies[0].appendChild(newRow);
}
}
function updateFilter() {
let filter = byId('catSelector').value;
let prodRows = qsa('#storeItemOutput > tbody > tr');
if (filter == 'All') {
prodRows.forEach(row => row.classList.remove('notShown'));
} else {
prodRows.forEach(
row => {
if (row.cells[5].textContent == filter)
row.classList.remove('notShown');
else
row.classList.add('notShown');
}
);
}
}
.notShown {
display: none;
}
.price,
.qty,
.max {
text-align: right;
}
<template id='productRowTemplate'><tr>
<td class='id'></td>
<td class='name'></td>
<td class='price'></td>
<td class='qty'></td>
<td class='max'></td>
<td class='cat'></td>
<td><img class='icon'/></td>
</tr></template>
<body>
Filter:
<select id='catSelector'></select>
<table id='storeItemOutput'>
<thead>
<tr>
<th>ID</th>
<th>Product</th>
<th>Price</th>
<th>Qty</th>
<th>Max</th>
<th>Category</th>
<th>Image</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</body>

setting the readonly value in the html table for a specific line at the start of the form

I try to set readonly fields for specific lines where "sth" is set in select box
the code is executed when the page is loaded / at the start of the form
i tried:
.js file
$(document).ready(function () {
$(".ecp-row").each(function () {
var start = $(this).find(".start");
var end = $(this).find(".end");
var hours = $(this).find(".gethours");
var selectboxlist = $(this).find(".selectboxlist").eq(0).val();
if (selectboxlist === "sth") {
alert("test"); // it works
start.readOnly = true; // it doesnt work
end.readOnly = true; // it doesnt work
hours.readOnly = true; // it doesnt work
}
});
});
html
#for (int nr_rows = 0; nr_rows < #ViewBag.sthelse; nr_rows++)
{
<tr class="ecp-row">
<td id="td3" >#Html.TextBoxFor(m => m.Model7[nr_rows].a, new { #class = "start"})</td>
<td id="td4" >#Html.TextBoxFor(m => m.Model7[nr_rows].b, new { #class = "end" })</td>
<td id="td5" >#Html.TextBoxFor(m => m.Model7[nr_rows].c, new { #class = "gethours" })</td>
<td id="td6" >#Html.DropDownListFor(m => m.Model7[nr_rows].sth, new SelectList(Enum.GetValues(typeof(sth))), " ", new { #class = "selectboxlist" })</td>
</tr>
}
As stated in the comment, you problem is that you are trying to execute javascript code on a jquery object.
You can either use start[0].readOnly = true; or start.attr("readOnly","true");
Demo
$(document).ready(function() {
$(".ecp-row").each(function() {
var start = $(this).find(".start");
alert("test"); // it works
//start[0].readOnly = true;
start.attr("readOnly","true");
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<table>
<tbody>
<tr class="ecp-row">
<td id="td3"><input class="start" /></td>
</tr>
</tbody>
</table>
If you want to change readonly,you can use prop or attr:
prop:
var start = $(this).find(".start");
start.prop("readonly", true);
attr:
var gethours = $(this).find(".gethours");
gethours.attr("readonly", true);
And here's the difference between them.
demo:
#for (int nr_rows = 0; nr_rows < #ViewBag.sthelse; nr_rows++)
{
<tr class="ecp-row">
start<td id="td3">#Html.TextBoxFor(m => m.Model7[nr_rows].a, new { #class = "start" })</td>
end<td id="td4">#Html.TextBoxFor(m => m.Model7[nr_rows].b, new { #class = "end" })</td>
gethours<td id="td5">#Html.TextBoxFor(m => m.Model7[nr_rows].c, new { #class = "gethours" })</td>
</tr>
}
#section scripts{
<script>
$(function () {
var start = $(this).find(".start");
start.prop("readonly", true);
var gethours = $(this).find(".gethours");
gethours.attr("readonly", true);
})
</script>
}
result:
your are setting readonly property to true wrongly try instead jquery attr() method
var start = $(this).find(".start");
start.attr("readOnly","true");// set readonly to true

How to add dynamic dataset using chart.js in ASP.NET MVC

I'm trying to display one pie chart for each question from my database. Right now, I am only able to display one chart on the first question.
I don't know how to display charts for the rest of the questions. I'm also not sure if I'm doing it the right way? I'm currently using ViewBag to pass the data from controller. How do I do it properly?
Please help me. Thanks.
This is my View:
<table class="table">
<tr>
<th>
Questions
</th>
</tr>
#foreach (var question in (List<Testv3.Models.MyViewModel>)ViewData["questionlist"])
{
<tr>
<td>
#question.Question
<br />
<div class="chart">
<canvas id="pie-chart"></canvas>
</div>
</td>
</tr>
}
</table>
#section Scripts {
<script type="text/javascript">
var PieChartData =
{
labels: ["Agree", "Somewhat Agree", "Disagree"],
datasets: [{
label: 'Overall report',
backgroundColor: [
"#f990a7",
"#aad2ed",
"#9966FF",
],
borderWidth: 2,
data: [#ViewBag.countAgree, #ViewBag.countSomewhatAgree, #ViewBag.countDisagree]
}]
};
window.onload = function () {
var ctx1 = document.getElementById("pie-chart").getContext("2d");
window.myBar = new Chart(ctx1,
{
type: 'pie',
data: PieChartData,
options:
{
responsive: true,
maintainAspectRatio: true
}
});
}
</script>
This is my controller:
List<PsychTestViewModel> questionlist = new List<PsychTestViewModel>();
var datalistQuestions = db.Questions.ToList();
foreach (var question in datalistQuestions)
{
PsychTestViewModel ptvm = new PsychTestViewModel();
ptvm.QuestionID = question.QuestionID;
ptvm.Question = question.Question;
questionlist.Add(ptvm);
ViewBag.questionlist = questionlist;
var agree = from ans in db.Answers
where ans.Answer == 1 && ans.QuestionID == ptvm.QuestionID
select new { Answer = ans.Answer };
var somewhatAgree = from ans in db.Answers
where ans.Answer == 2 && ans.QuestionID == ptvm.QuestionID
select new { Answer = ans.Answer };
var disagree = from ans in db.Answers
where ans.Answer == 3 && ans.QuestionID == ptvm.QuestionID
select new { Answer = ans.Answer };
int Agree = agree.Count();
int SomewhatAgree = somewhatAgree.Count();
int Disagree = disagree.Count();
ViewBag.countSomewhatAgree = SomewhatAgree;
ViewBag.countAgree = Agree;
ViewBag.countDisagree = Disagree;
}

Knockout array within array

I'm trying to create a simple spreadsheet using Knockout. I'm trying to make each cell observable, so that on changes, I can evaluate the value and calculate accordingly. So if they person enters 6+7 in a cell, I can evaluate and change the value to the total.
However, I can't get each cell to be observable. Maybe I am going about it wrong.
I have tried to create a fiddle, but am now battling to get jquery loaded. So although I can run it within Visual Studio locally, the fiddle is complaining about $. (Any help fixing that would be great).
http://jsfiddle.net/tr9asadp/1/
I generate my observable array like this:
self.RowCount = ko.observable(0);
self.ColumnCount = ko.observable(0);
self.Columns = ko.observableArray([]);
self.Rows = ko.observableArray([]);
self.Refresh = function () {
for (i = 0; i < self.RowCount(); i++) {
var obj = {
data: i + 1,
calculated: i,
rowNum: i,
colNum: 0,
columns: ko.observableArray([])
};
for (j = 0; j < self.ColumnCount(); j++) {
obj.columns.push(ko.observable({
label: self.Letters[j],
value: j + 1,
colIndex: j,
rowIndex: i
}));
}
self.Rows.push(obj);
}
self.ShowSheet(self.RowCount() > 0 && self.ColumnCount() > 0);
I render a table based on the column and rows entered by the user (For now, limited to 5 by 5, as I using an array to convert 1,2,3 (columns) to A,B,C. But that's temporary and will be fixed.
How can I get each cell to be observable so that I can subscribe and fire an event on change?
You don't seem to have made use of cellObject (from your fiddle). If you add objects of type cellObject to the rows and have an observable in there for value you can subscribe to changes on that.
Fixed code:
var cellObject = function() {
var self = this;
self.data = ko.observable();
self.calculated = ko.observable();
self.rowNum = ko.observable(0);
self.colNum = ko.observable(0);
self.rows = ko.observableArray([]);
self.value = ko.observable();
}
function SpreadsheetViewModel() {
var self = this;
self.ShowSheet = ko.observable(false);
self.ShowSheet(false);
self.Letters = ['A', 'B', 'C', 'D', 'E']
self.RowCount = ko.observable(0);
self.ColumnCount = ko.observable(0);
self.Columns = ko.observableArray([]);
self.Rows = ko.observableArray([]);
function valueChanged(newValue) {
console.log("Value changed to " + newValue);
}
self.Refresh = function() {
for (i = 0; i < self.RowCount(); i++) {
var row = {
cells: ko.observableArray([])
};
for (j = 0; j < self.ColumnCount(); j++) {
var cell = new cellObject();
cell.label = self.Letters[j];
cell.data(i + 1);
cell.calculated(i);
cell.rowNum(i);
cell.colNum(j);
cell.value(j + 1);
cell.value.subscribe(valueChanged);
row.cells.push(cell);
}
self.Rows.push(row);
}
self.ShowSheet(self.RowCount() > 0 && self.ColumnCount() > 0);
}
self.Refresh();
}
var vm = new SpreadsheetViewModel();
ko.applyBindings(vm);
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<div id="spreadsheetSection">
<div class="row">
<div class="col-xs-3 text-right">No. of Columns</div>
<div class="col-xs-2">
<input type="text" class="form-control" placeholder="Columns" data-bind="value: ColumnCount">
</div>
<div class="col-xs-3 text-right">No. of Rows</div>
<div class="col-xs-2">
<input type="text" class="form-control" placeholder="Rows" data-bind="value: RowCount">
</div>
<div class="col-xs-2">
<button class="btn btn-default" data-bind="click: Refresh">Refresh</button>
</div>
</div>
<div class="row">
<!-- ko if: ShowSheet -->
<table class="table table-bordered table-hover table-striped">
<tbody>
<tr data-bind="foreach: Rows()[0].cells">
<td>
<span data-bind="text: label"></span>
</td>
</tr>
</tbody>
<tbody data-bind="foreach: Rows">
<tr data-bind="foreach: cells">
<td>
<input type="text" class="form-control" data-bind="value: value">
</td>
</tr>
</tbody>
</table>
<!-- /ko -->
</div>
</div>
Fixed fiddle: https://jsfiddle.net/tr9asadp/3/
I used a writableComputable http://knockoutjs.com/documentation/computed-writable.html so that if you type 1 + 1 in one of the cells and tab out, it will change to 2. here is the updated fiddle. http://jsfiddle.net/tr9asadp/5/
function column(label, value, colIndex, rowIndex ){
var self = this;
this.label = ko.observable(label);
this.value = ko.observable(value);
this.colIndex = ko.observable(colIndex);
this.rowIndex = ko.observable(rowIndex);
this.writableValue = ko.pureComputed({
read: function () {
return self.value();
},
write: function (v) {
self.value(eval(v))
},
owner: this
});
}

Isolating events in Angular 1.5 Component Instances

I've created a component which is being used to add "data grid" functionality to HTML tables. The headers are clickable to allow sorting of the data (ascending/descending) on that column. So far it's working great unless I have two instances of the component on the same page. When I click a header in one table, it affects both tables.
Is there a way I'm missing to isolate the component's events to only affect that instance?
Component:
angular.module('app')
.component('datagrid', {
templateUrl:'components/datagrids/datagrids.component.html',
controller:DatagridController,
})
Controller (Work in progress, I know It's a bit of a mess at the moment!):
function DatagridController($filter, datagridService){
var ctrl = this;
ctrl.today = new Date();
ctrl.sortBy = null;
ctrl.fields = [];
ctrl.data = [];
ctrl.update = function(){
var service = datagridService;
console.log(datagridService);
var updatedFields = [];
console.log(datagridService.fields);
for(var i = 0; i < datagridService.fields.length; i++){
var fieldName = datagridService.fields[i];
var fieldDirection = (ctrl.fields.length === 0) ? 'ascending' : ctrl.fields[i].direction;
updatedFields.push({name:fieldName, direction:fieldDirection});
}
ctrl.fields = updatedFields;
console.log(ctrl.fields)
if (ctrl.sortBy == null){ ctrl.sortBy = $filter('toCamelCase')(ctrl.fields[0].name); }
ctrl.data = datagridService.data.sort(ctrl.sortData(ctrl.sortBy));
ctrl.today = new Date();
};
ctrl.sortData = function(field, reverse, primer){
console.log(field + ' | ' + reverse)
var key = primer ?
function(x) {return primer(x[field])} :
function(x) {return x[field]};
reverse = !reverse ? 1 : -1;
ctrl.sortBy = field;
return function (a, b) {
return a = key(a), b = key(b), reverse * ((a > b) - (b > a));
}
};
ctrl.toggleSortDirection = function(index){
console.log(index);
var field = ctrl.fields[index];
console.log(field);
var fieldName = field.name;
console.log(fieldName);
var direction = ctrl.fields[index].direction;
console.log(direction);
var reverse = (direction == 'ascending') ? true : false;
console.log(reverse);
var direction = (direction === 'ascending') ? 'descending' : 'ascending';
console.log(direction);
for(var i = 0; i < ctrl.fields.length; i++){
ctrl.fields[i].direction = 'ascending';
}
ctrl.fields[index].direction = direction;
ctrl.data.sort(ctrl.sortData($filter('toCamelCase')(fieldName), reverse));
};
ctrl.validDatetime = function(dt){
//this should probably be a service
console.log(dt);
var rx = /([0-9]{4})\-([0-9]{2})\-([0-9]{2})/;
if(dt.match(rx)){ console.log(dt); }
return (dt.match(rx)) ? true : false;
};
ctrl.$onInit = ctrl.update();
}
DatagridController.$inject = ['$filter', 'datagridService'];
Template:
<table ng-if="$ctrl.data.length > 0" class="datagrid">
<caption ng-if="$ctrl.caption">{{ $ctrl.caption }}</caption>
<colgroup ng-if="$ctrl.colgroup.length > 0">
<col ng-repeat="col in $ctrl.colgroup">
</colgroup>
<thead ng-if="$ctrl.hasHeader = true">
<tr>
<th ng-repeat="field in $ctrl.fields" ng-click="$ctrl.toggleSortDirection($index)" data-sortable="true">{{ field.name }}<div ng-class="field.direction"></div></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="record in $ctrl.data">
<td ng-repeat="field in $ctrl.fields">
<span ng-if="!$ctrl.validDatetime(record[(field.name|toCamelCase)])"><a>{{ record[(field.name|toCamelCase)] }}</a></span>
<span ng-if="$ctrl.validDatetime(record[(field.name|toCamelCase)])"><a>{{ record[(field.name|toCamelCase)] | date: 'dd-MMM-yyyy' }}</a></span>
</td>
</tr>
</tbody>
<tfoot ng-if="$ctrl.hasFooter = true">
<td colspan="{{ $ctrl.fields.length }}">Last Updated: {{ $ctrl.today | date: 'dd-MMM-yyyy' }}</td>
</tfoot>
</table>
Component Tag:
<datagrid></datagrid>
Components are isolated by default, which means there is its own $ctr for every instance.
Thing is that data is shared through service. For example you do datagridService.data.sort in first instance => it changes data in service => it gets reflected in all instances of your component (there is one data object in memory, that you are trying to access).
One fix might be, to make copies of data for every component instance.
ctrl.data = Object.assign([], datagridService.data);
Dont do any manipulation directly on datagridService.data, but use ctrl.data instead

Categories

Resources