How to can I create an array of knockout.js observables? - javascript

I have an array of objects for which I am showing their properties.
How can add an individual edit functionality to them? Lets say it to be an edit button for each one of the elements of the list.
I want to show input fields instead of text fields when the object is in edit mode, for this I am using the visible binding. So I need a Boolean observable for each of them.
How can I do this without knowing the amount of elements in the list... I also have add and delete, so I would need to add more observables to this array each time a new element is created.
I also tried to give a ko.observable element to my objects but I could not do this.

I like to use an object inside the observableArray. Here is an example of an inline edit feature for as many many rows as needed.
function Employee(emp) {
var self = this;
self.Name = ko.observable(emp.Name);
self.Age = ko.observable(emp.Age);
self.Salary = ko.observable(emp.Salary);
self.EditMode = ko.observable(emp.EditMode);
self.ChangeMode = function() {
self.EditMode(!self.EditMode());
}
}
function viewModel() {
var self = this;
self.Employees = ko.observableArray()
self.Employees.push(new Employee({
Name: "Joe",
Age: 20,
Salary: 100,
EditMode: false
}));
self.Employees.push(new Employee({
Name: "Steve",
Age: 22,
Salary: 121,
EditMode: false
}));
self.Employees.push(new Employee({
Name: "Tom",
Age: 24,
Salary: 110,
EditMode: false
}));
}
var VM = new viewModel();
ko.applyBindings(VM);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<table border=1>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>Salary</th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: Employees">
<tr data-bind="if: !EditMode()">
<td data-bind="text: Name"></td>
<td data-bind="text: Age"></td>
<td data-bind="text: Salary"></td>
<td><button data-bind="click: ChangeMode">Edit</button></td>
</tr>
<tr data-bind="if: EditMode()">
<td>
<input data-bind="value: Name">
</td>
<td>
<input data-bind="value: Age">
</td>
<td>
<input data-bind="value: Salary">
</td>
<td><button data-bind="click:ChangeMode">Save</button></td>
</tr>
</tbody>
</table>

Related

Knockout input type number detect up or down to add or remove item from observableArray

Using the following code, I want to build up a total cost of the items added using input type="number".
<div data-bind="foreach: availableItems()">
<br />
<input type="number" data-bind="event: {change: $parent.itemCountChange}" min="0" step="1" value="0" />
<label data-bind="text: name"/>
</div>
<label id="totalCost" data-bind="text: totalCost" />
JS:
var refreshmentsModel = function ref() {
var self = this;
self.totalCost = ko.observable(0);
self.addedItems = ko.observableArray();
self.availableItems = ko.observableArray([{name: "Tea", price: 3.00}, {name: "Coffee", price: 4.00}, {name: "Cake", price: 5.00}]);
self.itemCountChange = function(d) {
self.addedItems.push(d);
alert("Added items now: " + self.addedItems().length)
}
};
ko.applyBindings(new refreshmentsModel());
However I can't find out if there was an increase or a decrease, so my addedItems always gets a new item added, even if the count is reduced.
I have tried adding a binding to the value of the number input, but then that binds to each of the inputs in the foreach, so changing one changes them all.
Maybe it would be easier to redesign and have two buttons, one for Add and one for Remove, but if anyone has any ideas on the above that would be great!
Thanks
PS. Sorry, I tried to create a codepen with the code, but it kept giving me the error 'ko is not defined', even though I added the knockout reference in the settings window.
I think all that needs to happen is to add an observable field onto the data in the available items, which holds the value of what is ordered. and then have a computed observable that does the calculation when something is ordered.
something like this.
function ItemModel(data) {
var self = this;
self.name = ko.observable(data.name || 'unknown');
self.price = ko.observable(data.price || 0.00);
self.numberOrdered = ko.observable(data.numberOrdered || 0.00);
self.itemCost = ko.pureComputed(function() {
return parseFloat(self.numberOrdered()) * parseFloat(self.price());
});
}
var data = [{
name: "Tea",
price: 3.00
}, {
name: "Coffee",
price: 4.00
}, {
name: "Cake",
price: 5.00
}];
var refreshmentsModel = function ref() {
var self = this;
function totalCostCalc(accumulator, currentValue) {
return accumulator + currentValue.itemCost();
}
self.totalCost = ko.pureComputed(function() {
return self.availableItems().reduce(totalCostCalc, 0);
});
self.addedItems = ko.observableArray();
self.availableItems = ko.observableArray(data.map(x => new ItemModel(x)));
};
ko.applyBindings(new refreshmentsModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<table>
<theader>
<tr>
<td>Name</td>
<td>Ordered</td>
<td>Price</td>
<td>Item Total</td>
</tr>
</theader>
<tbody data-bind="foreach: availableItems()">
<tr>
<td><label data-bind="text: name" /></td>
<td>
<input type="number" data-bind="textInput: numberOrdered" min="0" step="1" value="0" />
</td>
<td data-bind="text: price">
</td>
<td data-bind="text: itemCost">
</td>
</tr>
</tbody>
<tfooter>
<tr>
<td>Total cost</td>
<td></td>
<td></td>
<td data-bind="text: totalCost"></td>
</tr>
</tfooter>
</table>

Angular4 dynamic table reset values on previous created row

I am creating a dynamic table which adds a row on click(). However, every add of the row resets the value of the previous row.
<tfoot>
<tr>
<td><button type="button" (click)="addRow()">Add row </td>
</tr>
</tfoot>
// html
<tbody>
<tr *ngFor="let row of rows">
<td><input name="something" type="text" [ngModel]="row.item.name </td>
</tr>
</tbody>
// component
...
this.item = {name: 'Superman'};
this.rows = [{
item: this.item
}];
....
this.addRow() {
this.rows.push({
item: this.item
});
}
[SOLVED] We just need to make the name of the input unique!
<tfoot>
<tr>
<td><button type="button" (click)="addRow()">Add row </td>
</tr>
</tfoot>
// html
<tbody>
<tr *ngFor="let row of rows; let i = index">
<td><input name="something_{{i}}" type="text" [ngModel]="row.item.name </td>
</tr>
</tbody>
// component
...
this.item = {name: 'Superman'};
this.rows = [{
item: this.item
}];
....
this.addRow() {
this.rows.push({
item: this.item
});
}
Because you're pushing same object reference in an array multiple times. So if you change this.item will effectively change the value in all newly added array element(since they carry the same object reference).
The solution would be, push clone object to an items array.
this.rows.push({
item: Object.assign({}, this.item); //created clone
});

Populate an input when checkbox is checked from ngRepeat

I'm trying to populate an input two ways. The first method is to simply type an amount into the input, which works perfectly. The second method (which I'm struggling with) is to check the checkbox generated within the ngRepeat directive.
The desired behavior is that the checkbox will take the value of item.amount from the JSON data and populate the input with that value. Here is the markup:
<table class="table table-striped header-fixed" id="invoiceTable">
<thead>
<tr>
<th class="first-cell">Select</th>
<th class="inv-res2">Invoice #</th>
<th class="inv-res3">Bill Date</th>
<th class="inv-res4">Amount</th>
<th class="inv-res5">Amount to Pay</th>
<th class="inv-res6"></th>
</tr>
</thead>
<tbody>
<tr ng-if="invoices.length" ng-repeat="item in invoices | filter: {status:'Unpaid'}">
<td class="first-cell"><input type="checkbox" /></td>
<td class="inv-res2">{{item.invoiceNum}}</td>
<td class="inv-res3">{{item.creationDate}}</td>
<td class="inv-res4" ng-init="invoices.total.amount = invoices.total.amount + item.amount">{{item.amount | currency}}</td>
<td class="inv-res5">$
<input ng-validate="number" type="number" class="input-mini" ng-model="item.payment" ng-change="getTotal()" step="0.01" /></td>
</tr>
</tbody>
</table>
<table class="table">
<tbody>
<tr class="totals-row" >
<td colspan="3" class="totals-cell"><h4>Account Balance: <span class="status-error">{{invoices.total.amount | currency }}</span></h4></td>
<td class="inv-res4"><h5>Total to pay:</h5></td>
<td class="inv-res5">{{total | currency}}</td>
<td class="inv-res6"></td>
</tr>
</tbody>
</table>
And here is the JavaScript:
myApp.controller('invoiceList', ['$scope', '$http', function($scope, $http) {
$http.get('assets/js/lib/angular/invoices.json').success(function(data) {
$scope.invoices = data;
});
$scope.sum = function(list) {
var total=0;
angular.forEach(list , function(item){
total+= parseInt(item.amount);
});
return total;
};
$scope.total = 0;
$scope.getTotal = function() {
$scope.total = 0;
$scope.invoices.forEach(function(item){
$scope.total += parseFloat(item.payment);
});
};
$scope.pushPayment = function(item){
if($scope.checked == 'checked'){
return item.payment;
}
};
}]);
If I understand correctly you want a toggle-able check box, If it is checked then you want to copy that invoices amount into the input box below. You could do something similar to below with a combination of ng-model and ng-change
<tr ng-if="invoices.length" ng-repeat="item in invoices | filter: {status:'Unpaid'}">
<td class="first-cell">
<input type="checkbox" ng-model="item.checked" ng-change="select(item)"/>
</td>
<td class="inv-res5">$
<input ng-validate="number" type="number" class="input-mini" ng-model="item.payment" step="0.01"/>
</td>
</tr>
and add the following to your controller
$scope.select = function(item) {
if(item.checked){
item.payment = item.amount;
}
}
What this should do:
You bind the status of the check box to $scope.checked using ng-model
Every time the checkbox status changes ng-change is called, therefore selectInvoice is called.
Select invoice checks whether the checkbox is checked and adjusts the item.payment value accordingly which is bound to the inputs ng-model
See this Plunker for a working example (Note I thinned out the code so its only the bit we're interested in
As an aside, you don't need to have the input box call getTotal when its value changes. Just change the last few lines to:
<td class="inv-res4"><h5>Total to pay:</h5></td>
<td class="inv-res5">{{getTotal() | currency}}</td>
And modify your JavaScript to:
$scope.getTotal = function() {
var total = 0;
$scope.invoices.forEach(function(item){
total += parseFloat(item.payment);
});
return total;
};
It will still be up to date every time Angular 'digests'
The Plunker
The html:
<table class="table table-striped header-fixed" id="invoiceTable">
<thead>
<tr>
<th class="first-cell">Select</th>
<th class="inv-res2">Invoice #</th>
<th class="inv-res3">Bill Date</th>
<th class="inv-res4">Amount</th>
<th class="inv-res5">Amount to Pay</th>
<th class="inv-res6"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in mainCtrl.invoiceList.invoices">
<td class="first-cell"><input type="checkbox" ng-model="item.selected" /></td>
<td class="inv-res2">{{item.invoiceNum}}</td>
<td class="inv-res3">{{item.creationDate}}</td>
<td class="inv-res4" ng-init="invoices.total.amount = invoices.total.amount + item.amount">{{item.amount | currency}}</td>
<td class="inv-res5">$
<input type="text" ng-model="mainCtrl.getAmount(item)"/></td>
</tr>
</tbody>
</table>
<table class="table">
<tbody>
<tr class="totals-row" >
<td colspan="3" class="totals-cell"><h4>Account Balance: <span class="status-error">{{invoices.total.amount | currency }}</span></h4></td>
<td class="inv-res4"><h5>Total to pay:</h5></td>
<td class="inv-res5">{{mainCtrl.getTotalAmount()}}</td>
<td class="inv-res6"></td>
</tr>
</tbody>
</table>
The JS:
var app = angular.module('plunker', []);
app.controller('MainCtrl', ['tempDataStorageService', function(tempDataStorageService) {
var myCtrl = this;
myCtrl.invoiceList = tempDataStorageService;
myCtrl.getAmount = function(item){
return item.selected? item.amount : "";
};
myCtrl.getTotalAmount = function(){
var total = 0;
for(var i = 0; i < tempDataStorageService.invoices.length; i++){
if(tempDataStorageService.invoices[i].selected){
total = total + tempDataStorageService.invoices[i].amount;
}
}
return total;
}
}]);
app.factory('tempDataStorageService', function() {
// The service object
var storage = this;
storage.invoices = [{invoiceNum: 1, creationDate: "1/1/16", amount: 1.50, selected: false},
{invoiceNum: 2, creationDate: "1/2/16", amount: 2.50, selected: false},
{invoiceNum: 2, creationDate: "1/2/16", amount: 2.50, selected: false},
{invoiceNum: 3, creationDate: "1/3/16", amount: 3.50, selected: false},
{invoiceNum: 4, creationDate: "1/4/16", amount: 4.50, selected: false},
{invoiceNum: 5, creationDate: "1/5/16", amount: 5.50, selected: false},
{invoiceNum: 6, creationDate: "1/6/16", amount: 6.50, selected: false},
{invoiceNum: 7, creationDate: "1/7/16", amount: 7.50, selected: false},
{invoiceNum: 8, creationDate: "1/8/16", amount: 8.50, selected: false}];
// return the service object
return storage;
});
That's a way of doing it
Add an attribute amountToPay to your invoices and send the item to the getTotal function:
<input ng-validate="number" type="number" class="input-mini" value="{{pushPayment()}}"
ng-model="item.amountToPay" ng-change="getTotal(item)" step="0.01" /></td>
In your checkbox change the ng-model to item.checked:
<input type="checkbox" ng-checked="item.checked" /></td>
Add this to your getTotal() function:
$scope.getTotal = function(item) {
item.checked = true;
$scope.total = 0;
$scope.invoices.forEach(function(item){
$scope.total += parseFloat(item.payment);
});
};
If you need to populate your input, just modify the amountToPay attribute
Thanks for the assistance, but I think I was overthinking it. I got to work with simply adding:
ng-click="item.payment=item.amount" ng-change="getTotal()"
to the checkbox. I still have to incorporate this into the sum function, but I solved the issue.

Make bound element appear when populated after initial page load

In the linked JS Fiddle, I have a list of 2 contact objects, and a null active_contact object when the page loads. When clicking on the "Make Active" link under a contact listing, I want that contact's properties to populate the active contact input fields. Currently, my function to populate the active_contact object is firing, and the value is populated as expected, but it is not showing on the page (when inspecting element, the input fields do not even show in the code).
It is worth mentioning this is my first time using Knockout, so it is entirely possible I am missing something very basic.
The code:
HTML
var initialData = [
{ firstName: "Danny", lastName: "LaRusso", phones: [
{ type: "Mobile", number: "(555) 121-2121" },
{ type: "Home", number: "(555) 123-4567"}]
},
{ firstName: "Sensei", lastName: "Miyagi", phones: [
{ type: "Mobile", number: "(555) 444-2222" },
{ type: "Home", number: "(555) 999-1212"}]
}
];
var ContactsModel = function(contacts) {
var self = this;
self.contacts = ko.observableArray(ko.utils.arrayMap(contacts, function(contact) {
return { firstName: contact.firstName, lastName: contact.lastName, phones: ko.observableArray(contact.phones) };
}));
self.active_contact = ko.observable();
self.makeActive = function(firstName){
for(var i in self.contacts()){
if(self.contacts()[i].firstName == this.firstName){
console.log(self.contacts()[i]);
self.active_contact = self.contacts()[i];
}
}
}
};
ko.applyBindings(new ContactsModel(initialData));
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class='liveExample'>
<h2>Contacts</h2>
<div id='contactsList'>
<h3>active contact: </h3>
<div class="active" data-bind="with: active_contact">
<input type="text" data-bind="value: firstName" />
<input type="text" data-bind="value: lastName" />
</div>
<table class='contactsEditor'>
<tr>
<th>First name</th>
<th>Last name</th>
<th>Phone numbers</th>
</tr>
<tbody data-bind="foreach: contacts">
<tr>
<td>
<input data-bind='value: firstName' />
<div><a href='#' data-bind='click: $root.makeActive'>Make Active</a></div>
</td>
<td><input data-bind='value: lastName' /></td>
<td>
<table>
<tbody data-bind="foreach: phones">
<tr>
<td><input data-bind='value: type' /></td>
<td><input data-bind='value: number' /></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
</div>
There are two problems in self.makeActive:
.makeActive does not receive a firstname, it really receives the full contact object - so the whole for loop is not necessary, since you already have what you are looking for.
The main issue is on this line:
self.active_contact = self.contacts()[i];
You are not assigning a new value to the observable, but are really replacing the observable itself. This is why your markup does not update anymore - the observable it was originally bound to is now gone.
Try this instead:
self.makeActive = function (contact) {
self.active_contact(contact);
};
Try this: http://jsfiddle.net/dsqo3mnw/
First, I changed this:
self.active_contact = self.contacts()[i];
to this:
var activeContact = self.contacts()[i];
self.active_contact(activeContact.firstName + " " + activeContact.lastName);
I'm new to knockout as well, but this is my understanding: active_contact isn't a string object; it's a knockout observable function. To change its value, you have to invoke that function and pass in the new value.
Second, I changed your data binding from this:
<div class="active" data-bind="with: active_contact">
<div data-bind="text: firstName"></div>
</div>
to this:
<p style="font-weight: bold">
Modified Active Contact Binding:
<span data-bind="text: active_contact"></span>
</p>
I'm not familiar with the with binding yet, but the text binding should suit your needs just fine, no?

How to update the values of Observable Array through Select element

I have a observable Array and I display that in a select element, for example if I have 4 elements in the array, then I will have 4 Select Element.
When I change the items in the Select list the observable array seems to be unchanged.
Please help me on how to update the observable array.
The jsfiddle for the same is here
HTML Code
<div>
<div>
<table>
<th>Name</th>
<th>Age</th>
<th>Country</th>
<tbody data-bind="foreach: players">
<tr>
<td data-bind="text: name"></td>
<td data-bind="text: age"></td>
<td data-bind="text: country"></td>
<td data-bind="text: trophies"></td>
<td>
<button data-bind="click: $root.editPlayers">Edit</button>
</td>
</tr>
</tbody>
</table>
</div>
<div data-bind="with: editPlayer">
<h3>Edit</h3>
Name:
<input type="text" data-bind="value: name" />
<br/>Age:
<input type="text" data-bind="value: age" />
<br/>Country:
<input type="text" data-bind="value: country" />
<span data-bind="foreach: trophies">
<br/>Tropies:
<select data-bind="options: $root.trophiesList, value:$data, optionsCaption:'Choose..'"></select>
</span>
</div>
</div>
JavaScript Code
var player = function (name, age, country, trophyArray) {
this.name = ko.observable(name);
this.age = ko.observable(age);
this.country = ko.observable(country);
this.trophies = ko.observableArray(trophyArray);
};
var playerModel = function () {
var self = this;
self.players = [
new player('Roger', 32, 'swiss', ['AO','FO','WB','US']),
new player('Murray', 28, 'Scott', ['WB','US'])];
self.editPlayer = ko.observable();
self.trophiesList = ['AO','US','FO', 'WB'];
self.editPlayers = function (player) {
self.editPlayer(player);
}
}
ko.applyBindings(new playerModel());
Remove the following markup:
<span data-bind="foreach: trophies">
and the closing </span> tag.
Then use the selectedOptions binding to manage the trophies array:
<br/>Tropies:
<select data-bind="options: $root.trophiesList, selectedOptions:trophies, optionsCaption:'Choose..'" multiple="true" size="10"></select>
The user can then select/deselect multiple trophies.
Hi I have created an updated fiddle for you
self.players = [
new player('Roger', 32, 'swiss', [{key: ko.observable('AO')},{key:ko.observable('FO')},{key:ko.observable('WB')},{key: ko.observable('US')}]),
new player('Roger', 32, 'swiss', [{key: ko.observable('AO')},{key:ko.observable('FO')},{key:ko.observable('WB')},{key: ko.observable('US')}])];
http://jsfiddle.net/M8m8R/4/
Hope this helps

Categories