Knockout Add and Edit observableArray Conflict - javascript

I have an observableArray which is displayed in a table using foreach binding where values are displayed inside textboxes. Now what I want to do is to add an edit link on each row which enables/disables the readonly state of the corresponding textbox in its row. I can do it, but the way I did it messed up my add new line (push) functionality.
Here is a fiddle of my code.
Try to delete a line then add it again by selecting it in the dropdown list, the edit link disappears as well as the value.
Any help will be greatly appreciated! Thank you.
So here's my HTML:
<table class="input-group" >
<tbody data-bind="foreach: loanDeductions">
<tr>
<td><strong data-bind='text: deductionName'></strong></td>
<td><input class="deductionCode form-control" style="text-align: right" data-bind='value: amount, valueUpdate: "afterkeydown", attr: { "readonly": getreadonlyState() }' /></td>
<td><a href='#' data-bind='click: $parent.removeLine'>Delete</a></td>
<td><span data-bind="text: linkText"></span></td>
</tr>
</tbody>
</table>
<table>
<tr>
<td colspan="3"><select data-bind="options: loanDeductionsList(), optionsText: 'deductionName', optionsCaption: 'Choose a deduction..', value: selectedDeduction"></select></td>
</tr>
</table>
Now here is my script:
var deductionLine = function (deductionID, deductionName, amount) {
self = this;
self.deductionID = ko.observable(deductionID);
self.deductionName = ko.observable(deductionName);
self.amount = ko.observable(formatCurrency(amount));
self.getreadonlyState = ko.observable('readonly');
self.linkText = ko.computed(function () {
return this.getreadonlyState() == 'readonly' ? "Edit" : "Stop Edit";
}, self);
};
var deductionList = function (deductionID, deductionName, amount) {
self = this;
self.deductionID = ko.observable(deductionID);
self.deductionName = ko.observable(deductionName);
self.amount = ko.observable(formatCurrency(amount));
};
function LoanDeductions(deductions) {
var self = this;
self.loanDeductions = ko.observableArray(ko.utils.arrayMap(deductions, function (deduction) {
return new deductionLine(deduction.deductionID, deduction.deductionName, deduction.amount)
}));
self.loanDeductionsList = ko.observableArray(ko.utils.arrayMap(deductions, function (deduction) {
return new deductionList(deduction.deductionID, deduction.deductionName, deduction.amount)
}));
self.selectedDeduction = ko.observable();
//edit link
self.readonly = function () {
if (BossBaU) {
if (this.getreadonlyState()) {
this.getreadonlyState(undefined);
}
else {
this.getreadonlyState('readonly');
}
}
else alert('Access denied!');
}
// adds deduction
self.selectedDeduction.subscribe(function (data) {
var match = ko.utils.arrayFirst(self.loanDeductions(), function (deduction) {
return deduction.deductionID() === data.deductionID();
});
if (match) {
alert(data.deductionName() + ' already exists!');
self.showAddDeduc(false);
} else {
self.loanDeductions.push({
deductionID: data.deductionID,
deductionName: data.deductionName,
amount: data.amount,
});
self.showAddDeduc(false);
}
});
//delete deduction
self.removeLine = function (line) { self.loanDeductions.remove(line) };
};
var viewModel = new LoanDeductions(#Html.Raw(Model.CRefLoansDeductions2.ToJson()));
$(document).ready(function () {
ko.applyBindings(viewModel);
});

In the subscribe handler, self.selectedDeduction.subscribe, you're adding an object to the list of loanDeductions when you should be adding a new instance of deductionLine just as you do when you declare self.loanDeductions.
Or to put it another way, self.loadDeductions is an observableArray of deductionLine instances, to which you then add an object with three properties.
Change that subscribe handler to push a new deductionLine(...) and you'll see the difference.

I found the cause of the problem, I had to mirror every changes I made with my observableArray to my list.
var deductionLine = function (deductionID, deductionName, amount) {
self = this;
self.deductionID = ko.observable(deductionID);
self.deductionName = ko.observable(deductionName);
self.amount = ko.observable(amount);
self.getreadonlyState = ko.observable('readonly');
self.linkText = ko.computed(function () {
return this.getreadonlyState() == 'readonly' ? "Edit" : "Stop Edit";
}, self);
};
var deductionList = function (deductionID, deductionName, amount) {
self = this;
self.deductionID = ko.observable(deductionID);
self.deductionName = ko.observable(deductionName);
self.amount = ko.observable(amount);
self.getreadonlyState = ko.observable('readonly');
self.linkText = ko.computed(function () {
return this.getreadonlyState() == 'readonly' ? "Edit" : "Stop Edit";
}, self);
};
Here's the fiddle in case anyone bump into a similar issue.

Related

How to access viewModel in Knockout component?

I am trying to preopulate a knockout component on document ready.
I've written the following code:
function Finding(id, trigger) {
var self = this;
self.id = ko.observable(id);
self.trigger = ko.observable(trigger);
}
function FindingViewModel() {
let self = this;
self.findings = ko.observableArray();
self.addFinding = function () {
self.findings.push(new Finding(self.findings().length + 1, ""));
};
self.removeFinding = function (finding) {
self.findings.remove(finding);
ko.utils.arrayForEach(self.findings(), function (value, i) {
self.findings.replace(value, new Finding(i + 1, value.trigger()));
});
};
self.update = function (data) {
var findings = data.findings;
for (var index = 0; index < findings.length; ++index) {
var finding = findings[index];
self.findings.push(new Finding(self.findings().length + 1, finding.trigger));
}
};
}
ko.components.register('finding', {
template: `<table>
<tbody data-bind="foreach: findings">
<tr>
<td><span data-bind="text: id"/></td>
<td><input data-bind="value: trigger"/></td>
<td>Remove</td>
</tr></tbody></table>
<button data-bind="click: addFinding">Add a Finding</button>`,
viewModel: FindingViewModel
});
$(function () {
ko.applyBindings();
$.getJSON("/_get_findings", function (data) {
//findingModel.update(data);
})
});
How can I access the underlying Viewmodel from the finding component to set data from inside the getJSON function?
thx to Jeroen the solution looks like this:
function FindingViewModel() {
let self = this;
self.findings = ko.observableArray();
self.addFinding = function () {
self.findings.push(new Finding(self.findings().length + 1, ""));
};
self.removeFinding = function (finding) {
self.findings.remove(finding);
ko.utils.arrayForEach(self.findings(), function (value, i) {
self.findings.replace(value, new Finding(i + 1, value.trigger()));
});
};
self.update = function (data) {
let findings = data.findings;
for (let index = 0; index < findings.length; ++index) {
let finding = findings[index];
self.findings.push(new Finding(self.findings().length + 1, finding.trigger));
}
};
$.getJSON("/_get_findings", function (data) {
self.update(data);
});
}

How to update an observable array inside a component from the main ViewModel?

I need to set from the main viewmodel a property inside an observable array.
I'm using the classic pre to debug and display the content of my observable array.
By using types.valueHasMutated() I can see the applied changes - just only to vm.types (which wouldn't be the case otherwise).
However, I need to see this changes reflected inside my component.
In my example, when I ckick "Apples", the corresponding input shall be disabled like the one below. Sadly, this is actually not the case.
What I'm doing wrong?
ko.components.register("available-items", {
viewModel: function(params) {
function AvailableItems(params) {
var self = this;
self.params = params;
self.location = "A";
self.types = ko.computed(function() {
var types = self.params.types();
return ko.utils.arrayFilter(types, function(item) {
return item.location == self.location;
});
});
self.addItem = function(data, event) {
self.params.items.addItem(self.location, data.type);
};
}
return new AvailableItems(params);
},
template: '<div>' +
'<h4>Add item</h4>' +
'<ul data-bind="foreach: types">' +
'<li>' +
'<input type="text" data-bind="value: type, enable:available, event: {click: $parent.addItem}" readonly/>' +
'</li>' +
'</ul>' +
'</div>',
synchronous: true
});
var types = [{
type: "Apples",
location: "A",
available: true
}, {
type: "Bananas",
location: "A",
available: false
}];
function Vm(data) {
var self = this;
self.items = ko.observableArray();
self.types = ko.observableArray(ko.utils.arrayMap(data, function(item) {
return item;
}));
self.items.addItem = function(location, type) {
self.items.push({
location: location,
type: type
});
if (location == "A" && type == "Apples") {
self.types()[0].available = false;
self.types.valueHasMutated();
}
};
}
ko.options.deferUpdates = true;
var vm = new Vm(types);
ko.applyBindings(vm);
pre {
position: absolute;
width: 300px;
right: 0;
top: 0;
}
<!DOCTYPE html>
<html>
<head>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
</head>
<body>
<div data-bind="component:{name:'available-items',params:vm}"></div>
<ul data-bind="foreach: items">
<li><span data-bind="text: location"></span> - <span data-bind="text: type"></span></li>
</ul>
<pre data-bind="text: ko.toJSON(vm.types, null, 2)"></pre>
</body>
</html>
I have run this on jfiddle and even when I added a new type, I wasn't getting any update.
It seems like there was an issue with the
'<ul data-bind="foreach: types">' +
I changed it to
'<ul data-bind="foreach: $root.types">' +
https://jsfiddle.net/fabwoofer/9szbqhj7/1/
Now the type gets added but it seems like the re-rendering of the first item is not handled. People with similar problems have suggested using template rendering as described here
Knockout.js Templates Foreach - force complete re-render
Hope this helps
Your available property isn't observable. In order to notify Knockout about changes and let it update UI - make this property observable.
ko.components.register("available-items", {
viewModel: function(params) {
function AvailableItems(params) {
var self = this;
self.params = params;
self.location = "A";
self.types = ko.computed(function() {
var types = self.params.types();
return ko.utils.arrayFilter(types, function(item) {
return item.location == self.location;
});
});
self.addItem = function(data, event) {
self.params.items.addItem(self.location, data.type);
};
}
return new AvailableItems(params);
},
template: '<div>' +
'<h4>Add item</h4>' +
'<ul data-bind="foreach: types">' +
'<li>' +
'<input type="text" data-bind="value: type, enable:available, event: {click: $parent.addItem}" readonly/>' +
'</li>' +
'</ul>' +
'</div>',
synchronous: true
});
var types = [{
type: "Apples",
location: "A",
// Make property observable
available: ko.observable(true)
}, {
type: "Bananas",
location: "A",
// Make property observable
available: ko.observable(false)
}];
function Vm(data) {
var self = this;
self.items = ko.observableArray();
self.types = ko.observableArray(ko.utils.arrayMap(data, function(item) {
return item;
}));
self.items.addItem = function(location, type) {
self.items.push({
location: location,
type: type,
available: ko.observable(false)
});
if (location == "A" && type == "Apples") {
// Update property as observable.
self.types()[0].available(false);
self.types.valueHasMutated();
}
};
}
ko.options.deferUpdates = true;
var vm = new Vm(types);
ko.applyBindings(vm);
pre {
position: absolute;
width: 300px;
right: 0;
top: 0;
}
<!DOCTYPE html>
<html>
<head>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
</head>
<body>
<div data-bind="component:{name:'available-items',params:vm}"></div>
<ul data-bind="foreach: items">
<li><span data-bind="text: location"></span> - <span data-bind="text: type"></span></li>
</ul>
<pre data-bind="text: ko.toJSON(vm.types, null, 2)"></pre>
</body>
</html>
You could use this solution given by the user JotaBe: Refresh observableArray when items are not observables.
ko.observableArray.fn.refresh = function (item) {
var index = this['indexOf'](item);
if (index >= 0) {
this.splice(index, 1);
this.splice(index, 0, item);
}
}
Now, I need to change addItem() and add the call to refresh with the updated element:
self.items.addItem = function(location, type) {
self.items.push({
location: location,
type: type
});
if (location == "A" && type == "Apples") {
self.types()[0].available = false;
self.types.refresh(self.types()[0]); // <--- New sentence
}
};
This will refresh the <pre>, that has a list of types. But will not refresh the component, that also has a list of types.
Then I used this link, Forcing deferred notifications to happen early, and I added ko.tasks.runEarly() in the refresh, and now it works ok, I think.
ko.observableArray.fn.refresh = function (item) {
var index = this['indexOf'](item);
if (index >= 0) {
this.splice(index, 1);
ko.tasks.runEarly(); // <--- New sentence
this.splice(index, 0, item);
}
}
Here it is a Codepen.
Inspired by implementation of pauseableComputed and observable withPausing I have created pauseableObservable and pauseableObservableArray that have abilities to stop notifications to subscribers and than resume when needed. Also it work recursively for all nested pauseable properties.
You can play with it HERE on Codepen (was provided example base on code from your question).
Also I place code of extensions that reaching the goal:
PauseableObservable:
// PauseableObservable - it's observable that have functions to 'pause' and 'resume' notifications to subscribers (pause/resume work recursive for all pauseable child).
ko.isPauseableObservable = function(instance) {
return ko.isObservable(instance) && instance.hasOwnProperty("pause");
}
ko.pauseableObservable = function(value) {
var that = ko.observable(value);
function getPauseableChildren() {
var properties = Object.getOwnPropertyNames(that());
var currentValue = that();
var pauseables = properties.filter((property) => {
return ko.isPauseableObservable(currentValue[property]);
});
return pauseables.map((property) => {
return currentValue[property];
});
}
that.pauseNotifications = false;
that.isDirty = false;
that.notifySubscribers = function() {
if (!that.pauseNotifications) {
ko.subscribable.fn.notifySubscribers.apply(that, arguments);
that.isDirty = false;
} else {
that.isDirty = true;
}
};
that.pause = function() {
that.pauseNotifications = true;
var pauseableChildren = getPauseableChildren();
pauseableChildren.forEach((child) => { child.pause(); });
}
that.resume = function() {
that.pauseNotifications = false;
if (that.isDirty) {
that.valueHasMutated();
}
var pauseableChildren = getPauseableChildren();
pauseableChildren.forEach((child)=> { child.resume(); });
}
return that;
}
PauseableObservableArray
// PauseableObservableArray - simply use pauseable functionality of his items.
// Extension for stop notifications about added/removed items is out of scope.
ko.pauseableObservableArray = function(items) {
var that = ko.observableArray(items);
that.pause = function () {
var items = that();
items.forEach(function(item) {
if(ko.isPauseableObservable(item)) {
item.pause();
}
});
}
that.resume = function () {
var items = that();
items.forEach(function(item) {
if(ko.isPauseableObservable(item)) {
item.resume();
}
});
}
that.refresh = function () {
that.resume();
that.pause();
}
return that;
}
Usage example:
var firstItem = ko.pauseableObservable("Hello");
var secondItem = ko.pauseableObservable("World");
var items = [
firstItem,
secondItem
];
var array = ko.pauseableObservable(items);
// Stop notifications from specific observable
firstItem.pause();
// Change won't raise notification to subscribers
firstItem("Hi");
// Resume notifications
firstItem.resume();
// Stop notifications from all items of array
array.pause();
// Change won't raise notification to subscribers
array()[0]("Hi");
// Resume notifications
array.resume();
I know that implementation isn't perfect and I haven't time to test it well, however I hope that it will help you to find inspiration and improve it.
I've found the problem. It's in your html: "params: vm" should be "params: $data"
<div data-bind="component:{name:'available-items',params:$data}"></div>

Applying filter to a observableArray?

I'm working on a project where I have a search field that filters an observableArray based on what is typed in the search box.
Here is my html code:
<input class="form-control" placeholder="Search for a burger joint around downtown Indianapolis, IN" data-bind="value: searchInput, valueUpdate: 'afterkeydown', event: { keyup: filterResults }">
Here is my js code:
var BurgerJoint = function(data) {
this.id = ko.observable(data.id);
this.name = ko.observable(data.name);
this.long = ko.observable(data.long);
this.lat = ko.observable(data.lat);
this.comments = ko.observable(data.comments);
}
var ViewModel = function() {
var self = this;
self.searchInput = ko.observable('');
this.burgerList = ko.observableArray([]);
initialBurgerJoints.forEach(function(burgerItem){
self.burgerList.push( new BurgerJoint(burgerItem) );
addmarker(burgerItem.lat, burgerItem.long, burgerItem.id, burgerItem.name, burgerItem.comments);
});
self.burgerList.sort(function (l, r) { return l.name() > r.name() ? 1 : -1 });
self.currentFilter = ko.observable();
self.filterResults = function(){
var value = self.searchInput().toLowerCase();
if(value != ''){
self.burgerList(self.burgerList.filter(function(data){
var startsWith = data.name.toLowerCase().startsWith(value);
return burgerList;
}));
}
return true;
};
self.showmap = function(data) {
viewmarker(data.id());
};
}
ko.applyBindings(new ViewModel());
I'm having issues with the filterResults function. Any suggestions?
Maybe you shouldn't set your original burgerList in filter function. Make a copy/clone of the original first. Try something like
self.filterResults = function(){
var value = self.searchInput().toLowerCase();
if(value != ''){
self.burgerList(self.burgerList().filter(function(data){
return data.name.toLowerCase().startsWith(value);
}));
}else{
// reset with the original burgerList contents here
self.burgerList(self.burgerListClone());
}
};

Knockout - Filtering Objects in Observable Arrays by multiple Object Properties

So here it is... I am attempting to build a data-grid with Knockout.js. I want to build it from scratch (skill building exercise), so I don't want to use KoGrid or SimpleGrid.
The issue I am having is that I want to be able to filter the results based on a text input. This filter has to go through each object and filter on ONLY the keys that match a column's value property.
Example
Filtering with the value '1' will return both of data's objects with properties that contain 1. (ID 1 and ID 3).
JSFIDDLE
HTML
<div data-bind="foreach: filteredItems">
<p data-bind="text: LastName"></p>
</div>
<p>Filter:
<input data-bind="value: filter, valueUpdate: 'keyup'" />
</p>
<p>Filter Value: <span data-bind="text: filter"></span></p>
JavaScript
var data = [{
Id: 1,
LastName: "Franklin"
}, {
Id: 2,
LastName: "Green"
}, {
Id: 3,
LastName: "1"
}];
var columns = [{
value: 'Id'
}, {
value: 'LastName'
}];
var Filtering = function (data, columns) {
var self = this;
self.items = ko.observableArray(data);
self.columns = ko.observableArray(columns);
self.filter = ko.observable();
self.filteredItems = ko.computed(function () {
var filter = self.filter();
console.log(filter);
if (!filter) {
return self.items();
} else {
return ko.utils.arrayFilter(self.items(), function (item) {
console.log('Filtering on Item');
ko.utils.arrayForEach(self.columns(), function (c) {
var val = item[c.value];
if (typeof val === 'number') {
val = val.toString();
}
console.log('Filtering on Column');
return val.toLowerCase().indexOf(filter.toLowerCase()) > -1;
});
});
}
});
};
ko.applyBindings(new Filtering(data, columns));
It works great statically setting the c.value in item[c.value], but when I try to loop through the array of self.columns() I do not get results returned.
Recs
I have jQuery, Knockout.js 3.0, and underscore.js at my disposal.
Any help is greatly appreciated.
Just few problems in your code :
a Boolean needs to be returned as output of ko.utils.arrayFilter
you need a sort of sum up for arrayForEach as your filter is an OR
I changed your code to address those details :
var Filtering = function (data, columns) {
var self = this;
self.items = ko.observableArray(data);
self.columns = ko.observableArray(columns);
self.filter = ko.observable();
self.filteredItems = ko.computed(function () {
var filter = self.filter();
console.log(filter);
if (!filter) {
return self.items();
} else {
return ko.utils.arrayFilter(self.items(), function (item) {
console.log('Filtering on Item');
var matching = -1;
ko.utils.arrayForEach(self.columns(), function (c) {
var val = item[c.value];
if (typeof val === 'number') {
val = val.toString();
}
console.log('Filtering on Column');
matching+= val.toLowerCase().indexOf(filter.toLowerCase())+1;
});
console.log(matching);
return matching>=0;
});
}
});
};
Worked for me there : http://jsfiddle.net/Lzud7fjr/1/
Your filtering case isn't returning anything. ko.utils.arrayFilter() should return a truthy value if the item should be included in the result. However since it doesn't return anything, nothing is included in the result. You'll need to rewrite it so it will return something.
I suppose you could change the inner ko.utils.arrayForEach() call to a filter and return true if the result is non-empty.
self.filteredItems = ko.computed(function () {
var filter = self.filter();
console.log(filter);
if (!filter) {
return self.items();
} else {
return ko.utils.arrayFilter(self.items(), function (item) {
console.log('Filtering on Item');
var result = ko.utils.arrayFilter(self.columns(), function (c) {
var val = item[c.value];
if (typeof val === 'number') {
val = val.toString();
}
console.log('Filtering on Column');
return val.toLowerCase().indexOf(filter.toLowerCase()) > -1;
});
return !!result.length;
});
}
});

Knockout JS setting optionsValue destroys my code

The code below is simplified, see the fiddle: http://jsfiddle.net/QTUqD/7/
Basically I'm setting the device name under the data-bind, but I also need to specify the optionsValue for sending off to the database, but when I set it, the display data-bind is blank.
<script id="extItems" type="text/html">
<tr>
<td data-bind="text: device() && device().name"></td>
</tr>
</script>
<script id="editExts" type="text/html">
<tr>
<td>
<select data-bind="options: $root.devicesForItem($data), optionsText: 'name', value: device, optionsValue: 'id'"></select>
</td>
</tr>
</script>
window.ExtListViewModel = new function () {
var self = this;
window.viewModel = self;
self.list = ko.observableArray();
self.pageSize = ko.observable(10);
self.pageIndex = ko.observable(0);
self.selectedItem = ko.observable();
self.extQty = ko.observable();
self.devices = ko.observableArray();
self.addressList = ko.observableArray(['addressList']);
self.availableDevices = ko.computed(function() {
var usedQuantities = {}; // for each device id, store the used quantity
self.list().forEach(function(item) {
var device = item.device();
if (device) {
usedQuantities[device.id] = 1 + (usedQuantities[device.id] || 0);
}
});
return self.devices().filter(function(device) {
var usedQuantity = usedQuantities[device.id] || 0;
return device.qty > usedQuantity;
});
});
// need this to add back item's selected device to its device-options,
// and to maintain original order of devices
self.devicesForItem = function(item) {
var availableDevices = self.availableDevices();
return self.devices().filter(function(device) {
return device === item.device() || availableDevices.indexOf(device) !== -1;
});
}
self.edit = function (item) {
if($('#extMngForm').valid()) {
self.selectedItem(item);
}
};
self.cancel = function () {
self.selectedItem(null);
};
self.add = function () {
if($('#extMngForm').valid()) {
var newItem = new Extension();
self.list.push(newItem);
self.selectedItem(newItem);
self.moveToPage(self.maxPageIndex());
}
};
self.remove = function (item) {
if (confirm('Are you sure you wish to delete this item?')) {
self.list.remove(item);
if (self.pageIndex() > self.maxPageIndex()) {
self.moveToPage(self.maxPageIndex());
}
}
$('.error').hide();
};
self.save = function () {
if($('#extMngForm').valid()) {
self.selectedItem(null);
};
};
self.templateToUse = function (item) {
return self.selectedItem() === item ? 'editExts' : 'extItems';
};
self.pagedList = ko.dependentObservable(function () {
var size = self.pageSize();
var start = self.pageIndex() * size;
return self.list.slice(start, start + size);
});
self.maxPageIndex = ko.dependentObservable(function () {
return Math.ceil(self.list().length / self.pageSize()) - 1;
});
self.previousPage = function () {
if (self.pageIndex() > 0) {
self.pageIndex(self.pageIndex() - 1);
}
};
self.nextPage = function () {
if (self.pageIndex() < self.maxPageIndex()) {
self.pageIndex(self.pageIndex() + 1);
}
};
self.allPages = ko.dependentObservable(function () {
var pages = [];
for (i = 0; i <= self.maxPageIndex() ; i++) {
pages.push({ pageNumber: (i + 1) });
}
return pages;
});
self.moveToPage = function (index) {
self.pageIndex(index);
};
};
ko.applyBindings(ExtListViewModel, document.getElementById('extMngForm'));
function Extension(extension, name, email, vmpin, device, macAddress, shipTo){
this.extension = ko.observable(extension);
this.name = ko.observable(name);
this.email = ko.observable(email);
this.vmpin = ko.observable(vmpin);
this.device = ko.observable(device);
this.macAddress = ko.observable(macAddress);
this.shipTo = ko.observable(shipTo);
}
When you use optionsValue, KO writes the property value to whatever you have bound against value. So, it would now populate value with the id rather than the object.
There are a couple of ways to tackle this scenario where you want both the value (for sending to the DB) and the object (for binding other parts of the UI against).
A pretty typical solution is to create a computed observable on your object that takes the currently selected object and returns the id.
So, in your Extension you would do something like:
this.device = ko.computed({
read: function() {
var device = this.device.asObject();
return device && device.id;
},
deferEvaluation: true, //deferring evaluation, as device.asObject has not been created yet
}, this);
//create this as a sub-observable, so it just disappears when we turn this into JSON and we are just left with the id to send to the DB
this.device.asObject = ko.observable(device);
Then remove the optionsValue and bind value against device.asObject
In this case, I added the asObject sub-observable, so it will just drop off when you turn this into JSON (ko.toJSON) to send to the server. The only tricky part about this technique is that if you are loading existing data from the server, then you would need to populate asObject with the appropriate choice from your options.
Here is a sample: http://jsfiddle.net/rniemeyer/Q3PEv/
Another option that I have used is to continue to use optionsValue, but then to create a custom binding that tracks the object in a separate observable. Here is a custom binding that creates an asObject sub-observable for whatever is bound against value. This way you really don't need to mess with it at all in your view model.
//when using optionsValue, still track the select object in a different observable
ko.bindingHandlers.valueAsObject = {
init: function(element, valueAccessor, allBindingsAccessor) {
var value = allBindingsAccessor().value,
prop = valueAccessor() || 'asObject';
//add an "asObject" sub-observable to the observable bound against "value"
if (ko.isObservable(value) && !value[prop]) {
value[prop] = ko.observable();
}
},
//whenever the value or options are updated, populated the "asObject" observable
update: function(element, valueAccessor, allBindingsAccessor) {
var prop = valueAccessor(),
all = allBindingsAccessor(),
options = ko.utils.unwrapObservable(all.options),
value = all.value,
key = ko.utils.unwrapObservable(value),
keyProp = all.optionsValue,
//loop through the options, find a match based on the current "value"
match = ko.utils.arrayFirst(options, function(option) {
return option[keyProp] === key;
});
//set the "asObject" observable to our match
value[prop](match);
}
};
Sample here: http://jsfiddle.net/rniemeyer/E2kvM/

Categories

Resources