Add and remove Items on client side with Knockoutjs - javascript

I've been struggling to make an interactive form, in which a viewmodel has a collection of items. I want to dynamically add/remove items from that collection.
I've found it difficult to find examples that go to this depth and most of them usually stay on a more straight forward implementation, however I've come across This post which pretty much explains what i'm doing with this brilliant jsfiddle, in which a json is pulled using the knockout mapping pluggin and then mapped.
var company;
function PersonViewModel(data) {
var personMapping = {
'ignore': ['twitter', 'webpage'],
'copy': ['age'],
'lastName': {
'create': function (options) {
return ko.observable(options.data.toUpperCase());
}
}
};
ko.mapping.fromJS(data, personMapping, this);
this.fullName = ko.computed(function () {
return this.firstName() + ' ' + this.lastName();
}, this);
}
function CompanyViewModel(data) {
var companyMapping = {
'ignore': ['address', 'website'],
'name': {
'create': function (options) {
return ko.observable(options.data.toUpperCase());
}
},
'employees': {
key: function (data) {
return ko.utils.unwrapObservable(data.personId);
},
create: function (options) {
return new PersonViewModel(options.data);
}
}
};
ko.mapping.fromJS(data, companyMapping, this);
}
What i don't know how to achieve is how and where exactly to add the 'addEmployee' and 'removeEmployee' functions? and how to bind them to a button?.
Thank you in advance!

The logical place to add these would be to your CompanyViewModel. For example, something like this:
function CompanyViewModel(data) {
var self = this;
var companyMapping = {
// ...as before
};
self.addEmployee = function () {
// as an example, we are just adding a static new employee
self.employees.push(new PersonViewModel({
lastName: "new",
firstName: "employee",
age: 10
}));
}
// important, with how we are binding the function, we expect the
// argument, e, to be the employee to remove
self.removeEmployee = function (e) {
self.employees.remove(e);
}
ko.mapping.fromJS(data, companyMapping, this);
}
Add to bind, you can do something like this:
<div id="company">
<h1 data-bind="text: name"></h1>
<h2>Employees</h2>
<input type="button" value="add" data-bind="click: addEmployee" />
<table>
<thead>
<tr>
<th>Full name</th>
<th>Last name</th>
<th>First name</th>
<th>Age</th>
</tr>
</thead>
<tbody data-bind="foreach: employees">
<tr>
<td data-bind="text: fullName"></td>
<td data-bind="text: lastName"></td>
<td data-bind="text: firstName"></td>
<td data-bind="text: age"></td>
<td>
<input type="button" value="x" data-bind="click: $parent.removeEmployee" />
</td>
</tr>
</tbody>
</table>
</div>
Which will add an add button as well as a remove x button to each employee which calls the removeEmployee function on the parent CompanyViewModel passing in the current employee.
Here's an updated fiddle

You would add those functions to your CompanyViewModel.
CompanyViewModel
...
this.addEmployee = function () {
this.employees.push(new PersonViewModel({
firstName: 'New',
lastName: 'Employee',
age: 777,
}));
};
this.removeEmployee = function () {
this.employees.pop();
};
HTML
....
<div data-bind="click: addEmployee">Add Employee</div>
<div data-bind="click: removeEmployee">Remove Employee</div>
...
http://jsfiddle.net/HBKYP/198/

Related

Uncaught Error: Unable to process binding "foreach" (knockout js)

Error :
"Uncaught Error: Unable to process binding "foreach: function() {return phones }"
Message: Invalid object that looks like an observable; possibly from another Knockout instance"
html :
<h3>Contacts</h3>
<div >
<table >
<thead>
<tr>
<th>first name</th>
<th>last name</th>
<th>number</th>
</tr>
</thead>
<tbody data-bind="foreach: contacts">
<tr>
<td>
<input data-bind="value :firstName" /><br />
delete contact
</td>
<td>
<input data-bind="value : lastName" />
</td>
<td>
<table>
<tbody data-bind="foreach : phones">
<tr>
<td><select data-bind="options : contactType , value : contactTypeValue"></select></td>
<td><input data-bind="value : number" /></td>
<td>delete number</td>
</tr>
</tbody>
</table>
add number
</td>
</tr>
</tbody>
</table>
<br /><br />
<button data-bind="click : addContact"> add contact</button>
</div>
javascript :
var contactsModel = function ()
{
var self = this;
this.contacts = ko.observableArray([]);
this.addContact = function ()
{
this.contacts.push({
firstName: "",
lastName: "",
phones: ko.observableArray()
});
};
this.deleteContact = function (contact)
{
this.contacts.remove(contact);
};
this.addNumber = function (contact)
{
contact.phones.push({
type: "",
number: ""
});
};
this.deleteNumber= function (phone)
{
$.each(this.contacts(), function () { this.phones.remove(phone) })
};
}
ko.applyBindings(new contactsModel());
I am new to Knockout JS so can someone please give solution to above error.
I will note 2 things.
First of all, you declare the observableArray without an empty array in it, this might look like a simple case, yet knockout has an issue regarding array declarations, at least it had, up to some version.
So change to this:
this.contacts.push({
firstName: "",
lastName: "",
phones: ko.observableArray([]) // here
});
Also there is a reason that you define self=this;, yet you never really use it.
Re-write your code like so:
var contactsModel = function ()
{
var self = this;
self.contacts = ko.observableArray([]);
self.addContact = function ()
{
self.contacts.push({
firstName: "",
lastName: "",
phones: ko.observableArray([])
});
};
self.deleteContact = function (contact)
{
this.contacts.remove(contact);
};
self.addNumber = function (contact)
{
contact.phones.push({
type: "",
number: ""
});
};
self.deleteNumber= function (phone)
{
$.each(self.contacts(), function () { self.phones.remove(phone) })
};
}

knockout observableArray not updating table

I wanted to get values from form fields and save them as an object into an observableArray. And show them in the table. So, every time i hit 'add' button the table should be updated but its not working.
<select data-bind="options: gradeList, optionsText: 'Name', value: selectedGrade"></select>
<input type="text" data-bind="value: komark" />
<button data-bind="click: addMark">Add</button>
<table>
<thead>
<tr>
<th>SN</th>
<th>Name</th>
<th>Mark</th>
</tr>
</thead>
<tbody data-bind="foreach: allMarks">
<tr>
<td data-bind="$data.id"></td>
<td data-bind="$data.name"></td>
<td data-bind="$data.mark"></td>
</tr>
</tbody>
</table>
<p data-bind="text: allMarks"></p>
this is my html. 'gradeList' is also an observableArray but its working and i'm getting nice dropdown menu. On last 'p' element, text gets updated with every 'add' button click with [Object object] text but table never gets updated.
var newModel = function () {
var self = this;
self.komark = ko.observable();
self.mark = ko.observable();
self.selectedGrade = ko.observable();
self.gradeList = ko.observableArray([]);
self.allMarks = ko.observableArray([]);
self.loadAllGrades = function () {
$.ajax({
type: "GET",
dataType: "text",
url: "studenthandler.ashx",
data: { "action": "getAllGrades", "id": 0 },
success: function (res) {
self.gradeList(JSON.parse(res));
},
error: function () {
alert("Failed to load.\nHit Refresh.");
}
});
};
self.addMark = function () {
// console.log("button clicked");
self.mark({ "id": self.selectedGrade().Id, "name": self.selectedGrade().Name, "mark": self.komark() });
console.log(self.mark());
self.allMarks.push(self.mark());
console.log(self.allMarks());
};
self.loadAllGrades();
}
this is my javasript. The value of 'mark' and 'allMarks' gets updated in console but TABLE never gets updated.
<td data-bind="$data.id"></td> doesn't do anything, you haven't specified a binding. You probably wanted:
<td data-bind="text: $data.id"></td>
<!-- ----------^^^^^^ -->
...and similar for name, mark.
Working example:
var newModel = function() {
var self = this;
self.komark = ko.observable();
self.mark = ko.observable();
self.selectedGrade = ko.observable();
self.gradeList = ko.observableArray([]);
self.allMarks = ko.observableArray([]);
self.loadAllGrades = function() {
/*
$.ajax({
type: "GET",
dataType: "text",
url: "studenthandler.ashx",
data: { "action": "getAllGrades", "id": 0 },
success: function (res) {
self.gradeList(JSON.parse(res));
},
error: function () {
alert("Failed to load.\nHit Refresh.");
}
});
*/
self.gradeList.push(
{Id: 1, Name: "Grade1"},
{Id: 2, Name: "Grade2"},
{Id: 3, Name: "Grade3"}
);
};
self.addMark = function() {
// console.log("button clicked");
self.mark({
"id": self.selectedGrade().Id,
"name": self.selectedGrade().Name,
"mark": self.komark()
});
//console.log(self.mark());
self.allMarks.push(self.mark());
//console.log(self.allMarks());
};
self.loadAllGrades();
}
ko.applyBindings(new newModel(), document.body);
<select data-bind="options: gradeList, optionsText: 'Name', value: selectedGrade"></select>
<input type="text" data-bind="value: komark" />
<button data-bind="click: addMark">Add</button>
<table>
<thead>
<tr>
<th>SN</th>
<th>Name</th>
<th>Mark</th>
</tr>
</thead>
<tbody data-bind="foreach: allMarks">
<tr>
<td data-bind="text: $data.id"></td>
<td data-bind="text: $data.name"></td>
<td data-bind="text: $data.mark"></td>
</tr>
</tbody>
</table>
<p data-bind="text: allMarks"></p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
Side note: $data.id is a long way to write id. :-)
Side note 2: The [object Object] you're seeing for allMarks is because you're applying the text binding to an array of objects, so you get the default toString behavior of the object. You probably want a foreach for allMarks as well.

Knockout- Issues editing item with pop up

Im new to knockout and loving it so far. I've been trying to build a row editing grid using a simple table with edit links. So far it seemed to be going great but been stuck on this issue where im getting the errors when tying to save and update or canel:
Uncaught TypeError: this.description is not a function
and
Uncaught TypeError: this.editdescription is not a function
Been staring at the code for several hours now can't seem to wrap my head around this one. I am able to replicate the issue in this JSFIDDLE:
http://jsfiddle.net/N2zNk/49/
Would anyone know what is cloging in my code?
Here is my HTML:
<table>
<tr>
<th>ID</th>
<th>Description</th>
<th>Department</th>
<th>Edit</th>
<th>Delete</th>
</tr>
<tbody data-bind="template: { name: 'rowTmpl', foreach: products }"></tbody>
</table>
<script id="rowTmpl" type="text/html">
<tr>
<td data-bind="text: ID"></td>
<td data-bind="text: Description"></td>
<td data-bind="text: Composante"></td>
<td>
Edit
</td>
<td>
Delete
</td>
</tr>
</script>
<!-- popup -->
<div id="dialogEditProduct" style="width: 400px; max-width: 100%;" data-role="popup" data-theme="c" data-overlay-theme="a" data-dismissible="false" data-bind="with: selectedProduct">
<div data-role="header" data-position="inline">
<h1 ></h1>
</div>
<div data-role="content" data-theme="c">
<p>
<label>Description:</label>
<input data-bind="value: editDescription" />
</p>
<p>
<label>Composante:</label>
<input data-bind="value: editComposante" />
</p>
<button data-role="button" data-bind="click: function() { Incidents.pvm.acceptEdit(); }">Save</button>
<button data-role="button" data-bind="click: function() { Incidents.pvm.cancelEdit() }">Cancel</button>
</div>
</div>
Here is my code:
function Item(ID, Description, Composante) {
var self = this;
this.ID = ID;
this.Description = ko.observable(Description);
this.Composante = ko.observable(Composante);
this.editDescription = ko.observable(Description);
this.editComposante = ko.observable(Composante);
this.accept = function () {
this.description(this.editdescription);
this.price(this.editPrice);
return true;
}.bind(this);
//reset to originals on cancel
this.cancel = function () {
this.editdescription(this.description);
this.editComposante(this.Composante);
}.bind(this);
}
Incidents = {
pvm: {},
productStore: {
products: [],
init: function (data) {
this.products = $.map(data, function (product) {
return new Item(product.ID, product.Description, product.Composante);
});
}
},
init: function () {
/*emuluate pulling orders from DB*/
/*this will come from server or local web storage*/
var dataFromServer = [{
ID: "123",
Description: "The server x is unavailable",
Composante: "CCD"
}, {
ID: "124",
Description: "The router located downtown is down",
Composante: "CCDD"
}, {
ID: "125",
Description: "Fiber optic cable downtown is flapping",
Composante: "MIG"
}, {
ID: "126",
Description: "Network unvailable at the beaver site",
Composante: "MIC"
}];
this.productStore.init(dataFromServer);
$(function () {
Incidents.pvm = new Incidents.productViewModel(Incidents.productStore.products);
ko.applyBindings(Incidents.pvm);
$("#productList").listview('refresh');
});
},
productViewModel: function (data) {
var self = this;
var productsArray = [];
if (data && data.length > 0) {
productsArray = data;
}
this.products = ko.observableArray(productsArray);
this.selectedProduct = ko.observable();
this.editProduct = function (productToEdit) {
self.selectedProduct(productToEdit);
// Incidents.pvm.selectedProduct(productToEdit);
};
this.acceptEdit = function () {
var selected = Incidents.pvm.selectedProduct();
if (selected.accept()) {
Incidents.pvm.selectedProduct("");
$('#dialogEditProduct').popup('close');
}
};
this.cancelEdit = function () {
Incidents.pvm.selectedProduct().cancel();
Incidents.pvm.selectedProduct("");
$('#dialogEditProduct').popup('close');
};
}
};
ko.bindingHandlers.jqButton = {
init: function (element) {
$(element).button();
},
update: function (element, valueAccessor) {
var currentValue = valueAccessor();
$(element).button("option", "disabled", currentValue.enable === false);
}
};
ko.bindingHandlers.jqmListView = {
init: function (element) {
$(element).listview();
},
update: function (element, valueAccessor) {
$(element).listview('refresh');
}
};
ko.bindingHandlers.openProductDialog = {
update: function (element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor());
if (value) {
$.mobile.changePage("#dialogEditProduct", {
role: 'dialog'
});
$("#dialogEditProduct").open();
// $("#dialogEditProduct").trigger('create');
}
}
};
$.extend({
isNumber: function (obj) {
return !isNaN(parseFloat(obj)) && isFinite(obj);
}
});
Incidents.init();
Javascript is case sensitive. You have mixed up description and Description. Also, editDescription and editdescription.

knockout dynamic grid, add column and rows

I am trying to add rows and columns to a knockout grid on the client side after .NET MVC has displayed the html grid
I could find a way to dynamically add rows but am not able to get the grid to add columns and rows. Does anyone know how to add both rows and columns dynamically with knockout and then have the data in the grid (including the new row and column headers) to be available for saving to the server?
<form action='/controller/save'>
<p>There are <span data-bind='text: series().length'> </span> series set</p>
<table data-bind='visible: series().length > 0'>
<thead>
<tr>
<th>---</th>
<!-- ko foreach: cols -->
<th data-bind='text:title'></th>
<!-- /ko -->
<th><button> Add New Column</button></th>
</tr>
</thead>
<tbody data-bind='foreach: series'>
<tr>
<td data-bind='text:name'></td>
<!-- ko foreach: cols -->
<td><input class='required' /></td>
<!-- /ko -->
<td><a href='#' data-bind='click: $root.removeSeries'>Delete</a></td>
</tr>
</tbody>
</table>
<button data-bind='click: addNewSeries'>Add New Series</button>
<button data-bind='enable: series().length > 0' type='submit'>Save</button>
and the js
var ItemModel = function (series) {
var self = this;
self.series = ko.observableArray(series);
self.addNewSeries = function () {
self.series.push({
name: "",
value: ""
});
};
self.removeSeries = function (series) {
self.series.remove(series);
};
self.save = function (form) {
ko.utils.postJson($("form")[0], self.items);
};
};
var viewModel = new ItemModel([
{ name: "2012/13", value: "122345" },
{ name: "2013/14", value: "564543" }
]);
ko.applyBindings(viewModel);
var ColumnModel = function (cols) {
var self = this;
self.cols = ko.observableArray(cols);
var Col = function (title) {
var self = this;
self.title = ko.observable(title);
self.addNewCol = function () {
self.cols.push(new Col('new'));
};
};
};
var columnModel = new ColumnModel([
{ title: "col1" },
{ title: "col2" }
]);
ko.applyBindings(columnModel);
I have created a fiddle to show
you made a lot of mistakes there. you need to use the chrome\explorer\firefox tools to debug your stuff. i got it yo work though
<form action='/controller/save'>
<p>There are <span data-bind='text: series().length'> </span> series set</p>
<div data-bind='visible: series().length > 0'>
<table>
<thead>
<tr data-bind="foreach: cols">
<th data-bind='text:$data.title'></th>
</tr>
<button data-bind="click:addNewCol"> Add New Column</button>
</thead>
<tbody data-bind='foreach: series'>
<tr>
<td data-bind='text:name'></td>
<!-- ko foreach :$root.cols -->
<!-- ko if: $index() != 0 -->
<td><input class='required' /></td>
<!-- /ko -->
<!--/ko-->
<td><a href='#' data-bind='click: $root.removeSeries'>Delete</a></td>
</tr>
</tbody>
</table>
</div>
<button data-bind='click: addNewSeries'>Add New Series</button>
<input type="text" data-bind="value: title"/>
<button data-bind='enable: series().length > 0' type='submit'>Save</button>
</form>
$(document).ready(function(){
var ItemModel = function (series) {
var self = this;
self.series = ko.observableArray(series);
self.addNewSeries = function () {
self.series.push({
name: self.title(),
value: ""
});
};
self.title=ko.observable();
self.removeSeries = function () {
//do something with series
self.series.remove(this);
};
self.save = function (form) {
ko.utils.postJson($("form")[0], self.items);
};
self.cols = ko.observableArray([{ title: "col1" },
{ title: "col2" }]);
function Col (title) {
this.title = ko.observable(title);
};
self.addNewCol = function () {
self.cols.push(new Col('new'));
};
};
var viewModel = new ItemModel([
{ name: "2012/13", value: "122345" },
{ name: "2013/14", value: "564543" }
]);
ko.applyBindings(viewModel);
/* var ColumnModel = function (cols) {
};
var columnModel = new ColumnModel([
{ title: "col1" },
{ title: "col2" }
]);
ko.applyBindings(columnModel);
*/
// Activate jQuery Validation
// $("form").validate({ submitHandler: viewModel.save });
});
http://jsfiddle.net/dCfYP/23/

UI does not update when Updating ObservableArray

When a user clicks on a row from a list in the UI (html table tblAllCert), I have a click event that fires to populate another observable array that should populate a second html table (tblCertDetails). My click event fires, I can see data come back, and i can see data in the observable array, but my view does not update.
Here is an overview of the sequence in the code-
1. user clicks on row from html table tblAllCert, which fires selectThing method.
2. In viewmodel code, selectThing passes the row data from the row selected to the GetCertificateDetails(row) method.
3. GetCertificateDetails(row) method calls getCertDetails(CertificateDetails, source) function on my data service (the data service gets data from a web api). getCertDetails(CertificateDetails, source) function returns an observable array.
4. once the data is returned to selectThing, CertificateDetails observable array is populated. It does get data to this point.
Step 5 should be updating the UI (specifically tblCertDetails html table), however the UI does not get updated.
Here is my view-
<table id="tblAllCert" border="0" class="table table-hover" width="100%">
<tbody data-bind="foreach: allCertificates">
<tr id="AllCertRow" style="cursor: pointer" data-bind="click: $parent.selectThing, css: { highlight: $parent.isSelected() == $data.lwCertID }">
<td>
<ul style="width: 100%">
<b><span data-bind=" text: clientName"></span> (<span data-bind=" text: clientNumber"></span>) <span data-bind=" text: borrowBaseCount"></span> Loan(s) </b>
<br />
Collateral Analyst: <span data-bind=" text: userName"></span>
<br />
Certificate: <span data-bind="text: lwCertID"></span> Request Date: <span data-bind=" text: moment(requestDate).format('DD/MMM/YYYY')"></span>
</td>
</tr>
</tbody>
</table>
<table id="tblCertDetails" border="0" class="table table-hover" width="100%">
<tbody data-bind="foreach: CertificateDetails">
<tr id="Tr1" style="cursor: pointer">
<td>
<ul style="width: 100%">
<b>Loan: <span data-bind=" text: loanNum"></span>
</td>
</tr>
</tbody>
</table>
My viewmodel-
define(['services/logger', 'durandal/system', 'durandal/plugins/router', 'services/CertificateDataService'],
function (logger, system, router, CertificateDataService) {
var allCertificates = ko.observableArray([]);
var myCertificates = ko.observableArray([]);
var isSelected = ko.observable();
var serverSelectedOptionID = ko.observable();
var CertificateDetails = ko.observableArray([]);
var serverOptions = [
{ id: 1, name: 'Certificate', OptionText: 'lwCertID' },
{ id: 2, name: 'Client Name', OptionText: 'clientName' },
{ id: 3, name: 'Client Number', OptionText: 'clientNumber' },
{ id: 4, name: 'Request Date', OptionText: 'requestDate' },
{ id: 5, name: 'Collateral Analyst', OptionText: 'userName' }
];
var activate = function () {
// go get local data, if we have it
return SelectAllCerts(), SelectMyCerts();
};
var vm = {
activate: activate,
allCertificates: allCertificates,
myCertificates: myCertificates,
CertificateDetails: CertificateDetails,
title: 'Certificate Approvals',
SelectMyCerts: SelectMyCerts,
SelectAllCerts: SelectAllCerts,
theOptionId: ko.observable(1),
serverOptions: serverOptions,
serverSelectedOptionID: serverSelectedOptionID,
SortUpDownAllCerts: SortUpDownAllCerts,
isSelected: isSelected,
selectThing: function (row, event) {
CertificateDetails = GetCertificateDetails(row);
isSelected(row.lwCertID);
}
};
serverSelectedOptionID.subscribe(function () {
var sortCriteriaID = serverSelectedOptionID();
allCertificates.sort(function (a, b) {
var fieldname = serverOptions[sortCriteriaID - 1].OptionText;
if (a[fieldname] == b[fieldname]) {
return a[fieldname] > b[fieldname] ? 1 : a[fieldname] < b[fieldname] ? -1 : 0;
}
return a[fieldname] > b[fieldname] ? 1 : -1;
});
});
return vm;
function GetCertificateDetails(row) {
var source = {
'lwCertID': row.lwCertID,
'certType': row.certType,
}
return CertificateDataService.getCertDetails(CertificateDetails, source);
}
function SortUpDownAllCerts() {
allCertificates.sort();
}
function SelectAllCerts() {
return CertificateDataService.getallCertificates(allCertificates);
}
function SelectMyCerts() {
return CertificateDataService.getMyCertificates(myCertificates);
}
});
And releveant excerpt from my data service-
var getCertDetails = function (certificateDetailsObservable, source) {
var dataObservableArray = ko.observableArray([]);
$.ajax({
type: "POST",
dataType: "json",
url: "/api/caapproval/CertDtlsByID/",
data: source,
async: false,
success: function (dataIn) {
ko.mapping.fromJSON(dataIn, {}, dataObservableArray);
},
error: function (error) {
jsonValue = jQuery.parseJSON(error.responseText);
//jError('An error has occurred while saving the new part source: ' + jsonValue, { TimeShown: 3000 });
}
});
return dataObservableArray;
}
Ideas of why the UI is not updating?
When you're using Knockout arrays, you should be changing the contents of the array rather than the reference to the array itself. In selectThing, when you set
CertificateDetails = GetCertificateDetails(row);
the new observableArray that's been assigned to CertificateDetails isn't the same observableArray that KO bound to your table.
You're very close to a solution, though. In your AJAX success callback, instead of creating a new dataObservableArray and mapping into that, you can perform the mapping directly into parameter certificateDetailsObservable. ko.mapping.fromJSON should replace the contents of the table-bound array (you would also get rid of the assignment in selectThing).

Categories