How to use computed method inside foreach in knockout.js - javascript

I'm playing with knockout a little bit, and I'm stuck with something. I did the example that you create the first name, the last name and then create a ko.computed to make the fullName. That works ok, but let's say I have an observable array with a lot of objects containing first names and last names. How to use the computed function to create the full name? If i create something like:
function vm() {
....
self.fullName = ko.computed(function() {
return self.names().firstName + "" + self.names().lastName;
}
I can't use it because this is a viewmodel method, and inside a foreach binding knockout will look for local methods (in this case, methods of self.names())
Also can't use $root.fullName because then knockout will not retrieve the correct value.
Fiddle: http://jsfiddle.net/mtfv6q6a/

you can call it by assiging a variable to your vm sth. like :
appModel = new vm();
ko.applyBindings(appModel);
and
<h3 data-bind="text: appModel.fullName()"></h3>
this works, but would always return undefinedundefined http://jsfiddle.net/mtfv6q6a/1/ because firstName is not a property of names()
you rather need some simple function like:
self.returnFullName = function(item) {
return item.firstName + " " + item.lastName;
};
and call it like
<h3 data-bind='text: appModel.returnFullName($data); '></h3>
http://jsfiddle.net/mtfv6q6a/2/

Related

KnockoutJs filter empties the list after search

I am using KnockoutJs to build a search list, the code as follows:
HTML:
<input type="search" id="search-bar" placeholder="Enter a name" data-bind="value:query,valueUpdate: 'keyup'">
<div id="list" data-bind='template: {foreach: name}'>
<li data-bind='text $data'></li>
</div>
Part of Js search function:
this.name = ko.observable('');
this.query = ko.observable('');
this.search = function (value) {
self.name([]);
for (var x in name) {
if (name[x].toLowerCase().indexOf(value.toLowerCase()) >= 0) {
self.name.push(name[x]);
}
}
}
this.query.subscribe(self.search);
The data (2-D list) is retrieved from a different URL using $.get and then parsed, I then assigned every first entry (arsed_data[i][0]) of the parsed data as name observable shown in the code.
The list meant to filter the content according to the searchbox entry, and it displays all the names initially, but as soon as I type something in the searchbox, the name list becomes empty, what could be the possible cause and is there a way to fix it?
If you want to use array methods like push and such directly on the observable, you need to make it an observable array, not just an observable. E.g.:
this.name = ko.observableArray();
not
this.name = ko.observable('');
Even though you have self.name([]) later, the observable is still just an observable (whose value is an array), not an observable array.
(I'm assuming you have var self = this; somewhere above the code you've shown.)
Also, since it's an array of names, you might want to call it names rather than name.
Side note: If the name in for (var x in name) { is an array, that's not how you should loop through arrays. See this question's answers for various correct ways to loop through arrays.
Side note 2: The template binding in your HTML seems suspect. You're providing the HTML right there, not in a separate template. So it should just be data-bind="foreach: name".
I would suggest introducing computed observable (say self.filteredNames) that would be dependent on both self.names array (it should be ko.observableArray indeed) and current value of self.query
You can then bind your template to self.filteredNames instead of self.names
A fragment of View Model definition could look like this:
self = this;
/* ... any other code related to VM */
self.names = ko.observableArray([]); // it's supposed to be later filled with AJAX requests
// self.names = ko.observableArray(['foo', 'bar', 'baz']); // however you can try with this not to bother with actual data loading
self.query = ko.observable('');
self.filteredNames = ko.pureComputed(function(){
// value of this pureComputed observable would be automatically updated each time either self.query or self.names is updated
return self.names().filter(function(item) {
return item.toLowerCase().indexOf(self.query().toLowerCase()) >= 0;
});
});
/* ... any other code related to VM */
A version of markup that allows to test how it works could look like this:
<input type="search" id="search-bar" placeholder="Enter a name" data-bind="value:query,valueUpdate: 'keyup'">
<ul id = "list" data-bind='foreach: filteredNames'>
<li data-bind=' text: $data'></li>
</div>

Vue.js: How can I update a single item in a "list" - computed property?

I'm trying to determine the best pattern to solve the following:
I want to show a list of people in a specific department
I made the department index a regular, reactive Vue property
I made the list of people in the department a computed property
Now:
My backend (a Mac app) can dispatch a "Person at index changed" event and I must reload the name of a single person.
But: the person property is an item in a computed property (i.e. "people", see code below).
How can I update the name of a person in the list list of people, which in turn is a computed property (although it is "computed" from the departmentIndex + backend call)?
I assume that I have a mistake in my original setup. Maybe the people list should not be a computed property in the first place?
Here is the code:
function pretendUpdateEventFromBackend() {
var event = new CustomEvent('PersonUpdated', {detail:{index:1, name:'Jimmy'}});
document.dispatchEvent(event);
}
var backend = {
// The actual backend is a Mac app with an embedded WebView...
listPeopleInDepartment(departmentIndex) {
return [
{name:'John'},
{name:'James'},
{name:'Jane'}
];
}
}
var app = new Vue({
el: '#app',
data: {
message:'',
departmentIndex:0,
},
computed: {
people() {
return backend.listPeopleInDepartment(this.departmentIndex);
}
},
created() {
const me = this;
document.addEventListener('PersonUpdated', function(e){
me.message += 'Updated ';
var personIndex = e.detail.index;
var newName = e.detail.name;
// How can I update person in the list of computed people here?
// Or how can I force a reload of the people list?
me.message += 'Person at: ' + personIndex + ' new name: ' + newName + "\n";
});
},
});
Html:
<button onclick="pretendUpdateEventFromBackend()">Trigger Update Event from Backend</button>
<div id="app">
<div class="person" v-for="person in people">
Name: {{ person.name }}
</div>
<pre>{{message}}</pre>
</div>
EDIT:
Try it on jsbin: http://jsbin.com/guwezoyena/1/edit?html,js,output
I just thought I should give you some further insight on your setup:
If listPeopleInDepartment is an ajax call in your actual code, this might be a bad pattern. See, Vue will identify every property (including other computed properties) that is used within a computed property and make it sure to re-compute it whenever any of them changes. In your case, this would mean whenever departmentIndex changes it'll recompute people.
Here's why it might be problematic, since you must return the result of the ajax request, it can't be asynchronous so it'll be a blocking call that would render the page unresponsive while it runs. Also, let's say you were viewing department 1, then you change to 2, if you go back to 1 it'll have to reload the people in department 1 because it's not stored anywhere.
You should probably implement some caching strategy and have ajax loads only when that data isn't already available, also in a non blocking fashion. You could achieve this with an array that stores the arrays of peopleInDepartment indexed by department id, and a watcher for departmentIndex that would do something like this:
function (newValue){
if (!this.peopleCache[newValue]){
var that = this;
//Load it via ajax
loadPeopleInDepartment(newValue, function(result){
that.peopleCache[newValue] = result;
});
}
}

Angular2 bind ngModel from ngFor

So I'm rendering my textarea dynamically using ngFor however I'm not sure how I can pass the ngModel to bind it in my function.
<div *ngFor="let inputSearch of searchBoxCount; let i = index" [ngClass]="{'col-sm-3': swaggerParamLength=='3', 'col-sm-9': swaggerParamLength=='1'}">
<textarea name="{{inputSearch.name}}" id="{{inputSearch.name}}" rows="3" class="search-area-txt" attr.placeholder="Search Product {{inputSearch.name}}"
[(ngModel)]="inputSearch.name"></textarea>
</div>
textarea example:
textarea is render based on the length of the response I get from api call in my case searchBoxCount is basically searchBoxCount.length, so if it length is = 1 then it will only render 1 textarea if its 3 then it will show 3 textareas. The objs have different names (example: id/email/whatever), so ngModel is based on the obj name from the json object.
How do I bind inputSearch.name to my function getQueryString()
getQueryString() {
this.isLoading = true;
let idInputValue = inputSearch.name; //bind it here
return "?id=" + idInputValue
.split("\n") // Search values are separated by newline and put it in array collection.
.filter(function(str) {
return str !== ""
})
.join("&id=");
}
Search func where getQueryString() is called
searchProduct() {
let queryString1 = this.getQueryString();
this._searchService.getProduct(queryString1)
.subscribe(data => {
console.log(data);
});
}
I know how to do it if the ngModel is not coming from the ngFor, is there another way to get the value from the textarea without ngModel? maybe that's the only way or if I can still use ngModel.
Summary of current state
First, let me summarize where your data is. You have a list of one or more objects named searchBoxCount. Each of the elements in the list is an object which has a name property, so you could, for example, call let name = this.searchBoxCount[0].name; to get the name of the first object in the list.
In the HTML template you use ngFor to loop through all of the objects in the searchBoxCount list, and in each iteration you assign the object to a local (to the ngFor) variable named inputSearch. You then bind the input from the textarea created in each loop iteration to the name property for that iteration's inputSearch object.
How to get your data
The key here is that the inputSearch is the same Object as is stored in searchBoxCount at some particular index (index 0 for the first object, etc...). So when the ngModel is tied to inputSearch.name it is also bout to searchBoxCount[n].name. External to the ngFor, you would loop through the searchBoxCount list to get each name you need.
As a consequence
Based on the comments on the original post, it sounds like you can have one or
more names that you need to include in the query string output. That means for your getQueryString() to work, you have to loop through the list (or as in this case, let the list loop for us):
getQueryString() {
this.isLoading = true;
let result : string = "?id=";
this.searchBoxCount.forEach(
(inputSearch:any) => { //Not the same variable, but same objects as in the ngFor
result = result + inputSearch.name + "&id=";
});
result = result.slice(0, result.length - 4); //trim off the last &id=
return result;
}
Edit: Multiple different fields with different names
From the comments on this post, it now is clear each inputSearch has its own key to be used in the query string, that is stored in the name property. You need to preserve that name, which means you can't bind the ngModel to it. Otherwise the user will destroy the name by typing in their own text and there will be no way to get the correct key back. To that end, you need to store bind the ngModel to some other property of the inputSearch object. I am going to assume the object has a value property, so it looks like this:
{
name: "id",
value: "33\n44"
}
That is, each inputSearch has a name, and the value will have one or more values, separated by new line. You would then have to change the HTML template to this:
<div *ngFor="let inputSearch of searchBoxCount; let i = index"
[ngClass]="{'col-sm-3': swaggerParamLength=='3', 'col-sm-9':
swaggerParamLength=='1'}">
<textarea name="{{inputSearch.name}}"
id="{{inputSearch.name}}" rows="3" class="search-area-txt"
attr.placeholder="Search Product {{inputSearch.name}}"
[(ngModel)]="inputSearch.value"></textarea>
</div>
Notice that I changed the ngModel from inputSearch.name to inputSearch?.value (the ? allows for null if there is no value to begin with) inputSearch.value. The getQueryString() method then looks something like this:
getQueryString() {
let result:string = "?";
//for each of the input search terms...
this.searchBoxCount.forEach( (inputSearch:any) => {
// first reparse the input values to individual key value pairs
let inputValues:string = inputSearch.value.split("\n")
.filter(function(str) { return str !== "" })
.join("&" + inputSearch.name + "=");
// then add it to the overall query string for all searches
result = result +
inputSearch.name +
"=" +
inputValues +
"&"
});
// remove trailing '&'
result = result.slice(0, result.length - 1);
return result;
}
Note, using RxJs this is probably easier but I am testing vanilla javascript.
Using this, if the user entered two IDs (33 and 44), a single sku, and two emails, the result would be ?id=33&id=24&sku=abc123&email=name#compa.ny&email=an.other#compa.ny

Custom filter on 'ng-repeat' does overwrite the scope

My aim is to replace the teacher-id(f_teacher) of one outputted array with the teacher name of another array. I wrote a custom filter, that should do the job:
angular.module('core')
.filter('replaceId', function () { //filter, which replaces Id's of one array, with corresponding content of another array
return function (t_D, s_D, t_prop, s_prop) { //data of target, data of source, target property, source property
var replacment = {};
var output = [];
angular.forEach(s_D, function (item) {
replacment[item.id] = item[s_prop]; //replacment - object is filled with 'id' as key and corresponding value
});
angular.forEach(t_D, function (item) {
item[t_prop] = replacment[item[t_prop]]; //ids of target data are replaced with matching value
output.push(item);
});
return output;
}
});
I use a 'ng-repeat' like this:
<tr ng-repeat="class in $ctrl.classes | filter:$ctrl.search | replaceId:$ctrl.teachers:'f_teacher':'prename' | orderBy:sortType:sortReverse">
<td>{{class.level}}</td>
<td>{{class.classNR}}</td>
<td>{{class.f_teacher}}</td>
</tr>
But it only outputs an empty column. Now the strange thing: If I follow the steps with the debugger, it works for the first time the filter is performed. But when it is performed a second time it outputs an empty column.
I noticed that the returned object of the filter overwrites the $ctrl.classes - array, but normally this shouldn't be the case?
Here is a plnkr:
https://plnkr.co/edit/EiW59gbcLI5XmHCS6dIs?p=preview
Why is this happening?
Thank you for your time :)
The first time through your filter the code takes the f_teacher id and replaces it with the teacher name. The second time through it tries to do the same thing except now instead of getting a teachers ID in f_teacher it finds the teacher's name so it doesn't work. You could fix it by making a copy of the classes instead of modifying them directly. e.g.
angular.forEach(t_D, function (item) {
var itemCopy = angular.copy(item);
itemCopy[t_prop] = replacment[itemCopy[t_prop]];
output.push(itemCopy);
});
https://plnkr.co/edit/RDvBGITSAis3da6sWnyi?p=preview
EDIT
Original solution will trigger an infinite digest because the filter returns new instances of objects every time it runs which will cause angular to think something has changed and retrigger a digest. Could you just have a getter function that gets a teachers name instead of using a filter?
$scope.getTeacherName = function(id) {
var matchingTeachers = $scope.teachers.filter(function(teacher) {
return teacher.id == id;
})
//Should always be exactly 1 match.
return matchingTeachers[0].prename;
};
And then in the HTML you could use it like
<tr ng-repeat="class in classes">
<td>{{class.level}}</td>
<td>{{class.classNR}}</td>
<td>{{getTeacherName(class.f_teacher)}}</td>
</tr>
https://plnkr.co/edit/gtu03gQHlRIMsh9vxr1c?p=preview

Make koGrid respect observable properties

I've been writing an app with the kogrid, recently I changed my datasource from an array of objects to an array of knockout objects. However, to my surprise when I update the observable properties within my objects the grid is not updated.
Here is my data array:
self.gridData = ko.observableArray([
{ name: ko.observable("joe"), age: ko.observable(5) }
]);
when I update the age property nothing happens on the grid:
self.gridData()[0].age(6);
does anyone have a good answer for why this is?
Update
I've answered the question below, but does anyone know why the kogrid would be caching the unwrapped values?
I looked into the kogrid source and found this line in src/classes/row.js
self.getProperty = function (path) {
return self.propertyCache[path] || (self.propertyCache[path] = window.kg.utils.evalProperty(self.entity, path));
};
it looks like the property cache is caching the unwrapped value of the property we're accessing in the default cell template:
<div data-bind="attr: { 'class': 'kgCellText colt' + $index()}, html: $data.getProperty($parent)"></div>
(Note: $data in the template is the column, which has a getProperty wrapper for row.getProperty)
I simply removed the line to cache property values like this:
self.getProperty = function (path) {
return window.kg.utils.evalProperty(self.entity, path);
};

Categories