How to update observable array element in knockoutjs? - javascript

I have following JavaScript array,
[{"unitPrice": 2499,"currency":"$","productId":1,"retailerId":1,"productName":"XX ","formattedPrice":"$ 2,499","productImage":"Images/2012_08_12_00_45_39_4539.jpg","productQuantity":"9","totalPrice":19992},
{"unitPrice": 4999,"currency":"$","productId":2,"retailerId":1,"productName":"XX","formattedPrice":"$ 4,999","productImage":"Images/2012_08_12_00_46_45_4645.jpg","productQuantity":2,"totalPrice":9998},
{"unitPrice":4555,"currency":"$","productId":1,"retailerId":1,"productName":"XXXXX","formattedPrice":"$ 4,555","productImage":"Images/2013_02_12_10_57_49_5749_9868.png","productQuantity":3,"totalPrice":13665}]
here is the relevent html,
<table>
<tbody data-bind="foreach: $root">
<tr>
<td><img width="45" height="45" alt="" data-bind="attr:{src: productImage}"/></td>
<td><span data-bind="html: productName"></span></td>
<td><span data-bind="html: formattedPrice"></span></td>
<td><input type="number" class="quantity" data-bind="value: productQuantity, attr:{'data-id': productId }" /></td>
<td><span data-bind="html: totalPrice"></span></td>
</tr>
</tbody>
</table>
Then I have created observable array as,
observableItems = ko.observableArray(items);
ko.applyBindings(observableItems);
Now I was able to get an specfic element using,
var obj = ko.utils.arrayFirst(list(), function (item) {
return item.productId === id;
});
However when I change,
item.productQuantity = 20;
But UI is not updating. Tried also,
item.productQuantity(item.productQuantity)
But getting error productQuantity is not a function

The above behavior is because only the array is an observable and not the individual elements within the array or the properties of each element.
When you do item.productQuantity = 20;, this will update the property but since it is not an observable, the UI does not get updated.
Similary, item.productQuantity(20) gives you an error because productQuantity is not an observable.
You should look at defining the object structure for each element of your array and then add elements of that type to your observable array. Once this is done, you will be able to do something like item.productQuantity(20) and the UI will update itself immediately.
EDIT Added the function provided by the OP :). This function will convert each property of the elements in an array to observables.
function convertToObservable(list)
{
var newList = [];
$.each(list, function (i, obj) {
var newObj = {};
Object.keys(obj).forEach(function (key) {
newObj[key] = ko.observable(obj[key]);
});
newList.push(newObj);
});
return newList;
}
END EDIT
If you are unable to change that piece of code, you can also do something like observableItems.valueHasMutated(). However, this is not a good thing to do as it signals to KO and your UI that the whole array has changed and the UI will render the whole array based on the bindings.

You can easily use ko.mapping plugin to convert object to become observable: http://knockoutjs.com/documentation/plugins-mapping.html
Convert regular JS entity to observable entity:
var viewModel = ko.mapping.fromJS(data);
Convert observable object to regular JS object:
var unmapped = ko.mapping.toJS(viewModel);

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>

How to loop over a complex JSON in return in Reactjs?

i am learning Reactjs while building a simple application. I know this is very basic and there are alot of answers for the same question available on stackoverflow. But i am unable to implement those maybe because of variable scopes. I would be grateful if someone is able to help me out with this.
My full code is pretty complex and irrelevant to the question. But please refer to this fiddle : Full Code. This will work in local server.
Relevant Code:
var localStore = {};
// localStore = {"f":{"symbol":"f","quantity":10,"pricePaid":136},"jnj":{"symbol":"jnj","quantity":30,"pricePaid":146}};
var SearchStock = React.createClass({
...
render: function() {
...
return ( // I tried map function in other ways too like [localstore].map and then get its values in similar way as i am getting below.
{/*localStore.map(function (l, i) {
return <tr>
<td>{Object.keys(l)[i].symbol}</td>
<td>{Object.keys(l)[i].quantity}</td>
<td>{Object.keys(l)[i].pricePaid}</td>
<td>{Object.keys(l)[i].symbol}</td>
</tr>
})*/}
{/*for(var x in localStorage){ // This is what i really want to do.
return <tr>
<td>{x.symbol}</td>
<td>{x.quantity}</td>
<td>{x.pricePaid}</td>
<td>anything</td>
</tr>
}*/}
{stocks.map(function(l) { // this works.
console.log("in return stocks",JSON.stringify(stocks));
return <tr>
<td>{l[Object.keys(l)[0]]["name"]}</td>
<td>{this.state.quantity}</td>
<td>{this.state.pricePaid}</td>
<td>{<input type="button" value="View Stock"/>}</td>
</tr>
}, this)}
I don't know why map function didn't work as value in localStore and stocks is similar: stocks = [{only one key pair of localStore}]. I would appreciate any help or guidance i can get for this.
You should use Object.keys to get an array with the keys of localStore and iterate through it to display your data.
{Object.keys(localStore).map(function (key) {
return <tr key={key}>
<td>{localStore[key].symbol}</td>
<td>{localStore[key].quantity}</td>
<td>{localStore[key].pricePaid}</td>
<td>{localStore[key].symbol}</td>
</tr>
})}
You were missing other things like
1º Provide a key to every child in the array.
2º Wrap your <tr> tags inside a <tbody>. <tr> cannot appear as a child of <table>
jsfiddle

Ember Handlebars nested each not working

The following Ember Handlebars template renders the 1st row, but does not render the one inside the nested each (or inner each)
<table width="50%">
{{#each someData.items as |item|}}
<tr> <!-- This one RENDERS -->
<td width="25%"><span class="boldTxt">{{item.fld1}}</span></td>
<td width="25%">{{item.fld2}}</td>
</tr>
{{#each item.Reas as |rea|}}
<tr> <!-- This one does not RENDER -->
<td>{{rea}}</td>
</tr>
{{/each}}
{{/each}}
</table>
What is the issue??
I am using Ember version 1.13
Most likely, your problem is that you are using Ember2.0 or above (based on your outer each loop) so your inner each loop has a now invalid (formerly deprecated) format. Also, you are using the same variable name item for both loops, which won't work properly.
http://guides.emberjs.com/v2.1.0/templates/displaying-a-list-of-items/
Just use the same format as in the outer loop:
Change:
{{#each item in item.Reasons}}
To:
{{#each item.Reasons as |reason|}}
EDIT
If your Reas arrays look as you've described in the comments:
item.Reas = [null]; // arrays containing a single `null` value
Then handlebars will show an empty string for these values since Handlebars coerces undefined and null to an empty string.
{{reas}} {{!-- if reas is null then an empty string is printed --}
If you want to show null and undefined values, you can make a simple helper to do so:
// helpers/show-value.js
import Ember from "ember";
export default Ember.Helper.helper(function(params) {
let value = params[0];
if(value === undefined) { return 'undefined'; }
if(value === null) { return 'null'; }
return value;
});
EDIT 2
Based on your explanation in the comment:
Since you are using Ember 1.13, you need a work around to achieve this. Here is one way:
// components/each-keys.js
export default Ember.Component.extend({
object: null, // passed in object
items: Ember.computed('object', function() {
var object = Ember.get(this, 'object');
var keys = Ember.keys(object);
return keys.map(function(key) {
return { key: key, value: object[key]};
})
})
})
Usage:
{{#each-keys object=item.Reas as |key value|}}
<tr>
<td>{{key}}</td>
<td>{{value}}</td>
</tr>
{{/each-keys}}
Here is a running example
If you update to Ember 2.0, which should be pretty straightforward from 1.13 (since 2.0 is basically 1.13 without deprecations) you can use the each-in helper to iterate over an object and get access to both its keys and values. Here is a simple example:
{{#each-in items as |key value|}}
<p>{{key}}: {{value}}</p>
{{/each-in}}

How can I replace an object in array that is displayed using ng-repeat?

I have an array of items that is displayed in a table using ng-repeat. When you click on an item, that item is pulled from the server and the table should then be updated with the updated item.
Function to get the updated item when clicking on an item in the table:
$scope.getUpdatedItem = function(item){
itemService.getItem(item).then(
function(updatedItem){
item = updatedItem;
},
function(error){
//Handle error
}
);
};
I'm displaying the items using:
<tr ng-repeat="item in myItems">
The problem: The item in the table is never updated.
What's the best way to update the item in the ng-repeat? Can i use "track by $index" in the ng-repeat for this? Or do I have to iterate over myItems to find the item I want to replace?
Update:
A possible solution is instead of using
item = updatedItem,
to use:
var index = $scope.myItems.indexOf(item);
$scope.myItems[index] = updateItem;
However, I feel that there should be a "cleaner" way of doing this.
There isn't a much cleaner way (then your update).
As you noticed, when you change item in your callback function you change the local reference, and not the original item in the array.
You can improve this a bit by using the $index from the ng-repeat, instead of calculating it yourself:
<div ng-click="getUpdatedItem(item, $index)"> </div>
And in your controller:
$scope.getUpdatedItem = function(item, index){
itemService.getItem(item).then(
function(updatedItem){
$scope.myItems[index] = updateItem;
},
function(error){
//Handle error
}
);
};
You can also use angular.copy instead but it's much less efficient:
function(updatedItem){
angular.copy(updateItem, item);
},
If I understand your problem properly
could something like this work?
<!-- template code -->
<table>
...
<tr ng-repeat="(index, item) in items">
<td>{{item.name}}</td>
<td>
{{item.detail}}
<button ng-if="!item.detail" ng-click="loadItem(index)">
</td>
</tr>
</table>
// Controller Code
$scope.items = [...]
$scope.loadItem = function(index){
itemService.getItemDetail($scope.items[index]).then(function(itemDetail){
$scope.items[index].detail = itemDetail;
});
};
item may start as a reference to an item in your list, but when you say:
item = updatedItem;
You reseat that binding -- you are no longer referring to the item in the list, but to the disconnected one that was returned in your promise. Either you will need to modify the item, like so:
function(updatedItem){
item.varA = updatedItem.varA
item.varB = updatedItem.varB
...
}
Or, if it gets too hairy, you might consider an item array that looks more like this:
var items = [
{ data: item1 },
{ data: item2 },
{ data: item3 }
};
At which point your update function will look like this:
function(updatedItem){
item.data = updatedItem;
},
I've just spent hours on this issue. I couldn't use the $index solution from #eladcon, as my ng-repeat also used a filter, to the index isn't correct if the rows/items are filtered.
I thought I would be able to just do this:
$filter('filter')($scope.rows, {id: 1})[0] = newItem;
but that doesn't work.
I ended up iterating the array until I found a match, and then using the $index from the iteration (not from the ng-repeat) to set the array item to the new item.
// i'm looking to replace/update where id = 1
angular.forEach($scope.rows, function(row, $index) {
if (row.id === 1) {
$scope.rows[$index] = newItem;
}
})
See here:
https://codepen.io/anon/pen/NpVwoq?editors=0011

Knockout compute new values on filter

I'm building a very number-heavy app and one feature of the app is to be able to filter between the three results at the end. These are: Drugs, Disease & All (both).
I'm pretty new to Knockout so not totally sure what the best-practise way to approach the following situation would be.
I have the values filtering, my problem lies with computing the new values. The totals at the bottom have to reflect what data is currently visible in the table. Although these cell values (apart from totals) in the codepen below are static, they're actually dynamic in my app and they're a computed combination of a couple of values that are stored in sessionStorage in the app on previous pages. (This screen is a totals page).
Example:
self.total = ko.computed(function () {
return sessionStorage.getItem('1') * sessionStorage.getItem('2')
});
So to clarify; initially the computed totals at the bottom of the table are a total sum of numbers in the table cells that are multiplications of numbers from sessionStorage. The totals values then need to update every time the table is filtered to reflect the total of the visible data.
http://codepen.io/anon/pen/nlemE
I appreciate this may be complicated to get your head around and if I haven't explained it well enough, let me know and I'll clarify.
Rather than use jQuery to hide/show the rows, it would be better to use an observableArray to store your values in, and a computed function to filter that array every time the filter value changes.
Here's an updated CodePen showing how that might work: http://codepen.io/anon/pen/HAzcf?editors=101
Your data code would be an array of objects instead of r1c1, r2c1, etc. We also add a filter field so we don't have to hard code that into our HTML.
self.values = ko.observableArray([
{
c1: ko.observable(10),
c2: ko.observable(32),
c3: ko.observable(36),
filter: ko.observable('drugs')
},
{
c1: ko.observable(70),
c2: ko.observable(46),
c3: ko.observable(31),
filter: ko.observable('disease')
}
// etc..
]);
Our toggle function now simply updates an observable:
// toggle
self.filter = ko.observable('all');
self.toggleVis = function (data, event) {
self.filter(event.target.value);
};
The filtered values are updated every time the self.filter observable is updated:
self.filteredValues = ko.computed(function() {
var filter = self.filter();
return ko.utils.arrayFilter(self.values(), function(item) {
return filter == 'all' || item.filter() == filter;
});
});
And our totals properties are updated whenever the filteredValues are changed:
self.total1 = ko.computed(function () {
var total = 0,
values = self.filteredValues();
for(var i = 0; i < values.length; i++) {
total += values[i].c1();
}
return total;
});
Your HTML table code also becomes simplified to simply iterate over the filtered observable array:
<table>
<tbody>
<!-- ko foreach: filteredValues -->
<tr class="row">
<td data-bind="text:c1"></td>
<td data-bind="text:c2"></td>
<td data-bind="text:c3"></td>
</tr>
<!-- /ko -->
<tr class="totals">
<td data-bind="text:total1"></td>
<td data-bind="text:total2"></td>
<td data-bind="text:total3"></td>
</tr>
</tbody>
</table>
You want to use a Knockout Subscription on each of your observables.
self.r1c1().subscribe(function(newValue) {
// update the sum
});
Rather than add 20 subscriptions however, you might want to use a Knockout Observable Array to store/update your individual values.
var myObservableArray = ko.observableArray();
myObservableArray.push(<value for row 1>);
myObservableArray.push(<value for row 2>);
// etc
Then you could get by with a single subscription for each of your three columns.
self.myObservableArray.subscribe(function(newValue) {
// re-calculate the sum of the values
});

Categories