Decrement a value after clicking a button in the same row - knockout - javascript

I have a table which I fill with some numbers. There is a button in each row. After clicking this button I would like to decrement a counter in this row. How to to this with knockout?
<div class="panel panel-default">
<div class=panel-heading>Title</div>
<table class=table>
<thead>
<tr>
<th>Counter</th>
<th>Increment</th>
</tr>
</thead>
<tbody data-bind="foreach: records">
<tr>
<td data-bind="text: counter"></td>
<td> <input type="button" value="increment" data-bind=??? ></td>
</tr>
</tbody>
</table>
</div>
<script>
function AppViewModel() {
var self = this;
self.records = ko.observableArray([]);
$.getJSON("/data", function(data) {
self.records(data);
})
//function to decrement
}
ko.applyBindings(new AppViewModel());
</script>

I would do it this way:
Process data you get from server, turn counter property into observable and add function to decrement counter property
Restructure you code a little so viewmodel will be created by the time of ajax request
Move applyBindings call to ajax callback so it would fire when everything has been loaded
So the code would look like:
<tr>
<td data-bind="text: counter"></td>
<td> <input type="button" value="decrement" data-bind="click: decrement"></td>
</tr>
function AppViewModel() {
var self = this;
self.records = ko.observableArray([]);
}
var vm = new AppViewModel();
// load data from server
$.getJSON("/data", function(data) {
data.forEach( function(item) {
// make counter observable
item.counter = ko.observable(item.counter);
// add function to decrement
item.decrement = function() {
this.counter( this.counter()-1 );
}
})
// load array into viewmodel
vm.records(data);
// apply bindings when all obervables have been declared
ko.applyBindings(vm);
})
Check demo: Fiddle

I prefer to initialize and bind my viewmodel right away, but agree with the other poster that you need an observable.
Here is a solution that continues to create and bind your viewmodel right away, as in your original example, but instead of an array of the raw records you receive back it converts them into their own little model objects that have an observable for the counter and an increment function that can be data bound too. This decouples your data load from the life of the viewmodel, so if you wanted to add a button to load fresh data to overwrite it or anything like that, it's just another call to getData().
<!-- ... -->
<tbody data-bind="foreach: records">
<tr>
<td data-bind="text: counter"></td>
<td> <input type="button" value="increment" data-bind="click: increment" ></td>
</tr>
</tbody>
<!-- ... -->
<script>
function AppViewModel() {
var self = this;
self.records = ko.observableArray([]);
self.getData = function(){ /* ... */ };
self.getFakeData = function(){
var data = [{ counter: 1 }, { counter: 2}, { counter: 3 }];
var freshData = data.map(function(record){
return new AppRecord(record);
});
self.records(freshData);
};
}
function AppRecord(rawRecord) {
var self = this;
self.counter = ko.observable(rawRecord.counter);
self.increment = function(){
self.counter(self.counter() + 1);
};
}
var vm = new AppViewModel();
vm.getFakeData(); // replace with getData()
ko.applyBindings(vm);
</script>
Fiddle, with a getFakeData with sample data: https://jsfiddle.net/4hxyarLa/1/
If you are going to have a lot of rows and are concerned abut memory, you could put the increment function in a prototype method for the AppRecord and access the record via a parameter on the function, or you could add the function to the AppViewModel and bind to $parent.increment to call it and access the record via parameter passed to that function to increment it's counter property.

Related

Remove items from Knockout observable array

I have the below structure for knockout model. It contains an observable array which in turn contains an object.
function ViewModel() {
var self = this;
self.newItem = ko.observable({
manufacturer: ko.observable(),
itemnumber: ko.observable(),
itemDescription: ko.observable()
});
self.AllItems = ko.observableArray();
self.addItem = function() {
self.newItem().manufacturer("test");
self.newItem().itemDescription("data");
self.AllItems.push(self.newItem);
};
self.removeItem = function(data) {
self.AllItems.remove(data);
};
}
First issue:Through this script I am entering a new itemnumber in the textbox and then clicking on add item to have the new item with the itemnumber from the textbox added to the observable array but when I change the item number and hit add it changes all the itemnumber inside the array. How can i have unique data inside the array.
Second issue: I need to remove the specific items from the array but it's not deleting it. Can someone please tell me how I can delete items from the observable array based on say the itemnumber property.
<input type="text" data-bind="value: newItem().itemnumber"/>
<div>
Items: <button data-bind="click: addItem">Add Item</button>
</div>
<div>
<table>
<tbody data-bind="template: { name: 'itemTemplate', foreach: AllItems }"></tbody>
</table>
</div>
<script type="text/html" id="itemTemplate">
<tr>
<td>
<input data-bind="value: itemnumber" />
Remove Item
</td>
</tr>
</script>
I have created this fiddle for quick view of the issue. Just started learning knockout so any help is appreciated.
http://jsfiddle.net/N3JaW/138/
Try the following for adding new item, which will solve your first issue:-
HTML code
<input type="text" id="textBox" data-bind="value : textBoxVal"/>
<div>
Items: <button data-bind="click: addItem">Add Item</button>
</div>
<div>
<table>
<tbody data-bind="template: { name: 'itemTemplate', foreach: AllItems }"></tbody>
</table>
</div>
<script type="text/html" id="itemTemplate">
<tr>
<td>
<input data-bind="value: itemnumber" />
Remove Item
</td>
</tr>
</script>
JS code:-
function ViewModel() {
var self = this;
self.newItem = ko.observable({
manufacturer: "",
itemnumber: "",
itemDescription: ""
});
self.textBoxVal = ko.observable();
self.AllItems = ko.observableArray();
self.addItem = function() {
self.newItem().manufacturer= "test";
self.newItem().itemDescription= "data";
self.newItem().itemnumber = self.textBoxVal();
self.AllItems.push(self.newItem);
};
self.removeItem = function(data) {
self.AllItems.remove(data);
};
}
$(document).ready(function() {ko.applyBindings(new ViewModel()); });
Your first issue was because, each time you are trying to add a new item, you were changing the value of itemNumber, which is an observable.
Observable value will be changed every where it is binded, when it's value is changed.
Instead you need to create new object and do push into the observableArray.
Refer doc to know more about observableArray.
For your second problem change removeItem as given below:-
self.removeItem = function(data) {
var dtIndex = self.AllItems.indexOf(data); //Get the index of the object you want to remove.
self.AllItems.splice(dtIndex, 1); //Then do splice
};
You can refer the above doc, to know how to use splice.
EDIT based on the suggestion in the comment :-
For working code of edited answer click here.
Hope this will solve your problem.

How to bind a ko.obersavableArray that is nested in an object

I have my knockout page hub, and I need a ko.obeservableArray nested in a ko.observable object, this is where I define them:
function IncomeDeclarationHub() {
//data comes from a ajax call.
self.myIncomeDeclarationViewModel = ko.observable(new IncomeDeclarationViewModel(data));
}
function IncomeDeclarationViewModel(data) {
var self = this;
self.retentionAmount = ko.observable();
self.taxableMonth = ko.observable();
self.incDecDetGroViewModels = ko.observableArray();
if (data != null) {
var arrayLenght = data.IncDecDetGroViewModels.length;
for (var i = 0; i < arrayLenght; i++) {
var myObject = new IncomeDecDetGroViewModel(data.IncDecDetGroViewModels[i]);
self.incDecDetGroViewModels.push(myObject);
}
}
}
And this is my HTML code:
<span class="label">
Retention Amount
</span>
<input data-bind="value: myIncomeDeclarationViewModel.retentionAmount" />
<table>
<tbody data-bind="foreach: myIncomeDeclarationViewModel.incDecDetGroViewModels">
...
</tbody>
</table>
Ok so the thing is that incDecDetGroViewModels never gets populated, I used to have that ko.obersableArray outside the object, and it worked fine, now that I inserted it in my object myIncomeDeclarationViewModel is not populating the html table. Do I need to call it in a different way at the data-bind
myIncomeDeclarationViewModel is an observable, so you have to unwrap it to access it's properties. Add parenthesis to unwrap it (access the observable's underlying value) like this:
<span class="label">
Retention Amount
</span>
<input data-bind="value: myIncomeDeclarationViewModel().retentionAmount" />
<table>
<tbody data-bind="foreach: myIncomeDeclarationViewModel().incDecDetGroViewModels">
...
</tbody>
</table>
Here's a working jsFiddle based on your example
JsFiddle
well previously you can access just becoz it is in scope but right now you done some nesting so you just need to some looping in your view part to get that .
Something like this may be :
<table data-bind="foreach:myIncomeDeclarationViewModel">
<tbody data-bind="foreach:$data.incDecDetGroViewModels">
...
</tbody>
</table>
You can also ContainerLess foreach if you looking for something different like :
<!-- ko foreach:myIncomeDeclarationViewModel -->
//your table code
<!--/ko-->
I hope this solves the riddle .

How to update a knockout view model based on a separate callback, rather than user input

I have built a view model representing time slices with the following structure:
function TimeslotViewModel() {
this.timeslots = ko.observableArray();
this.updateTimeslots = function(timeslots) {
this.timeslots.destroyAll();
}
this.clearTimeslots = function() {
this.timeslots.destroyAll();
}
this.addTimeslot = function(timeslot) {
this.timeslots.push(timeslot);
}
}
function Timeslot(time, available) {
this.time = time;
this.available = available;
}
I'm trying to render this in a tabular format like so:
<div class="container">
<table class="table">
<thead>
<tr><th>Time</th><th>Status</th>
</thead>
<tbody data-bind="foreach: timeslots">
<td data-bind="text: time"></td>
<td data-bind="text: available"</td>
</tbody>
</table>
</div>
I've bound on page load:
$(function() {
ko.applyBindings(new TimeslotViewModel());
});
I'm trying to populate this table based on the callback result from an ajax call, but it doesn't seem to be working as expected. Here is what I tried:
$.getJSON(
"/myAjaxCall",
function (jsonData) {
var timeslotViewModel = new TimeslotViewModel();
timeslotViewModel.clearTimeslots();
$.each(jsonData, function (i, ts) {
var tsData = JSON.parse(ts);
var timeslot = new Timeslot(tsData.time, tsData.booked);
timeslotViewModel.addTimeslot(timeslot);
});
});
Unfortunately, I'm not seeing my view model's array get populated at all from this code. What is the right way to populate a view model based on a callback function's response?
You are creating a new viewmodel instead of updating the current one.
Replace this line
var timeslotViewModel = new TimeslotViewModel();
Either create a global viewmodel:
var myVm = new TimeslotViewModel();
ko.applyBindings(myVm);
//...
var timeslotViewModel = myVm;
Or get the current one from a node:
var timeslotViewModel = ko.contextFor($('.container').get(0)).$root

knockout observable array is not updating view on removing elements from array

Here is my view model code
var TopicsViewModel = function() {
var self = this;
var fakeTopicData =
[
];
self.createProfile = function () {
alert("came to create profile");
};
self.editProfile = function () {
alert("came to edit profile");
};
self.removeProfile = function (profile) {
alert("came to remove profile");
fakeTopicData.pop();
self.topicsArr(fakeTopicData);
};
var refresh = function() {
self.topicsArr = fakeTopicData;
};
self.topicsArr = ko.observableArray([]);
refresh();
};
ko.applyBindings(new TopicsViewModel());
Here is my html for the view:
<hr />
<hr />
<table class="table table-striped table-bordered table-condensed">
<tr >
<th>Area</th>
<th>Name</th>
<th>Link</th>
<th>Description</th>
<th>Why</th>
</tr>
<tbody data-bind="foreach : topicsArr">
<tr>
<td data-bind="text :area"> </td>
<td class=""><a data-bind="text:name, click:$parent.editProfile"></a></td>
<td data-bind="text:link"> </td>
<td data-bind="text:desc"> </td>
<td data-bind="text:why" ></td>
<td><button class="btn btn-mini btn-danger" data-bind="click:$parent.removeProfile">remove</button></td>
</tr>
</tbody>
</table>
<script src="~/Scripts/Topic.js"></script>
The view initially display all the Topics in my fakeData Array.
On clicking the remove Button, I am trying to remove an element from the array, and expected the view to refresh and not show the removed item any more. However the view still shows all the 3 topics.
Could someone please point to what I might be doing wrong.
I spend a long time researching the other similar queries on stackoverflow, but am still stuck. Thanks so much for any insight into this issue.
You are replacing your observable array called topicsarr with one which isn't observable in your refresh method...
Change
var refresh = function() {
self.topicsArr = fakeTopicData;
};
to
var refresh = function() {
self.topicsArr(fakeTopicData);
};
you have 2 issues in your code.
First, you are setting your observableArray topicsArr with non observableArray or normal array in refresh function. Instead use self.topicsArr(fakeTopicData)
Second, in function removeProfile you are using pop() to remove profile element. From KnockoutJS documentation:
myObservableArray.pop() removes the last value from the array and
returns it
So, it's better to use remove(item) and pass to it your profile element or loop through your array and remove that specific item
myObservableArray.remove(someItem) removes all values that equal
someItem and returns them as an array

Knockout.js 2.2.1 can't find observable array

Not sure what's going wrong here, but KnockoutJS is having some issues finding my observable array that's inside my MasterViewModel. Using 2.2.1 with jQuery 1.8.x as well as not my first KJS app. Here it is:
Initialize
$(function() {
window.vm = new MasterViewModel();
ko.applyBindings(vm);
});
ViewModel
function MasterViewModel(data) {
var self = this;
self.currentAppView = ko.observable();
// Users
self.userList = ko.observableArray([]);
self.templateListGetter = ko.computed(function() {
$.getJSON("/user/list"), function(data) {
var mapped = $.map(data, function(item) { return new userModel(item) });
self.userList(mapped);
};
});
self.goToAppView = function(appView) {
location.hash = '!/' + appView;
};
Sammy(function() {
this.get('#!/:appView', function() {
self.currentAppView(this.params.appView);
$('.appview').hide();
ko.applyBindings(new window[this.params.appView+'VM']());
});
this.notFound = function(){
location.hash = "!/dashboard";
}
//this.raise_errors = true;
}).run();
}
The View
<table class="table table-bordered table-striped">
<tbody data-bind="foreach: userList">
<tr>
<td data-bind="text: guid"></td>
<td data-bind="text: firstName"></td>
<td data-bind="text: lastName"></td>
<td data-bind="text: email"></td>
<td data-bind="text: updated"></td>
<td data-bind="text: suspended"></td>
</tr>
</tbody>
</table>
I have a simple table that I am loading
Even after double-checking a couple things like adding defer="defer" to my JS tag and ensuring the userList exists, it simply cannot find the observableArray. It gives the error:
Message: ReferenceError: userList is not defined;
Bindings value: foreach: userList Error {}
Anyone have any idea what's going on?
Update
For those wondering what gets called every time the hash changes:
function usersVM() {
// Data
var self = this;
// Behaviours
$('#users').show();
}
It looks like you're initializing knockout with an undefined viewmodel?
ko.applyBindings(new window[this.params.appView+'VM']());, yet your actual viewmodel is window.vm. Case sensitivity ftw. Also, the viewmodel on window is already created / initialized. So you don't need the new operator.
So, change the applyBindings line to be
ko.applyBindings(window[this.params.appView+'vm']());
Updated Answer: By Poster
There was no necessity to keep running ko.applyBindings every time the route changed since it was already applying bindings on page load. So Sammy.js was changed to:
Sammy(function() {
this.get('#!/:appView', function() {
self.currentAppView(this.params.appView);
$('.appview').hide();
window[this.params.appView+'Route']();
});
this.notFound = function(){
location.hash = "!/dashboard";
}
//this.raise_errors = true;
}).run();
It does look like ko.computed or a regular function call to window.vm.getUserList() isn't running properly, but this will be saved for a different question.
function usersRoute() {
// Data
var self = this;
// Behaviours
$('#users').show();
window.vm.getUserList();
}

Categories

Resources