I'm stuck on how to update row sums in a foreach template using knockoutJS
<div id="timeEntryList" data-bind="foreach: timeEntries">
<table >
<tr>
...
<td> //there are more of this, not included here
<input type="number"
data-bind="value: Days[6].Hours,
event: { change: $root.setDirty }" />
</td>
<td> //this part needs to be updated when the above input is changed
<span data-bind="text: $root.sumRow($data)">
</span>
</td>
The last TD there contains a span element which displays the sum of hours reported for the current item in the foreach.
it displays correctly when the data is loaded, but then stays stale when I edit the elements.
How can I make this element update as I change the values of the input boxes?
Here is my view model in a very slimmed down version:
var TimeReportModel = function (init) {
this.timeEntries = ko.observableArray(init.TimeEntries);
//... helper functions
};
TimeEntries are objects representing a reported hours per week.
So it contains an array of days and each day has an hour property.
Based on what you're binding to, it appears you're binding to the result of a regular function. If you want to see the values updated when there are changes, you need to bind to an observable. Make the sum a computed observable in your view model and bind to it.
I have no idea what your view model looks like or what you are adding up but it would look something like this:
// calculate the sum of the hours for each of the days
self.totalDays = ko.computed(function () {
var sum = 0;
ko.utils.arrayForEach(self.days(), function (day) {
sum += Number(day.hours());
});
return sum;
});
Here's a fiddle to demonstrate.
Related
This question already has an answer here:
Knockout.js bound input value not updated when I use jquery .val('xyz')
(1 answer)
Closed 6 years ago.
self.totalHours = ko.pureComputed(function() {
var start=self.num1;
var end=self.num2;
return start+end;
});
<input type="text" data-bind="textInput: start">
<input type="text" data-bind="textInput: end">
<input type="text" data-bind='text: totalHours()'>
The above first is part of my viewmodel and the second is part of my model. num1,num2 are observables. Every time I change manually the value inside the above first two inputs the third input is updated immediately; however, when the values change by code, knockout does not listen to the changes and total is not updated. How may I oblige knockout to listen to the changes provoked by code?
Quite some stuff you can fix and improve here:
A computed value will re-evaluate when an observable it uses in its method changes: self.num1 and/or self.num2 need to be observable and evaluated using ()
If you want to bind an <input>'s value, you have to use either the value or textInput data-bind; the text bind will not work.
If you want to write to a computed, you'll have to specify a write method. You'll have to tell knockout how to update the computed's dependencies to make sure all values add up. (e.g.: setting totalHours could set num1 to totalHours and num2 to 0)
You've bound to start and end, while your viewmodel properties are named num1 and num2.
When using value or textInput, user input will be returned as a string. You'll need to parse the strings to numbers if you want to use them in any math.
Now that all code should be working correctly, you can update your viewmodel's values via the inputs, or via code:
var ViewModel = function() {
var self = this;
self.num1 = ko.observable(0);
self.num2 = ko.observable(0);
self.totalHours = ko.pureComputed(function() {
var start = parseFloat(self.num1());
var end = parseFloat(self.num2());
return start + end;
});
};
var vm = new ViewModel();
ko.applyBindings(vm);
// Updating your values from code:
vm.num1(1);
vm.num2(2);
// Whenever the values need to be updated via js,
// you should change the source values, _not_ the
// <input>'s values. Worst case scenario, you'll
// have to do something like this:
var updateNum1ViaDOM = function() {
ko.dataFor(document.querySelector("input")).num1(5);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input type="text" data-bind="textInput: num1">
<input type="text" data-bind="textInput: num2">
<span data-bind='text: totalHours'></span>
Note: it's probably better to use an extender to force num1 and num2 to be numeric: Live Example 1: Forcing input to be numeric
Not sure if it is a copy paste problem but the the code you posted will not work as intended. I've updated the example, when changing an observable value it must be passed as parameter so as not to overwrite the knockout observable
self.start = ko.observable();
self.end = ko.observable();
self.totalHours = ko.computed(function() {
return self.start() + self.end();
});
<input type="text" data-bind="textInput: start">
<input type="text" data-bind="textInput: end">
<input type="text" data-bind='text: totalHours()'>
//Then when changing the value by code
var newValue = 42;
model.start(newValue); //assuming you are making the change outside your viewmodel
*Just noticed this code will throw an exception when you edit the input bound to totalHours as it does not have a write handler defined. This is a separate issue though.
I have a UI similar to excel, where the user can change the data in which ever row he wants.
Currently i am using ng-repeat to display the existing data, after fetching from mongoDB. This data is an array of Json Objects , so ng-repeat works fine.
Now i want to have a single save button, clicking which, all the edits should be saved. My question is how to maintain or know which rows have been changed ?? i can just send the whole array and update in the db, but when the number of rows is large and data of just one row has changed there will be an unnecessary overhead. so let me know how this can be optimised.
Thanks in advance
Add an ng-change event to fields where they can edit the values.
On ng-change, set a new key 'modified: true' on 'obj in objects'.
Something like this:
<div ng-repeat="row in rows track by $index">
<input type="text" ng-change="markAsChanged($index)" ng-model="row.textValue">
</div>
And your JS:
$scope.markAsChanged = function(index) {
$scope.rows[index].changed = true;
}
Then your update function could iterate all rows and send changed rows to the webservice for updating instead of sending them all.
$scope.rowsChanged = [];
$scope.Changed = function(row) {
$scope.rowsChanged.push(row);
}
<div ng-repeat="row in rows track by $index">
<input type="text" ng-change="Changed(row)" ng-model="row.textValue">
</div>
Hope it helps..
This thing can be achieved by maintaining an array which contain index for all the rows that has changed. On clicking on save button , an iteration will be required on this array (For rows which have changed).
A new $index can be pushed to this array anytime when the user is just clicking on a row or on changing any input in the array.
HTML:
<div ng-repeat="row in rows track by $index">
<input type="text" ng-change="markAsChanged($index)" ng-model="row.textValue">
</div>
Controller:
var listOfChangedRows = [];
$scope.markAsChanged = function(index){
if(listOfChangedRows.indexOf(index)==-1)
{
listOfChangedRows.push(index);
}
}
//Save button Clicked
$scope.saveEditedRows = function(){
var updatedItems = [];
for(var i=0;i<listOfChangedRows.length;i++)
{
updatedItems.push($scope.rows[i]);
}
//Now send the updatedItems array to web service
}
I have a bit of HTML here:
<tr taskId="(#=obj.task.id#)" assigId="(#=obj.assig.id#)" class="assigEditRow" >
<td><select name="resourceId" class="get-resources formElements"></select></td>
<td><span class="resources-units"></span></td>
<td><span class="resources-quantity"></span></td>
<td><input type="text" placeholder="Required Q"></td>
<td align="center"><span class="teamworkIcon delAssig" style="cursor: pointer">d</span></td>
</tr>
And a bit of JS here:
'use strict';
function addResourceFunction(){
let ResourcesJSON = (json) => {
let Resources = json;
console.log(Resources);
let contactsLength = json.length;
let arrayCounter = -1;
let resID;
let resName;
let resUnit;
let resQuantity;
let Option = $('<option />');
let assignedID = $('tr.assigEditRow:last').attr("assigId");
while(arrayCounter <= contactsLength) {
arrayCounter++;
resID = Resources[arrayCounter].ID;
resName = Resources[arrayCounter].name;
resUnit = Resources[arrayCounter].unit;
resQuantity = Resources[arrayCounter].quantity;
$('.assigEditRow').last().find('select').append($('<option>', {
value: resName.toString(),
text: resName.toString(),
resourceID: resID.toString(),
resourceUnit: resUnit.toString(),
resourceQuantity: resQuantity.toString()
}));
}
}
$.getJSON("MY JSON URL IS HERE", function(json) {
ResourcesJSON(json);
});
};
So what's actually going on here: I get my data from the URL (JSON array), trigger the addResourceFunction() on click to create a new table row and to add a new select with options passed from the array. As you see from my HTML markup, the select input is placed in td.get-resources, and all that works good. I get my date set, I populate the select field and all works good. I can add as many rows/select dropdowns as I want.
Also, every option has a few custom attributes (you can see it in my JS code above), and I want to add the values of those attributes to the second and third column of the row (in HTML those are span.resources-units and span.resources-quantity). The thing is, I have no clue how to make it work 1:1, meaning that one select dropdown "alters" only units and quantity of its own row. Below is the code for that:
let idCounter = 1;
$(document).on('change', '.get-resources', function() {
$('.assigEditRow').last().find('.resources-units').attr('id', 'units-' + idCounter);
$('.assigEditRow').last().find('.resources-quantity').attr('id', 'quantity-' + idCounter);
this.resourceUn = $( ".get-resources option:selected" ).attr( "resourceUnit" );
this.resourceQuant = $( ".get-resources option:selected" ).attr( "resourceQuantity" );
$('#units-' + idCounter).append(this.resourceUn);
$('#quantity-' + idCounter).append(this.resourceQuant);
idCounter++;
});
What happens is that if I add one select input, and change options, the thing works. When I add another one and change its options, it gets attributes of the first one. Adding more - same thing. Whatever I change, it takes the attribute value of the first item added.
Try getting the id from the element instead of from the variable, since you always update the element with the id of the counter, instead of the element with the id of the row that was clicked.
Hmm, what does the counter do exactly? The more I look at it, the less I understand. What I do know is that you're not selecting the correct elements by using the idCounter to reference the correct row.
You want to do something like
$(document).on('change', '.get-resources', function() {
//var row = this;
this.find(/* Some path to the second column */).att(/* some att to change */);
this.find(/* Some path to the third column */).att(/* some att to change */);
});
where you always use the row as the root again, instead of finding a certain id, so you only update that row.
Native:
<table>
<tr>
<td>
<select>
<option data-text="resName1" data-resourceID="resID1" data-resourceUnit="resUnit1" data-resourceQuantity="resQuantity1">1</option>
<option data-text="resName2" data-resourceID="resID2" data-resourceUnit="resUnit2" data-resourceQuantity="resQuantity2">2</option>
<option data-text="resName3" data-resourceID="resID3" data-resourceUnit="resUnit3" data-resourceQuantity="resQuantity3">3</option>
</select>
</td>
<td>
<div class="column2"></div>
</td>
<td>
<div class="column3"></div>
</td>
</tr>
</table>
<script>
document.addEventListener('change', function ( event ) {
var select = event.target,
option = select.options[select.selectedIndex],
values = {
'text' : option.getAttribute('data-text'),
'resourceID' : option.getAttribute('data-resourceID'),
'resourceUnit' : option.getAttribute('data-resourceUnit'),
'resourceQuantity' : option.getAttribute('data-resourceQuantity')
},
row = select.parentNode.parentNode,/* depending on how deep the select is nested into the tr element */
column2 = row.querySelector('.column2'),
column3 = row.querySelector('.column3');
column2.textContent = 'some string with the values you want';
column3.textContent = 'some string with the other values you want';
});
</script>
Basically you start with the select that was changed, from there you get the option node that was clicked. Then you get the attributes you need from that option. Then you go up a few nodes to the row parent and find the two columns inside that row. Then you can set the content of these two columns.
I am generating a table with values from a database, the total of each columns is displayed at the end of the table.
I want to give the user the option of removing rows they don't want and have the total change accordingly.
Since the form will be submitted I already have hidden input elements added. I want to be able to get the value of a column in the row being removed and subtract it from the total, then display the new total.
I thought the best way would be to give each column a class name and for the specific row get the value of the class, as it is being deleted.
This is my table structure
<tbody>';
for($i=0;$i<count($alto);$i++){
echo' <tr>
<td>'.$key[$i].'</td>
<td>'.$talk[0][$i].'<input type="hidden" class="bate" value="'.$talk[0][$i].'" name="0'.$key[$i].'"/></td>
<td>'.$talk[1][$i].'<input type="hidden" class="vito" value="'.$talk[1][$i].'" name="1'.$key[$i].'"/></td>
<td>'.$talk[2][$i].'<input type="hidden" class="hist" value="'.$talk[2][$i].'" name="2'.$key[$i].'"/></td>
<td><button class="delete" type="button">Delete</button><td>
</tr>';
}
echo '</tbody>
From my limited knowledge of jquery I think I would need something like this:
var exp = $(this).val('.vito');
Possibly I'm having traversing issues and I need to add a .parents() tag in there somewhere, but I'm not sure.
Create function calculate_total() that calculate the table total and call it after deleting row :
function calculate_total(){
var bate_total, vito_total, hist_total = 0;
$("table").find('tr').each(function(){
bate_total += parseInt( $(this).find('.bate').val() );
vito_total += parseInt( $(this).find('.vito').val() );
hist_total += parseInt( $(this).find('.hist').val() );
});
//Here you can assign the new totals to the columns that contain totals values
}
Hope this helps.
I have a list of checkboxes in my html page, like so:
<ul id="myList">
<li class="checkboxItem">
<input type="checkbox" name="item1" value="001" id="item-1"/>
<label for="item-1" class="checkboxLabel">Display 1</label>
</li>
<li class="checkboxItem">
<input type="checkbox" name="item2" value="042" id="item-2"/>
<label for="item-2" class="checkboxLabel">Display 42</label>
</li>
</ul>
now I make a call to get some json data, which comes back like so:
[{"name":"002","title":"Display 1"}]
what I want to do is loop the returned json and update the list of checkboxes such that any item not in the returned list is disabled, and those where the title matches a given label, the input value is updated.
so in this example, item2 will be disables and item1 will have its value updates to 002.
here's what I have so far, i'm not quite sure where to go from here, that is, what to do inside the loop. I do have some control over how the json is returned, so if it makes sense to retunr the json in another format, I can do that.
EDIT, updated the function, see below. however, once I get inside the for loop inside the each function, elem is getting a value of "0" rather than a js object such as:
{"name":"002","title":"Display 1"}. clearly data, is being transferred from the outside scope of the function to the inside scope of the each function, but how do I make that happen?
function(data) {
$('#myList').children('li').each(function(i,e) {
for(var elem in data) {
var elemDescr = elem['title'];
var elemName = elem['name'];
if(elemDescr==$(this).find('.checkboxLabel').text()) {
$(this).find('input').attr('value',elemName);
}
}
});
It might be easier to have an outer loop for each checkbox, and an inner loop go through each json element, enabling or disabling based on whether the element/checkboxes have a match.
So an inversion of what you have:
function(data) {
$('#myList').children('li').each(function() {
// loop through your data elements here
});
}
Another option (probably less desirable because it may cause multiple disabled/enabled transitions) is to disable all checkboxes, and enable them as you loop through each element.
I have found my problem. rather than doing:
for(var elem in data) {
var elemDescr = elem['title'];
var elemName = elem['name'];
}
I needed to do:
for(var index in data) {
var elemDescr = data[index].title;
var elemName = data[index].name;
}