Knockout Foreach creating duplicate items - html

On our UI, we have classrooms that are eligible for assessments. After creating and adding in eligibleclassrooms, I am getting duplicates of each classroom type. The expected output is one of each classroom.
Relavant HTML:
<div class="eligible-classrooms">
<h5 class="semi-bold btm-gap-sm">Eligible Classrooms</h5>
<div class="classrooms">
<!-- ko foreach: model.sortedEligibleClassrooms -->
<div class="classroom">
<span data-bind="text: $data.ageRangeTypeName"></span><span class="count" data-bind="text: $data.numClassrooms"></span>
</div>
<!-- /ko -->
</div>
</div>
Relevant Typescript:
Model:
numberOfEligibleClassrooms: KnockoutObservableArray<{ ageRangeTypeId: number, ageRangeTypeName: string, numClassrooms: number }>,
sortedEligibleClassrooms: KnockoutComputed<Array<{ ageRangeTypeId: number, ageRangeTypeName: string, numClassrooms: number }>>
self.model:
numberOfEligibleClassrooms: ko.observableArray([]),
sortedEligibleClassrooms: null,
constructor(orgCaseId: number, observationType: ObservationTypeModel, orgObservationSetTypeId: number) {
var self = this;
self.model.sortedEligibleClassrooms = ko.computed(function () {
return self.model.numberOfEligibleClassrooms().sort((left: { ageRangeTypeId: number, ageRangeTypeName: string, numClassrooms: number }, right: { ageRangeTypeId: number, ageRangeTypeName: string, numClassrooms: number }): number => {
return left.ageRangeTypeId > right.ageRangeTypeId ? 1 : -1;
});
}, self.model.numberOfEligibleClassrooms);
self.load = (): void => {
CommonFactory.AppSettings.get('OrgClassroomCollectMajorityAgeRange', function (setting: IApplicationSetting) {
const majorityAgeRangeEnabled = setting ? setting.value.toString().toLowerCase() === 'true' : false;
OrgFactory.ObservationTypeAgeRangeTypes.search({ "observationTypeId": self.model.observationType.id(), isCount: true }, function (ageRangeTypes) {
for (var j = 0; j < ageRangeTypes.length; j++) {
// The following function preserves the scope of the variables
(function (ageRangeTypeId, ageRangeTypeName) {
var minimumAgeRangeTypeId = self.settings.isERSObservation() ? ageRangeTypeId : null;
var search = {
minimumAgeRangeTypeId: minimumAgeRangeTypeId,
includesAgeRangeTypeId: majorityAgeRangeEnabled ? null : ageRangeTypeId,
majorityAgeRangeTypeId: majorityAgeRangeEnabled ? ageRangeTypeId : null,
isCount: true
};
OrgFactory.OrgCase.getEligibleObservationClassrooms(orgCaseId, search, function (data) {
self.model.numberOfEligibleClassrooms.push({ ageRangeTypeId: ageRangeTypeId, ageRangeTypeName: ageRangeTypeName, numClassrooms: data.length });
});
})(ageRangeTypes[j].ageRangeTypeId, ageRangeTypes[j].ageRangeTypeName);
}
});
});
}
Output:

What I did to fix this is create a distinctEligibleClassrooms method that distinctly sorts through the list of eligible classrooms:
//creates a distinct array of eligible classrooms.
self.model.distinctEligibleClassrooms = ko.computed(function () {
const classrooms = self.model.numberOfEligibleClassrooms();
const seen = {};
const distinctClassrooms = classrooms.filter(classroom => {
if (!seen[classroom.ageRangeTypeId]) {
seen[classroom.ageRangeTypeId] = true;
return true;
}
return false;
});
return distinctClassrooms;
}, self.model.numberOfEligibleClassrooms);
I added this directly under the current sortedEligibleClassrooms method and removed that because it will no longer be in use.

Related

JSON data calculation and re-formate using Angular

I have a JSON file and I am trying to calculate the JSON file key based on the value and reformating it. My JSON file looks like below:
data=[
{
pet:'Cat',
fruit:'Apple',
fish:'Hilsha'
},
{
pet:'Dog',
fish:'Carp'
},
{
pet:'Cat',
fruit:'Orange',
fish:'Lobster'
}
];
I do like to calculate and formate it like below:
data=[
{
label:'Pet',
total:3,
list:[
{
name:'Cat',
value: 2,
},
{
name:'Dog',
value: 1,
}
]
},
{
label:'Fruit',
total:2,
list:[
{
name:'Apple',
value: 1,
},
{
name:'Orange',
value: 1,
}
]
},
{
label:'Fish',
total:3,
list:[
{
name:'Hilsha',
value: 1,
},
{
name:'Carp',
value: 1,
},
{
name:'Lobster',
value: 1,
}
]
},
];
If anybody can help me, it will be very help for me and will save a day.
I have fixed this task myself. If I have any wrong, you can put your comment fill-free :)
``
ngOnInit(): void {
this.dataService.$data.subscribe(data => {
// Create new object and calculation according to category
let petObj: any = {}
let fruitObj: any = {}
let fishObj: any = {}
data.forEach((el: any) => {
if (el.pet != undefined) {
petObj[el.pet] = (petObj[el.pet] || 0) + 1;
}
if (el.fruit != undefined) {
fruitObj[el.fruit] = (fruitObj[el.fruit] || 0) + 1;
}
if (el.fish != undefined) {
fishObj[el.fish] = (fishObj[el.fish] || 0) + 1;
}
});
// Create list according to category
let pet_list: any = [];
let fruit_list: any = [];
let fish_list: any = [];
for (var key in petObj) {
let pet = {
label: key,
value: petObj[key]
}
pet_list.push(pet)
}
for (var key in fruitObj) {
let fruit = {
label: key,
value: fruitObj[key]
}
fruit_list.push(fruit)
}
for (var key in fishObj) {
let fish = {
label: key,
value: fishObj[key]
}
fish_list.push(fish)
}
// Calculate total sum according to category
var totalPet = pet_list.map((res: any) => res.value).reduce((a: any, b: any) => a + b);
var totalFruit = fruit_list.map((res: any) => res.value).reduce((a: any, b: any) => a + b);
var totalFish = fish_list.map((res: any) => res.value).reduce((a: any, b: any) => a + b);
// Rearrange the JSON
this.rearrangeData = [
{
label: 'Pet',
total: totalPet,
list: pet_list
},
{
label: 'Fruit',
total: totalFruit,
list: fruit_list
},
{
label: 'Fish',
total: totalFish,
list: fish_list
}
]
console.log(this.rearrangeData)
// End rearrange the JSON
});
}
``
You can simplify your function. Take a look this one
group(oldData) {
const data = []; //declare an empty array
oldData.forEach((x) => {
//x will be {pet: 'Cat',fruit: 'Apple',fish: 'Hilsha'},
// {pet: 'Dog',fish: 'Carp'}
// ...
Object.keys(x).forEach((key) => {
//key will be 'pet','fruit',...
const item = data.find((d) => d.label == key); //search in the "data array"
if (item) { //if find it
item.total++; //add 1 to the property total of the element find it
// and search in the item.list the 'Cat'
const list = item.list.find((l) => l.name == x[key]);
//if find it add 1 to the property value of the list
if (list)
list.value++;
else
//if not, add to the list
//an object with property "name" and "value" equal 1
item.list.push({ name: x[key], value: 1 });
} else
//if the element is not in the "array data"
//add an object with properties label, total and list
//see that list is an array with an unique element
data.push({
label: key,
total: 1,
list: [{ name: x[key], value: 1 }],
});
});
});
return data;
}
You can use like
this.dataService.$data.subscribe(data => {
this.rearrangeData=this.group(data)
}
NOTE: this function the labels are 'pet','fruit' and 'fish' not 'Pet', 'Fruit' and 'Fish'
Did you try reading the text leading up to this exercise? That'd be my first approach. After that, I'd use reduce. You can do pretty much anything with reduce.

change value from of a specific item on button click

As I asked yesterday in my first post, I have a json file that looks like this:
groups:{[
{
title:Animal
shown:false
data:[{....}]
}
........
.....
]}
I want to change the shown value on a button click. The closest thing I found to my problem was this part of code:
newState = this.state.groups.map((val,i) => {
if(index === i){
return { ...val, shown: false};
}
return val;
})
this.setState({
groups: newState,
})
However, it doesn't seem to work, logging on console doesn't show any differences before and after the button press. I'm rather new to this so do you mind to help me understand what i did bad?
edit: I tried changing from index to a simple number to see if that was the problem, but still the same problem.
A JSON object is collection of Key Value pairs. i.e.
let FullName = {
firstName: "Stack",
lastName: "OverFlow"
}
In FullName Object Keys are firstName and lastName and corresponding values are "Stack" and "Overflow".
The groups Object that you have defined is missing the key Property.
Coming to Your problem:
Case1: If groups Object is an Array of Objects then:
var groups = [
{
title: 'Animal',
shown: false,
data: [{}]
},
{
title: 'Birds',
shown: false,
data: [{}]
}
]
/* Upadate By Index value */
/*
var index = 1;
let updatedGroup = groups.map((val,i) => {
if(index === i){
return { ...val, shown: true};
}
return val;
})
*/
/* Upadate By title */
/* let title = "Animal";
let updatedGroup = groups.map((val,i) => {
if(val.title === title){
return { ...val, shown: true};
}
return val;
}) */
// To toggle the shown Value Each Time
let title = "Animal";
let updatedGroup = groups.map((val,i) => {
if(val.title === title){
return { ...val, shown: !val.shown};
}
return val;
})
console.log("updatedGroup", updatedGroup);
Case2: If groups Object is Object of Objects then
var groups = {
group1: {
title: 'Animal',
shown: false,
data: [{}]
},
group2: {
title: 'Birds',
shown: false,
data: [{}]
}
}
let index = 1;
let updatedGroup = Object.values(groups).map((val, i)=>{
if(index === i){
return { ...val, shown: true};
}
return val;
})
console.log("updatedGroup",updatedGroup)

Loading different modal based on knockout.js value

So I have a table of people's information and I used codes that look something like below (people's location in this case):
<!-- ko foreach: filteredApplications -->
<div class="col-md-6">
<span>Location:</span>
<span data-bind="text: application.candidateLocation"></span>
</div>
to display location information for all people.
And when I click the "Preview Application" link, it is supposed to give me a modal like the attached image and show the information of the corresponding person.
I tried to remove the foreach knockout for the modal, but then it gives me an error saying that it cannot find the application.candidateLocation variable.
How do I have to approach this issue?
Please help!
EDIT (viewmodel):
public partial class Application
{
public dynamic JsonForm => new
{
CandidateLocation,
Job = this.Job.JsonForm,
CandidateStatus = this.CurrentStatuse.CandidateStatus,
};
public string CandidateLocation
{
get
{
switch (ApplicationCandidateType)
{
case ApplicationCandidateType.Standard:
return InsideApplication.CandidateApplication.Candidate.Location;
case ApplicationCandidateType.Guest:
return null;
case ApplicationCandidateType.Manual:
return null;
default:
throw new Exception("Unhandled ApplicationCandidateType");
}
}
}
function ViewModel() {
var self = this;
self.invite = ko.observable(false);
self.changeStylesInvite = function () {
self.invite(true);
}
self.notifications = ko.observableArray(#Html.Json(Model.Notifications.Select(o => o.JsonForm)) || []);
self.applications = ko.observableArray(#Html.Json(Model.ApplicationCompatibilities.Select(o => o.JsonForm)) || []);
self.applicationInvitations = ko.observableArray(#Html.Json(Model.ApplicationInvitations.Select(o => o.JsonForm)) || []);
self.applicationsFilter = ko.observable("new");
self.showHiddenApplications = ko.observable(false);
self.newApplicationsCount = ko.computed(function() {
return ko.utils.arrayFilter(self.applications(), function(i) {
return !i.application.isShortlisted && !i.application.isContactInfoSent && (self.showHiddenApplications() || !i.application.isHidden);
}).length;
});
self.shortlistedApplicationsCount = ko.computed(function() {
return ko.utils.arrayFilter(self.applications(), function(i) {
return i.application.isShortlisted && (self.showHiddenApplications() || !i.application.isHidden);
}).length;
});
self.connectedApplicationsCount = ko.computed(function() {
return ko.utils.arrayFilter(self.applications(), function(i) {
return i.application.isContactInfoSent && (self.showHiddenApplications() || !i.application.isHidden);
}).length;
});
self.allApplicationsCount = ko.computed(function() {
return ko.utils.arrayFilter(self.applications(), function(i) {
return (self.showHiddenApplications() || !i.application.isHidden);
}).length;
});

Transform Request to Autoquery friendly

We are working with a 3rd party grid (telerik kendo) that has paging/sorting/filtering built in. It will send the requests in a certain way when making the GET call and I'm trying to determine if there is a way to translate these requests to AutoQuery friendly requests.
Query string params
Sort Pattern:
sort[{0}][field] and sort[{0}][dir]
Filtering:
filter[filters][{0}][field]
filter[filters][{0}][operator]
filter[filters][{0}][value]
So this which is populated in the querystring:
filter[filters][0][field]
filter[filters][0][operator]
filter[filters][0][value]
would need to be translated to.
FieldName=1 // filter[filters][0][field]+filter[filters][0][operator]+filter[filters][0][value] in a nutshell (not exactly true)
Should I manipulate the querystring object in a plugin by removing the filters (or just adding the ones I need) ? Is there a better option here?
I'm not sure there is a clean way to do this on the kendo side either.
I will explain the two routes I'm going down, I hope to see a better answer.
First, I tried to modify the querystring in a request filter, but could not. I ended up having to run the autoqueries manually by getting the params and modifying them before calling AutoQuery.Execute. Something like this:
var requestparams = Request.ToAutoQueryParams();
var q = AutoQueryDb.CreateQuery(requestobject, requestparams);
AutoQueryDb.Execute(requestobject, q);
I wish there was a more global way to do this. The extension method just loops over all the querystring params and adds the ones that I need.
After doing the above work, I wasn't very happy with the result so I investigated doing it differently and ended up with the following:
Register the Kendo grid filter operations to their equivalent Service Stack auto query ones:
var aq = new AutoQueryFeature { MaxLimit = 100, EnableAutoQueryViewer=true };
aq.ImplicitConventions.Add("%neq", aq.ImplicitConventions["%NotEqualTo"]);
aq.ImplicitConventions.Add("%eq", "{Field} = {Value}");
Next, on the grid's read operation, we need to reformat the the querystring:
read: {
url: "/api/stuff?format=json&isGrid=true",
data: function (options) {
if (options.sort && options.sort.length > 0) {
options.OrderBy = (options.sort[0].dir == "desc" ? "-" : "") + options.sort[0].field;
}
if (options.filter && options.filter.filters.length > 0) {
for (var i = 0; i < options.filter.filters.length; i++) {
var f = options.filter.filters[i];
console.log(f);
options[f.field + f.operator] = f.value;
}
}
}
Now, the grid will send the operations in a Autoquery friendly manner.
I created an AutoQueryDataSource ts class that you may or may not find useful.
It's usage is along the lines of:
this.gridDataSource = AutoQueryKendoDataSource.getDefaultInstance<dtos.QueryDbSubclass, dtos.ListDefinition>('/api/autoQueryRoute', { orderByDesc: 'createdOn' });
export default class AutoQueryKendoDataSource<queryT extends dtos.QueryDb_1<T>, T> extends kendo.data.DataSource {
private constructor(options: kendo.data.DataSourceOptions = {}, public route?: string, public request?: queryT) {
super(options)
}
defer: ng.IDeferred<any>;
static exportToExcel(columns: kendo.ui.GridColumn[], dataSource: kendo.data.DataSource, filename: string) {
let rows = [{ cells: columns.map(d => { return { value: d.field }; }) }];
dataSource.fetch(function () {
var data = this.data();
for (var i = 0; i < data.length; i++) {
//push single row for every record
rows.push({
cells: _.map(columns, d => { return { value: data[i][d.field] } })
})
}
var workbook = new kendo.ooxml.Workbook({
sheets: [
{
columns: _.map(columns, d => { return { autoWidth: true } }),
// Title of the sheet
title: filename,
// Rows of the sheet
rows: rows
}
]
});
//save the file as Excel file with extension xlsx
kendo.saveAs({ dataURI: workbook.toDataURL(), fileName: filename });
})
}
static getDefaultInstance<queryT extends dtos.QueryDb_1<T>, T>(route: string, request: queryT, $q?: ng.IQService, model?: any) {
let sortInfo: {
orderBy?: string,
orderByDesc?: string,
skip?: number
} = {
};
let opts = {
transport: {
read: {
url: route,
dataType: 'json',
data: request
},
parameterMap: (data, type) => {
if (type == 'read') {
if (data.sort) {
data.sort.forEach((s: any) => {
if (s.field.indexOf('.') > -1) {
var arr = _.split(s.field, '.')
s.field = arr[arr.length - 1];
}
})
}//for autoquery to work, need only field names not entity names.
sortInfo = {
orderByDesc: _.join(_.map(_.filter(data.sort, (s: any) => s.dir == 'desc'), 'field'), ','),
orderBy: _.join(_.map(_.filter(data.sort, (s: any) => s.dir == 'asc'), 'field'), ','),
skip: 0
}
if (data.page)
sortInfo.skip = (data.page - 1) * data.pageSize,
_.extend(data, request);
//override sorting if done via grid
if (sortInfo.orderByDesc) {
(<any>data).orderByDesc = sortInfo.orderByDesc;
(<any>data).orderBy = null;
}
if (sortInfo.orderBy) {
(<any>data).orderBy = sortInfo.orderBy;
(<any>data).orderByDesc = null;
}
(<any>data).skip = sortInfo.skip;
return data;
}
return data;
},
},
requestStart: (e: kendo.data.DataSourceRequestStartEvent) => {
let ds = <AutoQueryKendoDataSource<queryT, T>>e.sender;
if ($q)
ds.defer = $q.defer();
},
requestEnd: (e: kendo.data.DataSourceRequestEndEvent) => {
new DatesToStringsService().convert(e.response);
let ds = <AutoQueryKendoDataSource<queryT, T>>e.sender;
if (ds.defer)
ds.defer.resolve();
},
schema: {
data: (response: dtos.QueryResponse<T>) => {
return response.results;
},
type: 'json',
total: 'total',
model: model
},
pageSize: request.take || 40,
page: 1,
serverPaging: true,
serverSorting: true
}
let ds = new AutoQueryKendoDataSource<queryT, T>(opts, route, request);
return ds;
}
}

KnockoutJS : how to set the initial value from a dropdown list when data is retrieved asynchronously using JSON?

Issue is related to this and this.
The problem is that when a options list is not yet populated with valid data (because the JSON call return is asynchronously), you cannot set the initial selected value.
function PersonViewModel() {
// Data members
this.Function_Id = ko.observable('#(Model.Function_Id)');
this.Functions = ko.observableArray([{ Id: '#(Model.Function_Id)', Name: ''}]); // This works
//this.Functions = ko.observableArray(); // This does not work
this.SubFunctions = ko.observableArray();
this.GetFunctions = function () {
var vm = this;
$.getJSON(
'#Url.Action("GetFunctions", "Function")',
function (data) {
vm.Functions(data);
if (vm.Function_Id() === undefined) {
//vm.Function_Id('#(Model.Function_Id)'); // Only way to solve my problem?
}
}
);
};
}
$(document).ready(function () {
var personViewModel = new PersonViewModel();
ko.applyBindings(personViewModel);
personViewModel.GetFunctions();
});
See this modified fiddle
function Item(id, name) {
this.id = ko.observable(id);
this.name = ko.observable(name);
}
var viewModel = {
selectedItem: ko.observable(),
//items: ko.observableArray([new Item(3, "")])
items: ko.observableArray()
};
ko.applyBindings(viewModel);
var selectedIndex = 3;
setTimeout(function() {
viewModel.items([
new Item(1, "pencil"),
new Item(2, "pen"),
new Item(3, "marker"),
new Item(4, "crayon")
]);
var selectedValue;
$.each(viewModel.items(), function(index, item) {
if (index === selectedIndex) {
selectedValue = item;
}
});
viewModel.selectedItem(selectedValue);
}, 1000);
h2 { font-size: 1.1em; font-weight: bold; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<select data-bind="options: items, optionsText: 'name', value: selectedItem"><option selected="selected"></option></select>
<div data-bind="with: selectedItem">
<span data-bind="text: name"></span>
</div>
Thanks to 'RP Niemeyer' and 'Sandeep G B', I've created this solution.
Maybe it's also an idea to add an 'initialValue' property to the data-bind functionality for a the data-bind attribute from a select like this:
<select id="Function_Id"
data-bind='
options: Functions,
optionsValue : "Id",
optionsText: "Name",
initialValue = 1,
value: Function_Id,
optionsCaption: Functions().length == 0 ? "Loading..." : "--NONE--"'>
</select>