Sending only the updated object from ko.observableArray - javascript

How can I send only the updated model from an observable Array instead of sending the entire array?
var student = function (){
this.studentId=0;
this.firstName=ko.obserable();
this.lastName=ko.obserable();
}
var course= function (){
this.courseId=0;
this.students=ko.obserableArray([]);
this.Name=ko.obserable();
}
Now I want to get only that particular student from course whose info is updated. Assuming that when we add a new class we can dynamically add new students to it on the go. Supposing that you have to validate the previous student before adding a new one. When I get that particular student I want to send that student info back to the server.
Thanks.

If I understood your task right, you could use "arrayChange" event type to get exact changed (added/removed) items:
sourceArray = ko.observableArray();
sourceArray.subscribe(function (changes) {
changes.forEach(function(arrayChange) {
if(arrayChange.status === 'added') {
// some code on add
} else if(arrayChange.status === 'deleted') {
// some code on delete
}
});
}, null, "arrayChange");

If you want to get list of students which have been modified, you can provide a flag to identify if an object has been modified in student object. Use .subscribe to modify that flag whenever a value is updated. Then use ko.computed or ko.pureComputed to get that list.
Also it supposes to be observable.
var student = function (id, firstName, lastName) {
var self = this;
self.hasChanged = ko.observable(false);
var modified = function(){
self.hasChanged(true);
};
self.studentId = ko.observable(id);
self.firstName = ko.observable(firstName);
self.firstName.subscribe(modified);
self.lastName = ko.observable(lastName);
self.lastName.subscribe(modified);
}
var course= function (){
var self = this;
self.courseId = 0;
self.students = ko.observableArray([new student(1, "Cristiano", "Ronaldo"), new student(2, "Lionel", "Messi")]);
self.modifiedStudent = ko.computed(function(){
return ko.utils.arrayFilter(self.students(), function(student) {
return student.hasChanged();
});
}, self);
self.Name = ko.observable("Programming 101");
}
$(document).ready(function () {
var myViewModel = new course();
ko.applyBindings(myViewModel);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
List of all students:
<div data-bind="foreach: students">
<div>
<span data-bind="text: studentId"></span>
<input type="text" data-bind="value: firstName" />
<input type="text" data-bind="value: lastName" />
</div>
</div>
<br/>
List of students which has been modified:
<div data-bind="foreach: modifiedStudent">
<div>
<span data-bind="text: studentId"></span>
<input type="text" data-bind="value: firstName" readonly />
<input type="text" data-bind="value: lastName" readonly />
</div>
</div>

Related

Create observable returning other observables as a single map

I have an own binding for numeric inputs made in knockoutJS which accepts only numbers.
To make big numbers I declare various instances of number in a NumberField like:
var NumberField = function () {
var self = this;
self.maskFormat = "0";
self.firstNumber = ko.observable("");
self.secondNumber = ko.observable("");
self.thirdNumber = ko.observable("");
};
And
<input id="0" maxlength="1" type="tel" data-bind="numeric: firstNumber">
<input id="1" maxlength="1" type="tel" data-bind="numeric: secondNumber">
<input id="2" maxlength="1" type="tel" data-bind="numeric: thirdNumber">
This is working like a charm, but when I made submission, system is expecting a map with numbers. I achieved it IMHO in an ugly way:
Added to NumberField this attribute:
this.cleanNumber = ko.pureComputed(function () {
return this.firstNumber().toString() + this.secondNumber().toString() + this.thirdNumber().toString();
}, this);
And in the code, when I need to use it I must do this:
let unwrapNumbers = this.numbers().cleanNumber().split("").map(function (item){
return Number(item);
});
This is working, but... I'm pretty sure there is an easier and more straight way.... Any suggestions?
I think it could help to split the computed in to two parts:
Getting the numbers you want to include in order
Creating a string based on the ordered values
Often it makes sense to split a computed in to several pure computeds that have a single clear data processing responsibility.
var NumberField = function () {
var self = this;
self.firstNumber = ko.observable(1);
self.secondNumber = ko.observable(2);
self.thirdNumber = ko.observable(3);
self.orderedNumbers = ko.pureComputed(function() {
return [self.firstNumber,
self.secondNumber,
self.thirdNumber].map(ko.unwrap);
});
self.cleanedNumber = ko.pureComputed(function() {
return self.orderedNumbers().join("");
});
};
var nf = new NumberField();
// If you want the numbers:
console.log(nf.orderedNumbers());
// If you want the string
console.log(nf.cleanedNumber());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
Now, I'm not sure what your requirements are, but you can take it one step further and use an observableArray as the base data format:
var NumberField = function () {
var self = this;
self.numbers = ko.observableArray(
[ko.observable(0), ko.observable(1), ko.observable(2)]);
self.add = function() {
self.numbers.push(ko.observable(self.numbers().length));
}
self.unwrappedNumbers = ko.pureComputed(function() {
return self.numbers().map(ko.unwrap);
});
self.cleanedNumber = ko.pureComputed(function() {
return self.unwrappedNumbers().join("");
});
};
ko.applyBindings(new NumberField());
label { display: block }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="foreach: numbers">
<label>
<span data-bind="text: 'Number ' + $index()"></span>
<input type="number" data-bind="textInput: $parent.numbers()[$index()]">
</label>
</div>
<button data-bind="click: add">add</button>
<pre>
Unwrapped:<code data-bind="text: unwrappedNumbers"></code>
Cleaned:<code data-bind="text: cleanedNumber"></code>
</pre>

Knockout js add a new list

I am trying to create a form with a list which adds a list dynamically!
However I just can't get my add function working and it keep complains that my list is not defined.
Here is my viewmodel in jave script
function DeliveryCategoryViewModel() {
var self = this;
self.DeliveryCategory = ko.observable(new DeliveryCategory(1, "new Name", [new DeliveryOption("first"), new DeliveryOption("second")]));
self.addDeliveryOptions = function () {
self.DeliveryOptions.push(new DeliveryOption("new Option"));
}
self.removeDeliveryOptions = function (option) {
self.DeliveryCategory.remove(option);
}
}
and these are actual model which holds the data
function DeliveryCategory(id, name, option) {
this.Id = id;
this.Name = name;
this.DeliveryOptions = ko.observableArray(option);
}
function DeliveryOption(name) {
this.Id = "2";
this.Name = name;
}
$(document).ready(function () {
ko.applyBindings(new DeliveryCategoryViewModel());
});
This is my form
<div id="newDelCategory">
<input data-bind="value:DeliveryCategory().Id" type="hidden" />
<label class="newDelCategoryLabel">New delivery category name: </label>
<input type="text" data-bind="value:DeliveryCategory().Name" class="newDelCategoryText" id="newDelCategoryText" placeholder="Delivery category name" />
</div>
<div id="addOption">
<a id="addOptionLink" href="#" data-bind="click:addDeliveryOptions" class="link">+Add delivery option</a>
</div>
<div id="deliveryOptionContent" data-bind="foreach: DeliveryCategory().DeliveryOptions">
<div class="newDelOption">
<input data-bind="value:$data.Id" type="hidden" />
<div class="divider"></div>
<label class="newDelOptionLabel">New delivery option name: </label>
<input type="text" data-bind="value:$data.Name" class="categoryName" id="newDelOptionText" placeholder="Delivery option name" />
<a id="removeOptionLink" data-bind="click:$parent.removeDeliveryOptions" class="link removeOptionLink">Remove</a>
</div>
</div>
When I try to click click:addDeliveryOptions, it return on Firebug console.
TypeError: self.DeliveryCategory.DeliveryOptions is undefined
self.DeliveryCategory.DeliveryOptions.push(new DeliveryOption("new Option"));
I tried different things such as click:$root.addDeliveryOptions, and also tried to add addDeliveryOptions function as a prototype (e.g. DeliveryCategory.prototype.addDeliveryOptions(...)) and still getting Typeerror...
Is it because DeliveryOptions is not initialised? I expected that it would be when DeliveryCategory is initialised from the DeliveryCategoryViewModel() ...
Any idea? Thanks!
Small Oversight. Easy Fix.
You were calling push and remove off the observable array from the view model but it does not exists as a direct member of the view model.
This is because you never add the observable array directly to this view model. You use a constructor to create an object to observe with DeliveryCategory. One of the properties on that object is an observable array DeliveryOptions. To get access to the observable array from this scope, you have to evaluate DeliveryCategory to get access to it's property DeliveryOptions before you run any array methods. So, instead of this:
self.addDeliveryOptions = function () {
self.DeliveryOptions.push(new DeliveryOption("new Option"));
}
self.removeDeliveryOptions = function (option) {
self.DeliveryCategory.remove(option);
}
The Solution:
self.addDeliveryOptions = function() {
self.DeliveryCategory().DeliveryOptions.push(new DeliveryOption("new Option"));
}
self.removeDeliveryOptions = function(option) {
self.DeliveryCategory().DeliveryOptions.remove(option);
}
See the snippet below
function DeliveryCategoryViewModel() {
var self = this;
self.DeliveryCategory = ko.observable(new DeliveryCategory(1, "new Name", [new DeliveryOption("first"), new DeliveryOption("second")]));
self.addDeliveryOptions = function() {
self.DeliveryCategory().DeliveryOptions.push(new DeliveryOption("new Option"));
}
self.removeDeliveryOptions = function(option) {
self.DeliveryCategory().DeliveryOptions.remove(option);
}
}
function DeliveryCategory(id, name, option) {
this.Id = id;
this.Name = name;
this.DeliveryOptions = ko.observableArray(option);
}
function DeliveryOption(name) {
this.Id = "2";
this.Name = name;
}
$(document).ready(function() {
ko.applyBindings(new DeliveryCategoryViewModel());
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="newDelCategory">
<input data-bind="value:DeliveryCategory().Id" type="hidden" />
<label class="newDelCategoryLabel">New delivery category name:</label>
<input type="text" data-bind="value:DeliveryCategory().Name" class="newDelCategoryText" id="newDelCategoryText" placeholder="Delivery category name" />
</div>
<div id="addOption">
<a id="addOptionLink" href="#" data-bind="click:addDeliveryOptions" class="link">+Add delivery option</a>
</div>
<div id="deliveryOptionContent" data-bind="foreach: DeliveryCategory().DeliveryOptions">
<div class="newDelOption">
<input data-bind="value:$data.Id" type="hidden" />
<div class="divider"></div>
<label class="newDelOptionLabel">New delivery option name:</label>
<input type="text" data-bind="value:$data.Name" class="categoryName" id="newDelOptionText" placeholder="Delivery option name" />
Remove
</div>
</div>

Knockout : find out which observable triggerred computed

I have an object with multiple observables. Is there a way in a computed to know which observable changes, therefore which observable fired the computed?
Thank you in advance
Matthew
Without details of exactly what you are trying to achieve, I'll post this in the hope it might help.
A simple way to track changes is to use the .subscribe method on an observable you want to track. Each time the observable gets updated, this method will fire.
self.myValue = ko.observable('initial value');
self.myValue.subscribe(function (item) {
alert('myValue has changed to: ' + item);
});
The item passed in to the subscribe function is optional, so you can use the new value if required.
Here's a simple example of it in use with a computed:
Sample JSFiddle
JS:
var viewModel = function () {
var self = this;
self.firstName = ko.observable('Mod');
self.lastName = ko.observable('dinu');
self.valueChanged = ko.observable('');
self.fullName = ko.computed(function () {
var val = '';
if (self.valueChanged() !== '') {
val = ' (' + self.valueChanged() + ' Changed)';
}
return self.firstName() + ' ' + self.lastName() + val;
});
self.firstName.subscribe(function () {
self.valueChanged('First Name');
});
self.lastName.subscribe(function () {
self.valueChanged('Last Name');
});
};
ko.applyBindings(new viewModel());
HTML:
<div>
<label for="fname">First Name:</label>
<input id="fname" data-bind="value: firstName" />
</div>
<div>
<label for="lname">Last Name:</label>
<input id="lname" data-bind="value: lastName" />
</div>
<hr />
<div>Hello <span data-bind="text: fullName"></span></div>
<hr />
<div>Value Changed: <span data-bind="text: valueChanged"></span></div>

KnockoutJS, Checkbox list reflecting which groups my currently selected object belongs to?

This is the first time I post a question here, please let me know if I need to improve it. (including the title)
I have in my model two types of objects, Device and Group. A Device can belong to a Group or not, and a Group can contain Devices or not.
What I would like to do is to have a selectlist on the left, showing a list of Device.Name. When the user selects one of them, the details of the selected Device shows in the middle of the screen, and then on the right-hand side, have a list of checkboxes, each checkbox representing a Group.
The problem is I cannot find a way to define the Checked bindings of my checkboxes. I have tried defining an observableArray of CheckedGroup{ Group, bool }, but could not get the bool value to update dynamically according to the currently selected device.
My viewmodel:
function Device(data) {
this.DeviceId = ko.observable(data.DeviceId);
this.Name = ko.observable(data.Name);
this.Number = ko.observable(data.Number);
this.IpAddress = ko.observable(data.IpAddress);
this.Description = ko.observable(data.Description);
this.Groups = ko.observableArray();
this.Groups = $.map(data.Groups, function (grp, i) {
return grp;
});
}
function Group(data) {
var self = this;
self.GroupId = ko.observable(data.GroupId);
self.Name = ko.observable(data.Name);
self.Description = ko.observable(data.Description);
}
function DevicesViewModel() {
// Data
var self = this;
self.devices = ko.observableArray([]);
self.groups = ko.observableArray([]);
self.currentdevice = ko.observable();
$.getJSON(deviceapiuri, function (data) {
var mappeddevices = $.map(data, function (dev, i) {
return new Device(dev)
});
self.devices(mappeddevices);
});
$.getJSON(groupapiuri, function (data) {
var mappedgroups = $.map(data, function (grp, i) {
return new Group(grp)
});
self.groups(mappedgroups);
});
}
My View :
// The device list on the left side
<select id="devicelist_input" size="2" data-bind="options: devices, optionsText: function (i) { return i.Name }, value: currentdevice"> </select>
//One of the input fields that are bound to my currentdevice - this correctly updates when selecting my device.
<input id="stbdescription_input" type="text" data-bind="value: currentdevice() && currentdevice().Description" />
//The list of checkboxes I an trying to bind - the list is populated correctly, but the boxes' Checked property won't update according to the currentdevice
<div id= "group_check_list_input" data-bind="foreach: groups">
<input type="checkbox" class="groupcheckbox" data-bind="value: $data, Checked:$root.currentdevice() && $root.currentdevice().Groups" />
It's quite difficult to get the checkbox selecting. The key thing to doing this is you must be using the same object in both the DevicesViewModel.groups and Device.groups. It needs to be exactly the same object and not just an object with the same data.
You can do this by passing in the viewModel groups to the device constructor and then if the the groupId matches then you add the group to the device groups. I did this using the following code:
function Device (device, groups) {
this.DeviceId = ko.observable(device.DeviceId);
this.Name = ko.observable(device.Name);
this.Number = ko.observable(device.Number);
this.IpAddress = ko.observable(device.IpAddress);
this.Description = ko.observable(device.Description);
var mappedGroups = $.map(device.Groups, function (grp) {
var group = ko.utils.arrayFilter(groups, function(group)
{
return group.GroupId() == grp.GroupId;
});
return group[0];
});
this.Groups = ko.observableArray(mappedGroups);
};
Other problems with your code is you are using value binding for the checkbox when you need to use checkedValue and checked can be just be set to $parent.Groups if you are using a with for the currentdevice.
<select data-bind="options: devices, optionsText: 'Name', value: currentdevice, optionsCaption: 'Select a device'"></select>
<div data-bind="with: currentdevice">
<label>DeviceId:</label>
<span data-bind="text: DeviceId"></span>
<label>Name:</label>
<span data-bind="text: Name"></span>
<label>Number:</label>
<span data-bind="text: Number"></span>
<label>IpAddress:</label>
<span data-bind="text: IpAddress"></span>
<label>Description:</label>
<span data-bind="text: Description"></span>
<div data-bind="foreach: $root.groups">
<input type="checkbox" data-bind="checked: $parent.Groups, checkedValue: $data"/><span data-bind="text: Name"></span>
</div>
</div>
Here is the jsfiddle of it working:
http://jsfiddle.net/k4kAU/1/

knockout observable array not updating after ajax addition

I have an observable array of a complex object. The initial load is fine, and all the expected data looks fine. Now I am working on POSTing new items to that array. NOTE: The observable array is being loaded via ASP.NET ajax web api call.
posting a new item works fine as far as saving it to the database, but my DOM is not getting updated with the new item and I don't know what I am missing.
Here is the entire ViewModel
function ClientList() {
//data
var self = this;
self.initialized = ko.observable(false);
self.clients = ko.observableArray();
self.userId = ko.observable("");
self.name = ko.observable("");
self.logo = ko.observable("");
self.projects = ko.observableArray();
self.clientAddress = ko.observableArray();
self.addClient = function () {
var client = {
UserId: self.userId,
Name: self.name,
Logo: self.logo,
}
client = ko.toJSON(client);
lucidServer.addClient(client);
self.clients.push(client);
}.bind(self);
(function () {
$.ajax({
url: lucidServer.getClients(1),
success: function (data) {
ko.mapping.fromJS(data, {}, self.clients);
self.initialized(true);
}
});
})();
};
function IncompleteStoriesList() {
//data
var self = this;
self.initialized = ko.observable(false);
self.stories = ko.observableArray();
(function () {
$.ajax({
url: lucidServer.getIncompleteStory(1),
success: function (data) {
ko.mapping.fromJS(data, {}, self.stories);
self.initialized(true);
}
});
})();
};
function ViewModel() {
var self = this;
self.clientList = new ClientList();
self.storyList = new IncompleteStoriesList();
}
ko.applyBindings(new ViewModel());
Here is the particular snippet where I am doing the POST (within the ClientList() function).
self.addClient = function () {
self.client = {
UserId: self.userId(),
Name: self.name(),
Logo: self.logo(),
}
//make client object to send to server
var client = ko.toJSON(self.client);
lucidServer.addClient(client);
//push the self.client to the observablearray of clients
self.clients.push(self.client);
}.bind(self);
I verified it is JSON that is sitting inside the client variable, and no error messages are getting thrown, so I am confused. After I add an item and refresh the entire page, it will show up in the list.
EDIT: here is the html associated:
<form data-bind="submit: clientList.addClient">
<div>
<label>userId</label>
<input type="text" data-bind="value: clientList.userId" />
</div>
<div>
<label>name</label>
<input type="text" data-bind="value: clientList.name" />
</div>
<div>
<label>logo</label>
<input type="text" data-bind="value: clientList.logo" />
</div>
<button type="submit">Add</button>
</form>
<!-- ko ifnot: clientList.initialized -->
<span>Loading...</span>
<!-- /ko -->
<ul data-bind="template:{name: 'clientList', foreach:clientList.clients}">
</ul>
And the external template looks like this:
<div id="clientListOutput">
<li><span data-bind="text: name"></span>
<div data-bind="template: {foreach: clientAddress}">
<span data-bind="text: city"></span>
<span data-bind="text: state"></span>
</div>
<hr />
<ul data-bind="template: {foreach: projects}">
<li>
<span data-bind="text: name"></span>
<span data-bind="text: summary"></span>
<span data-bind="text: description"></span>
</li>
</ul>
</li>
I am quite certian you have a typo in your HTML. Here is a working example using ko.observablearray
HTML:
<form data-bind="submit: addItem">
prop1: <input data-bind='value: prop1, valueUpdate: "afterkeydown"' />
prop2: <input data-bind='value: prop2, valueUpdate: "afterkeydown"' />
<button type="submit">Add</button>
<p>Your items:</p>
<div data-bind="foreach: items">
<span data-bind="text: prop1"></span> &nbsp - &nbsp
<span data-bind="text: prop2"></span>
<br />
</div>
</form>
JS:
var SimpleListModel = function() {
this.items = ko.observableArray();
this.prop1 = ko.observable("");
this.prop2 = ko.observable("");
this.addItem = function() {
this.items.push({prop1:this.prop1, prop2: this.prop2});
}.bind(this); // Ensure that "this" is always this view model
};
ko.applyBindings(new SimpleListModel());
http://jsfiddle.net/NjSBg/2/
I suppose it's also possible you forgot to apply the bindings...
Edit
I appologize for posting the wrong fiddle, right one up now.
self.addClient = function () {
var client = {
UserId: self.userId(),
Name: self.name(),
Logo: self.logo()
}
lucidServer.addClient(ko.toJSON(client));
self.clients.push(client);
}.bind(self);
You add the parenthesis to get the current static value of the observable

Categories

Resources