Knockout.js ObservableArray sort not updating UI - sorting

I have a Knockout observable array that refuses to update the UI (a jquery Accordion) to which it is bound after a .sort() call, but happily updates the UI after a .reverse() call - I've been banging my head against this problem for days - can't seem to find an answer... help!
Container View Model - implements the observableArray Property :
function DataTextsKOViewModel( jsonTexts ) {
var self = this;
// Texts array
self.texts = ko.observableArray();
// Build from json data array
for (var i = 0; i < jsonTexts.AuthorityTexts.length; i++) {
var jsontext = jsonTexts.AuthorityTexts[i];
// Push Text VModel objects onto the KO observable array list ..
self.texts.push(
new DataTextKOViewModel( jsontext )
);
}
}
Array Object Model - These are the objects which are sorted:
// Single data text view model
function DataTextKOViewModel(jsonText) {
// Other properties omitted for brevity
this.ListOrder = ko.observable(jsonText.ListOrder);
}
Custom Sort Function :
function textListOrderCompare(l, r) {
// Ascending sort
var retval = ((l.ListOrder() == r.ListOrder()) ? 0
: ((l.ListOrder() > r.ListOrder()) ? 1 : -1));
return retval;
}
Binding Markup :
<!-- ko foreach: texts -->
<div class="group" data-bind="attr:{'id': clientId() }">
<h3 data-bind="attr:{'dataKey': responsibilityId() }">
<span data-bind="text: responsibilitySectionHeader"></span>
</h3>
<div>
<!-- section content goes here -->
</div>
</div>
When User Elects to sort:
myDataTextsKOViewModel.rollbackChanges();
dataTextsViewModel.texts.sort(textListOrderCompare);
// UI Never updates - but I've checked console output to ensure that the array
/ /sorts correctly
dataTextsViewModel.texts.sort(textListOrderCompare);
// UI updates with items in reverse order
Any help you can give me would be most appreciated.
EDIT: Should have mentioned - I already call valueHasMutated() on the observable array property after the .sort()! - Andrew

Try to call valueHasMutated function after sorting:
dataTextsViewModel.texts.sort(textListOrderCompare);
dataTextsViewModel.texts.valueHasMutated();

dataTextsViewModel.texts(dataTextsViewModel.texts().sort(textListOrderCompare));
this way you change observable. Other way was changing the array inside and didn't trigger the change.

Related

Use methods and computed properties in child component

In my List component I have a method which count the length of the array within certain categories.
methods: {
getLengthofaCategory(cat) {
const LowerCaseSearch = this.search.toLowerCase();
let categoryCount = this.products.filter(
product =>
(product.name.toLowerCase().includes(LowerCaseSearch) ||
product.category.toLowerCase().includes(LowerCaseSearch)) &&
(!this.checked.length || this.checked.includes(product.category)) &&
product.category === cat
);
return categoryCount.length;
}
}
See here my setup in this sandbox.
But I want the values next to the checkboxes (which are coming from my CheckBox component).
How do I get the logic from the method getLengthofaCategory into my CheckBox component?
So I am able to use {{ getLengthofaCategory('tennis') }} in the v-for loop, inside the CheckBox component. And then maybe I can also use category.value instead of hardcoding e.g 'tennis' as the paramater?
In your list.vue, you can use the already created computed function filteredData instead of doing the filter again. This saves some performance because in Vue, computed properties are "cached" once run.
So you can create a new computed function that creates an object with keys per category and value can either be just the amount or an array of products in this category.
I would then pass this computed value to the CheckBox component via a prop, then inside the CheckBox component, you can display the .length or value regarding how many items each category has:
List.vue:
computed: {
//...
amountPerCategory() {
return this.filteredData.reduce((categories, product) => {
if (!(product.category in categories)) {
categories[product.category] = [];
}
categories[product.category].push(product);
return categories;
}, {});
}
}
CheckBox.vue:
<span class="ml-2 text-gray-700 capitalize">{{ category.value }}</span> -
<span
v-if="count[category.value]"
class="ml-2 text-gray-700 capitalize"
>{{ count[category.value].length }}</span>
count: {
type: Object,
default: () => ({})
}
https://codesandbox.io/s/admiring-ellis-4hojl?file=/src/components/CheckBox.vue

Angular2 - Custom validator using old data

I'm having an issue with a custom validator that I've written. The validator is used to validate all the data in a component.
Here's the setup:
The component displays a table with X rows of data. Some of the rows are display-only mode and others will be in edit mode (certain cells in the table will have inputs, selects, etc). Backing the table is an array of data objects (tableData).
If a row is in edit mode, each input is two-way bound to the appropriate element in the tableData array.
My custom validator is applied to the form tag and takes tableData as input. Everything mostly works. The validation data looks at each row in the table and does everything I need it to do.
The issue is that the data based to my custom validator is old data. So if a row is in edit mode, when I change a value in a select, tableData is updated, but the version of it that's passed to the validator is before the update. So I'm always validating an old version of tableData.
I'm not sure how to get the validation to use the up to date version of tableData. I think the issue may be related to the fact that the select binding changes a value of an object in the tableData array, but the tableData array itself doesn't actually change.
I tried adding callback to the (change) event on the select in the row being edited. The method called on (change) manually triggers change detection using a ChangeDetectorRef, but that didn't work.
I don't want to spam everyone with all the entire files, so I've tried to just add the important snippets.
Here's the template:
<form #f="ngForm" novalidate custom-validator="{{tableData | json}}">
<p-dataTable [value]="tableData">
...
<p-column [header]="'Program Name'">
<template let-row="rowData" let-idx="rowIndex" pTemplate type="body">
<span *ngIf="!row['edit']">
{{row['data'].programName}}
</span>
<div *ngIf="row['edit']">
<select #progName="ngModel" [(ngModel)]="row['data'].programCode"
title="Select Program" required (change)="onProgramChange($event, idx)"
name="programSelect-{{idx}}">
<option [value]=""></option>
<option *ngFor="let prog of programList" [value]="prog.code">
{{prog.name}}
</option>
</select>
</div>
</template>
</p-column>
...
</p-dataTable>
</form>
Here's the backing component:
//imports...
...
private tableData: PersonAssignmentRowData[] = [];
private programList: Program[] = [];
...
onProgramChange(event: any, index: number) {
for(let prog of this.programList) {
if(prog.code == event.target.value) {
this.tableData[index].data.programAcronym = prog.acronym;
this.tableData[index].data.programLocation = prog.location;
this.tableData[index].data.programName = prog.name;
break;
}
}
}
...
Here's the validator:
#Directive({
selector: '[custom-validator]',
providers:[{provide: NG_VALIDATORS, useExisting: CustomValidator, multi: true}]
})
export class CustomValidator implements Validator{
#Input('custom-validator') tableDataString: string;
validate(control: AbstractControl) {
if(this.tableDataString == null || this.tableDataString.length == 0) {
return null;
}
let tableData: PersonAssignmentRowData[] = [];
tableData = JSON.parse(this.tableDataString);
let message: string = '';
//logic that tests the validity of the data and sets any error messages in the message variable
if(message.length > 0) {
return {'validationMessage': message};
} else {
return null;
}
}
}
Now it's clear. Of course it will not work. The only data source that the validator should check is a control that's passed to validate() method. No #Inputs() or anything of the kind. The only difference is that control.value below will contain all values of all controls in the form, not just your table, so you should pick the correct nested value where your table is.
#Directive({
selector: '[custom-validator]',
providers:[{provide: NG_VALIDATORS, useExisting: forwardRef(() => CustomValidator), multi: true}]
})
export class CustomValidator implements Validator {
validate(control: AbstractControl) {
tableData = control.table.value; // need to figure out exact path based on your form structure
let message: string = '';
//logic that tests the validity of the data and sets any error messages in the message variable
if(message.length > 0) {
return {'validationMessage': message};
}
return null;
}
}

I want to check at least one value in ng-repeat, if found, then to display <div> class

In my ng-repeat, scored/not scored are there. I want to display <div> class if at least one item in the ng-repeat has "Not Scored"
You should be determining whether or not you are displaying that div inside your controller. Doing it within the ng-repeat would mean you would have logic in your view and that's just not good practice. Here is a simple example on how to accomplish what you're wanting.
In your controller:
$scope.showDiv = false;
$scope.getItems = function () {
// fetch items via ajax...
for (var i = 0; i < data.items.length; i++) {
if (data.items[i].foo == 'Not Scored') {
showDiv = true;
}
}
}
And on your view:
<div ng-show="showDiv">
// do your ng-repeat here
</div>

How to calculate a total in a grid with breeze.js extended entities?

I am working on an MVC web application using the MVVM pattern, breeze.js and knockout.js. This is the first time I use these js libraries and I still have to grasp how they work.
One of the pages of the application has a grid where both columns and rows are generated dynamically. I need to add an additional column where for each row I have the total of values displayed in the following row cells. Here an example:
Data type | Comment | Fact 1 | Fact 2 | Total | Value 1 | Value 2 | Value 3 | Value 4
==============================================================================================
Item 1 | any comment | fact 1 | fact 2 | calc. sum | 10 | 20 | 30 | 40
The grid is generated by binding a breeze entity object (planningItems) to templates. The object has the properties DataTypeId, Comment, Member, Total, FactValues. Total is the calculated sum.
<script type="text/html" id="list-planning-template">
<tr data-bind="mouseOverButton: $data">
<td style="text-align: center">
<button class="actionbutton actionbutton-item" data-bind="selectItem: $root.selectedItems, itemId: FactId"></button>
</td>
<td data-bind="text: DataTypeId" />
<td data-bind="text: Comment().Text" />
<!-- ko foreach: FactMembers -->
<td data-bind="text: Member().Code"></td>
<!-- /ko -->
<td data-bind="text: Total" />
<!-- ko foreach: FactValues -->
<td style="width: 50px" data-bind="text: Value"></td>
<!-- /ko -->
</tr>
I have been trying to add the Total property by extending the breeze entity object in the following way:
var FactCtor = function () {
this.Total = ko.computed({
read: function () {
var sum = 0;
if (this.FactValues) {
this.FactValues().forEach(function (fv) {
sum += fv.Value();
});
}
return sum;
},
deferEvaluation: true
}, this);
};
manager.metadataStore.registerEntityTypeCtor("Fact", FactCtor);
Essentially, what this code is supposed to do is to extend the entity by adding a knockout computed observable named Total with deferred evaluation. The function iterates through the breeze observable array FactValues and adds the values. I have been mucking about different versions of this code to no avail. Can anyone give me a hint on what is wrong with this code?
UPDATE:
We were not able to get the code posted in my previous post to work. We were eventually able to overcome the problem by using a custom binding with breeze. Here is the code:
ko.bindingHandlers.getFyTotal = {
update: function (element, valueAccessor) {
var sum = 0;
var fact = valueAccessor();
if (fact.FactValues()) {
fact.FactValues().forEach(function (fv) {
sum += parseFloat(fv.Value());
});
}
$(element).html(sum);
}
};
The custom binding is then referenced in the HTML code the following way:
<td data-bind="getFyTotal: $data" />
Hope this may help others.
REVISED VERSION:
We have updated the above code to take advantage of ko.utils functions:
ko.bindingHandlers.getFyTotal = {
update: function (element, valueAccessor) {
var sum = 0;
var fact = valueAccessor();
if (fact.FactValues()) {
ko.utils.arrayForEach(fact.FactValues(), function (fv) {
sum += parseFloat(fv.Value());
});
}
$(element).html(sum);
}
};
I modeled your code outside of Breeze and it's working:
http://jsfiddle.net/DazWilkin/yGZ7g/7/
I made a small tweak of adding a reference to FactValues (observableArray) on your constructor to overcome what - I believe - is the looping/this issue in JavaScript.
However, I haven't tried this in Breeze and wanted to do something similar. I was unable to get a similar function working and ultimately, created the totals during the 'then' processing of my executeQuery:
...manager.executeQuery(....).then(function(data) {
...
Fact.Total(FactValues()
.map(function(fv){ return fv.Value(); })
.reduce(function (total,curr) { return total+curr; });
...
}
I will try to get back to working on my version of this today and, if I find a better solution, I'll report back.

Sorting Div's With PrototypeJS

I am looking for some help in sorting div's with PrototypeJS. Here is what I have so far:
document.observe('dom:loaded',function(){
$$('.sortcol').invoke('observe', 'click', function() {
if (this.hasClassName('desc')) {
var desc = false;
this.removeClassName('desc');
} else {
var desc = true;
this.addClassName('desc');
}
var colname = this.className;
var contentid = this.up(2).id;
sortColumn(contentid,colname,desc);
});
});
function sortColumn(contentid,colname,desc) {
$$('#'+contentid).select('.'+colname).sort(function(a,b){
if (desc) {
return (a.text.toLowerCase() >= b.text.toLowerCase() ) ? -1 : 1;
} else {
return (a.text.toLowerCase() < b.text.toLowerCase() ) ? -1 : 1;
}
});
}
Example data:
<div id="contentbox_Users" class="userList">
<div class="userListHeader">
<div class="userListHeaderCell col1">First Name</div>
<div class="userListHeaderCell col2">Last Name</div>
</div>
<div id="contentbox_People">
<div class="userListRow">
<div class="userListCell col1">John</div>
<div class="userListCell col2">Smith</div>
</div>
<div class="userListRow">
<div class="userListCell col1">Bob</div>
<div class="userListCell col2">Ray</div>
</div>
<div class="userListRow">
<div class="userListCell col1">Fred</div>
<div class="userListCell col2">Jones</div>
</div>
</div>
</div>
Basically anything with a class "sortcol", when it is clicked, I want it to sort by the column name clicked (class). The first issue is I need to be able to get the class name correctly when there is multiple classes. The classes are all like col1, col2, etc. How would I find the correct class?
The second thing is changing sortColumn so that it keeps column data together (each row is wrapped by another div) and output the result, replacing the current data.
This needs to be done in prototypejs and I can't change the code to tables.
Thanks in advance for the help.
For the first part of your question it would be much easier if the column name was it's own attribute like rel or data-*, but you say you cannot change the HTML. It is possible to pick out the likeliest class with regex...
var colname = this.className.match(/\bcol\d+\b/).first()
But this is unnecessary if we assume every row has the same columns in the same order. This would be a safer assumption if a table were used.
var colnumber = this.up().childElements().indexOf(this);
The second part of your question is easy, just sort the rows instead of the cells.
Your draft sortColumn function doesn't actually change the elements - select returns an array of element references, not their container - so you need to do something with the resulting array. Luckily any append or insert action of an element causes it to be removed from it's parent first, so simply append them once more and they'll assume the correct order. No replacing is needed, I've seen libraries that bizarrely convert the elements to HTML, concatenate that then reinsert it!?!
The following has been tested.
document.observe('dom:loaded',function() {
$$('.userListHeaderCell').invoke('observe', 'click', function() {
this.toggleClassName('desc');
var colnumber = this.up().childElements().indexOf(this);
var content = this.up(2); // use the element directly instead of it's ID
sortColumn(content, colnumber, this.hasClassName('desc'));
});
});
function sortColumn(content, colnumber, desc) {
content.select('.userListRow').sort(function(a,b){
var atext = a.down(colnumber).innerHTML.stripTags().toLowerCase();
var btext = b.down(colnumber).innerHTML.stripTags().toLowerCase();
return atext.localeCompare(btext) * (desc ? -1 : 1);
}).each(Element.prototype.appendChild, content);
}
This to me seems like you are creating tabular data. So why not use a table? And once you use a table, there are many sorting scripts out there. A quick google came up with this one.

Resources