Why does setting an optionsValue break Knockout updating? - javascript

I've been going through the Knockout tutorials, and I was playing around with one tutorial when something puzzled me. Here is my HTML:
<h2>Your seat reservations</h2>
<table>
<thead><tr>
<th>Passenger name</th><th>Meal</th><th>Surcharge</th>
</tr></thead>
<tbody data-bind="foreach: seats">
<tr>
<td><input data-bind="value: name" /></td>
<td><select data-bind="options: $root.availableMeals, optionsValue: 'mealVal', optionsText: 'mealName', value: meal"></select></td>
<td data-bind="text: formattedPrice"></td>
</tr>
</tbody>
</table>
<button data-bind="click: addSeat">Reserve another seat</button>
... and here is my JavaScript:
// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
var self = this;
self.name = name;
self.meal = ko.observable(initialMeal);
self.formattedPrice = ko.computed(function() {
var price = self.meal().price;
return price ? "$" + price.toFixed(2) : "None";
});
}
// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
var self = this;
// Non-editable catalog data - would come from the server
self.availableMeals = [
{ mealVal: "STD", mealName: "Standard (sandwich)", price: 0 },
{ mealVal: "PRM", mealName: "Premium (lobster)", price: 34.95 },
{ mealVal: "ULT", mealName: "Ultimate (whole zebra)", price: 290 }
];
// Editable data
self.seats = ko.observableArray([
new SeatReservation("Steve", self.availableMeals[0]),
new SeatReservation("Bert", self.availableMeals[0])
]);
// Operations
self.addSeat = function() {
self.seats.push(new SeatReservation("", self.availableMeals[0]));
}
}
ko.applyBindings(new ReservationsViewModel());
When I run this example and select a different "Meal" from the dropdown menu for a passenger, the "Surcharge" value is not updated. The reason for this seems to be that I added optionsValue: 'mealVal' into the data-bind attribute for the select, and when I remove that, the "Surcharge" does indeed update when a new dropdown option is selected. But why does adding optionsValue break the updating? All that does is set the select list's option value attributes, which is quite useful for form submission - I don't see why it should prevent Knockout from auto-updating.
UPDATE: Upon further investigation, I've discovered that the formattedPrice fn is still getting called, but self.meal() is now resolving to the value string such as PRM instead of the whole meal object. But why is this? The documentation says that optionsValue sets the value attribute in the HTML, but doesn't say anything about changing the view model behaviour.
I think what's going on is that when you specify options: $root.availableMeals, but don't specify an optionsValue, Knockout magically determines which selection in the list you've made when the selection is changed and gives you access to the object from availableMeals instead of just the string value that was put into the value attribute. This does not appear to be well-documented.

I think you understand what's happening and why it breaks your code, but are still looking for an explanation on when you actually need to use optionsValue, and when not.
When to use the optionsValue binding
Let's say your meals can be sold out and you want to check with the server for updates in availableMeals:
const availableMeals = ko.observableArray([]);
const loadMeals = () => getMeals().then(availableMeals);
const selectedMeal = ko.observable(null);
loadMeals();
ko.applyBindings({ loadMeals, availableMeals, selectedMeal });
function getMeals() {
return {
then: function(cb) {
setTimeout(cb.bind(null, [{ mealVal: "STD", mealName: "Standard (sandwich)", price: 0 }, { mealVal: "PRM", mealName: "Premium (lobster)", price: 34.95 }, { mealVal: "ULT", mealName: "Ultimate (whole zebra)", price: 290 }]), 500);
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<select data-bind="options: availableMeals,
value: selectedMeal,
optionsText: 'mealName'"></select>
<button data-bind="click: loadMeals">refresh meals</button>
<div data-bind="with: selectedMeal">
You've selected <em data-bind="text: mealName"></em>
</div>
<div data-bind="ifnot: selectedMeal">No selection</div>
<p>Make a selection, click on refresh and notice the selection is lost when new data arrives.</p>
What happens when you replace the objects in availableMeals:
Knockout re-renders the select box's options
Knockout checks the new values for selectedMeal() === mealObject
Knockout does not find the object in selectedMeal and defaults to the first option
Knockout writes the new object's reference to selectedMeal
Problem: you loose your UI selection because the object it points to is no longer in the available options.
optionsValue to the rescue!
The optionsValue allows us to solve this issue. Instead of storing a reference to an object that might be replaced at any time, we store a primitive value, the string inside mealVal, that allows us to check for equality in between different API calls! Knockout now does something like:
selection = newObjects.find(o => o["mealVal"] === selectedMeal());
Let's see this in action:
const availableMeals = ko.observableArray([]);
const loadMeals = () => getMeals().then(availableMeals);
const selectedMeal = ko.observable(null);
loadMeals();
ko.applyBindings({ loadMeals, availableMeals, selectedMeal });
function getMeals() {
return {
then: function(cb) {
setTimeout(cb.bind(null, [{ mealVal: "STD", mealName: "Standard (sandwich)", price: 0 }, { mealVal: "PRM", mealName: "Premium (lobster)", price: 34.95 }, { mealVal: "ULT", mealName: "Ultimate (whole zebra)", price: 290 }]), 500);
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<select data-bind="options: availableMeals,
value: selectedMeal,
optionsText: 'mealName',
optionsValue: 'mealVal'"></select>
<button data-bind="click: loadMeals">refresh meals</button>
<div data-bind="if: selectedMeal">
You've selected <em data-bind="text: selectedMeal"></em>
</div>
<div data-bind="ifnot: selectedMeal">No selection</div>
<p>Make a selection, click on refresh and notice the selection is lost when new data arrives.</p>
The downsides of optionsValue
Notice how I had to rewrite the with binding? Suddenly, we only have one of meal's properties available in our viewmodel, which is quite limiting. Here's where you'll have to do some additional work if you want your app to be able to update its data. Your two options:
Store the string (hash) of your selection and the actual object independently, or
Have a repository of view models, when new server data arrives, map to the existing instances to ensure you keep selection states.
If it helps, I could add code snippets to explain those two approaches a bit better

OK, after looking through the Knockout code, I've figured out what's happening - and as of the time of writing this is not documented.
The value binding, when it reads the value of a select element, doesn't just look at the DOM value for the element; it calls var elementValue = ko.selectExtensions.readValue(element);
Now, what selectExtensions does, unsurprisingly, is implement special behaviour for select (and their child object) elements. This is where the magic happens, because as the comment in the code says:
// Normally, SELECT elements and their OPTIONs can only take value of type 'string' (because the values
// are stored on DOM attributes). ko.selectExtensions provides a way for SELECTs/OPTIONs to have values
// that are arbitrary objects. This is very convenient when implementing things like cascading dropdowns.
So, when the value binding tries to read the select element via selectExtensions.readValue(...), it will come to this code:
case 'select':
return element.selectedIndex >= 0 ? ko.selectExtensions.readValue(element.options[element.selectedIndex]) : undefined;
This basically says "OK, find the selected index and use this function again to read the option element at that index. So then it reads the option element and comes to this:
case 'option':
if (element[hasDomDataExpandoProperty] === true)
return ko.utils.domData.get(element, ko.bindingHandlers.options.optionValueDomDataKey);
return ko.utils.ieVersion <= 7
? (element.getAttributeNode('value') && element.getAttributeNode('value').specified ? element.value : element.text)
: element.value;
Aha! So it stores its own "has DOM data expando property" flag and if that is set it DOESN'T get the simple element.value, but it goes to its own JavaScript memory and gets the value. This is how it can return a complex JS object (like the meal object in my question's example) instead of just the value attribute string. However, if that flag is not set, it does indeed just return the value attribute string.
The writeValue extension, predictably, has the other side of this where it will write the complex data to JS memory if it's not a string, but otherwise it will just store it in the value attribute string for the option:
switch (ko.utils.tagNameLower(element)) {
case 'option':
if (typeof value === "string") {
ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, undefined);
if (hasDomDataExpandoProperty in element) { // IE <= 8 throws errors if you delete non-existent properties from a DOM node
delete element[hasDomDataExpandoProperty];
}
element.value = value;
}
else {
// Store arbitrary object using DomData
ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, value);
element[hasDomDataExpandoProperty] = true;
// Special treatment of numbers is just for backward compatibility. KO 1.2.1 wrote numerical values to element.value.
element.value = typeof value === "number" ? value : "";
}
break;
So yeah, as I suspected, Knockout is storing complex data behind-the-scenes but only when you ask it to store a complex JS object. This explains why, when you don't specify optionsValue: [someStringValue], your computed function received the complex meal object, whereas when you do specify it, you just get the basic string passed in - Knockout is just giving you the string from the option's value attribute.
Personally I think this should be CLEARLY documented because it is a bit unexpected and special behaviour that is potentially confusing, even if it's convenient. I'll be asking them to add it to the documentation.

Related

Angular ng-options selected object

why is this not working?
HTML:
<div ng-controller="MyCtrl">
<select class="rp-admin-rooms-selected-select" name="teacherSelect" id="teacher"
ng-model="teacherSel"
ng-options="teacher.name for teacher in teachers "
>
</select>
{{teacherSel}}
</div>
JS:
var myApp = angular.module('myApp',[]);
//myApp.directive('myDirective', function() {});
//myApp.factory('myService', function() {});
function MyCtrl($scope) {
$scope.teachers = [
{name:'Facundo', id:1},
{name:'Jorge', id:3},
{name:'Humberto', id:5},
{name:'David', id:7},
]
$scope.teacherSel = {name:'Facundo', id:1};
}
I would expect to be the selected element be Facundo
The thing is, I know that its possible to do this via teacherSel = id
and ng-options="teacher.name as teacher.id"...
But I have the object yet their, and I need the new object. not just the id.
jsfiddle:
http://jsfiddle.net/Lngv0r9k/
Michael Rose got it right. Another option would be force angular to do the comparison by value using a track by statement, as follows:
ng-options="teacher.name for teacher in teachers track by teacher.ID"
This works in angular 1.2 and after.
Updated fiddle: http://jsfiddle.net/Lngv0r9k/3/.
The issue is that the comparison for the selected entry is done via reference, not value. Since your $scope.teacherSel is a different object than the one inside the array - it will never be the selected one.
Therefore you need to find the selected entry inside the array and then use this as follows: $scope.teacherSel = $scope.teachers[indexOfSelectedEntry].
See at the bottom of your updated jsfiddle: http://jsfiddle.net/Lngv0r9k/1/
On your example you dont give a teacher object to the room.teacher , so the ng-options cant match anything to the ng-model.
As you see on the screen below, value=? means that it cant find correct value to match up.
you could try for example:
$scope.room = {
teacher: $scope.teachers[an item index];
};
OR
$scope.room = {
teacher: {
"ID": "1",
"name": "Adolf Ingobert",
"short": "AD",
"display": "Adolf I.",
"sectionFK": "2",
"invisible": "0"
};
};

Group results in autocompleted dropdown [Meteor]

I try to do a dropdown list in my app. First of all I use a Meteor, so that's specific kind of app ofc :)
Second thing is that I use sebdah/meteor-autocompletion package, because I want my results to be sorted in specific way and limited.
The last thing I need is to group my results.
For example: If I have 2 products named "blah" I want to get only 1 "blag" in my dropdown "autocompletion" list.
Some code:
HTML:
<template name="InvoicesEditInsertInsertForm">
<input id="descriptionautocomplete" type="text" name="description" value="" class="form-control" autofocus="autofocus" placeholder="New Item...">
</template>
JS:
Template.InvoicesEditInsertInsertForm.rendered = function() {
AutoCompletion.init("input#descriptionautocomplete");
};
Template.InvoicesEditInsertInsertForm.events({
'keyup input#descriptionautocomplete': function () {
AutoCompletion.autocomplete({
element: 'input#descriptionautocomplete', // DOM identifier for the element
collection: InvoicesItem, // MeteorJS collection object
field: 'description', // Document field name to search for
limit: 5, // Max number of elements to show
sort: { modifiedAt: -1 },
}); // Sort object to filter results with
},
});
I need to use function that could group my "description" here.
I tried to do it in helper and I get it on my screen, but to be honest I don't know how to put that into my dropdown :(
try: function() {
var item= InvoicesItem.find({},{sort:{modifiedAt:-1}}).fetch();
var descriptions={};
_.each(item,function(row){
var description = row.description;
if(descriptions[description]==null)
descriptions[description]={description:description};
});
return _.values(descriptions);
},
I don't think you can do what you want with that package. If you have a look at the current limitations of the package documentation, you can see other potential solutions to your problem.
You can do addtional filtering as follows:
filter: { 'gender': 'female' }});
but I don't think this will allow you to demand only unique options.
The code you wrote above for try won't do anything. Autocomplete doesn't take a field called try.

In Angular models, can you switch value type between JSON Object and String?

I have an application at work that I am working on that requires me to display various fields based on the value of the associated rule.itemType. The issue I am coming across is that I am unable to modify the model data in an ng-repeat if the previous set value in rule.value was a String object that now is displaying fields that require an Object. When I try to assign new values it returns: TypeError: Cannot assign to read only property 'course' of ABC123.
I did find that if the value was an Object it would display it as a String of [object Object], which I am assuming comes from the Object.prototype.toString() function I was reading about, and if changed will replace rule.value with a new String object. Even though this direction works, if I am needing to do a String to Object we end up back at the above mentioned issue.
I have attached sample code to demonstrate what I am trying to do below plus some data. I also forked, modified, and linked a replier's Plunker so you can see it in action:
http://plnkr.co/edit/v4gSyc6MbYeGGyJppvnc?p=preview
JavaScript:
var app = angular.module('plunker', []);
app.controller('appCtrl', function ($scope) {
$scope.rules = [
{id: 1, itemType: 'CS', value: {course: 'ABC', number: '123'}},
{id: 2, itemType: 'SA', value: 'ABC123'}
];
});
HTML, with Angular v1.3.0:
<body>
<div ng-controller="appCtrl as app">
<div ng-repeat="rule in rules">
Rule {{$index+1}}
<!-- Change the itemType for the rule -->
<select ng-model="rule.itemType">
<option value="SA">String</option>
<option value="CS">Object</option>
</select>
<!-- Fields shown if rule.itemType (SA: String, CS: Object) -->
<input type="text" ng-if="rule.itemType === 'SA'" ng-model="rule.value" />
<span ng-if="rule.itemType === 'CS'">
<input ng-model="rule.value.course" />
<input ng-model="rule.value.number" />
</span>
</div>
</div>
</body>
Update 1:
Added the suggestion from Ryan Randall to use ng-change='changeType(rule) which provides the appropriate behavior I was looking for, example plunker below.
http://plnkr.co/edit/bFahZj?p=preview
JavaScript Changes:
// Contributed by: http://stackoverflow.com/a/26702691/949704
$scope.typeChange = function (rule) {
if (rule.itemType === 'CS') rule.value = {};
if (rule.itemType === 'SA') rule.value = '';
};
HTML Changes:
<!-- Change the itemType for the rule -->
<select ng-model="rule.itemType" ng-change="changeType(rule)">
<option value="SA">String</option>
<option value="CS">Object</option>
</select>
One way to avoid the issue you're having is to explicitly set rule.value when rule.itemType changes.
Here's a working plunker containing the tweaks: http://plnkr.co/edit/rMfeBe?p=preview
The following has been added to the select:
ng-change="typeChange(rule)"
And the following has been added to the controller:
$scope.typeChange = function(rule) {
if (rule.itemType === 'CS') rule.value = {};
if (rule.itemType === 'SA') rule.value = '';
};
Don't really understand your question mate... Can you please specify exactly what you did to get that error because it's not happening to me.. (maybe also the browser you're on)
Seems to work fine, see below plunkr
'http://plnkr.co/edit/04huDKRSIr2YLtjItZRK?p=preview'

Options Binding overrides initial View Model value

I tried to follow the other similar questions on Stack Overflow but thus far have been unsuccessful in fixing the problem.
I am using jQuery AJAX to retrieve several items: a contact and its associated information, all available salutation types, all available email types and all available phone types.
I have successfully bound the options to the select boxes. However, it appears to overwrite the 'value' binding that holds the initial view model value.
Could any of you help me solve this? Please let me know if you have any questions for clarification.
Please see the code below:
View Model:
function contactPageViewModel() {
var self = this;
self.contact = ko.observable();
self.availableSalutations = ko.observableArray();
self.availableEmailTypes = ko.observableArray();
self.availablePhoneTypes = ko.observableArray();
self.availableAddressTypes = ko.observableArray();
}
where contact is an object coming from the server, which includes the element contact.salutation.
The json coming back for contact is:
{
//...
"createdBy":null,
"createdOn":1392848929000,
"updatedBy":null,
"updatedOn":1392848929000,
"contactId":305,
"salutation":{"salutationId":102,"salutation":"Mrs."},
"firstName":"Laura",
"middleInitial":"K",
"lastName":"Ritchey"
//...
}
the json coming back from availableSalutations (which is a property of a json object wrapper 'listObject') is:
[{"salutationId":41,"salutation":"Ms."},
{"salutationId":101,"salutation":"Mr."},
{"salutationId":66,"salutation":"CDR"},
{"salutationId":81,"salutation":"LCDR"},
{"salutationId":102,"salutation":"Mrs."},
{"salutationId":121,"salutation":"Mr."},
{"salutationId":64,"salutation":"LTC"}]
The code to map the JSON result to the knockout observables:
contactPageViewModel.contact = ko.mapping.fromJS(data.listObject[0]);
contactPageViewModel.availableEmailTypes = ko.mapping
.fromJS(data.listObject[1]);
contactPageViewModel.availableSalutations = ko.mapping
.fromJS(data.listObject[2]);
....
applyBindings();
The HTML:
<label for="rank"> Rank / Title: </label>
<select data-bind="optionsText: 'salutation',
options: availableSalutations,
value: contactPageViewModel.contact.salutation"
class="rankList"
name="Rank"
id="rankSelect">
</select>
Try value: $root.contact().salutation instead of value: contactPageViewModel.contact.salutation.
Or:
<label for="rank"> Rank / Title: </label>
<!-- ko with: contact -->
<select data-bind="options: $root.availableSalutations, optionsText: 'salutation', value: salutation" class="rankList" name="Rank" id="rankSelect">
</select>
<!-- /ko -->
Update:
You could look at this Fiddle. May be it contains a lot of excess code and you can simplify it, but the main things is to separate initial and selected salutations and add optionsCaption to select bindings:
var initialSalutation = new salutationViewModel(data.salutation);
And:
self.salutation = ko.observable();
self.displayedSalutation = ko.computed(function () {
if (self.salutation()) {
return self.salutation();
} else {
return initialSalutation;
}
})
Update 2:
Look at this Fiddle. I've added optionsValue: 'salutationId' to select bindings and move displayedSalutation to contactPageViewModel.
I think problem was with matching objects (select item and salutation from contact). When value of select is salutationId and contact salutation also salutationId (number value, not object) all working good.

Knockout - strategies to get and set selectbox values in templates, when sharing an observableArray?

I have just started experimenting with JavaScript, JQuery and Knockout.js (I'm normally working on the server-side in Java) and it's beyond brilliant - however I have hit a wall, and I'm extremely doubtful that I'm doing, whatever I'm trying to do, 'by the book'.
I have created a fiddle for it here: http://jsfiddle.net/kcyhw/
My mission:
I need to create a template that can be re-used all over the website. Fx. a template that contains the possibility to create, edit and delete users - which would make it easy to create users in a "User configuration"-window, or as a part of a wizard. Either way, the logic controlling everything should be the same. However, besides sharing the same arrayObservable for data, the selection choices should of course not observe each other. Right now, it's entirely a select box.
I'm using JQuery.serialize to convert an entire form to key-value, to be sent to a server, so it's important that I not only get the value, but also have it "saved" in the value attribute on the select box.
My problem:
I simply can't figure out how Knockout.js and the select box is connected. All the objects are displayed fine by their respective values (id and fullname), both in the select box, and in the properties section.
When using serialize with jQuery it just prints: "perselect="... so it doesn't get a value.
I tried the following:
Using optionValue in the data-bind - it works, and it binds to the "value", however, I can see that it "takes over" my binding, and kills the text I retrieve from the object. I removed it, and continued...
Computed values, however it didn't work out since the template wants (after my knowledge) a literal object, and functions can't reference other properties in such an object.
Created my own binding, so I could get a reference to both the element (the select box) and all the other binding values. In this function I tried, using jQuery, to set the attribute 'value' on the element which was passed in, however, it doesn't work.
I can also see, that the binding gets called 4 times (that probably because it calls init and then update for each template I created which contains the select box).
To me, it looks like I have created a friggin' mess, and I would really appreciate if some smart people could point me into the right direction of how to solve this. Resources, code-snippets, advice... whatever you got.
The code:
<html>
<head>
<script src="javascript/jquery-1.10.2/jquery-1.10.2.js"></script>
<script src="javascript/knockout-2.3.0/knockout-2.3.0.js"></script>
<script src="javascript/knockout.mapping-master-2.0/knockout.mapping-latest.js"></script>
<script type="text/javascript" src="javascript/json2-2.0/json2.js"></script>
<title>A Knockout Demo</title>
<script>
/**
* JQuery Function
*/
$(document).ready(function() {
// Domain Object
var Person = function(id, fullname) {
var self = this;
self.id = id;
self.fullname = fullname;
};
// Knockout Model
var KoModel = function() {
var self = this;
// Declare observables
self.persons = ko.observableArray();
// Allows observables to share an array without observing each other
self.createPersonSelector = function(namevalue) {
var person = new Object();
person.selectedPerson = ko.observable();
person.name = namevalue;
return person;
}
// Prints a serialized string which could be sent to the server
self.printFormElements = function(formElements) {
alert($(formElements).serialize());
}
// Will change the person select value, to a real value
self.changePersonSelectValue = function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var value = valueAccessor(), allBindings = allBindingsAccessor();
// Next, whether or not the supplied model property is observable, get its current value
var valueUnwrapped = ko.unwrap(value);
// Now manipulate the DOM element
var $eleme = $(element);
if ($eleme == null) {
return;
}
// Change to item number two in the list *doesn't work*.
$eleme.val(2);
};
// Person selectbox value binding
ko.bindingHandlers.personSelect = {
init : function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
self.changePersonSelectValue(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
},
update : function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
self.changePersonSelectValue(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
}
};
// Put some test-data into the array
self.persons.push(new Person(1, 'Martin Smith'));
self.persons.push(new Person(2, 'Andy Gregersen'));
self.persons.push(new Person(3, 'Thomas Peep'));
};
// Apply bindings
ko.applyBindings(new KoModel());
});
</script>
<script type="text/html" id="person-template">
<span>Choose ID: </span><select data-bind="options: $root.persons, optionsText: 'id', personSelect: true, value:selectedPerson, attr: {'name': name, 'id': name}"></select></br>
<span>ID:</span> <span data-bind="text: selectedPerson().id"></span></br>
<span>Full Name: </span> <span data-bind="text: selectedPerson().fullname"></span></br>
</script>
<body>
<h1>Person Select One</h1>
<form data-bind="submit: printFormElements">
<div
data-bind="template: { name: 'person-template', data:createPersonSelector('personselect')}"></div>
<button type="submit">Submit</button>
</br>
</form>
<h1>Person Select Two</h1>
<form data-bind="submit: printFormElements">
<div
data-bind="template: { name: 'person-template', data:createPersonSelector('personselecttwo')}"></div>
<button type="submit">Submit</button>
</br>
</form>
</body>
</html>
The easiest way for me to answer is by changing quite a few things that I might do differently. The main issue that's holding you back is that you're using jQuery for things that can be handled by KO in a much easier way.
Below are the things I'd change, to see the full result have a look at this fiddle (which doesn't use jQuery at all).
Simplify your model to something like this:
var KoModel = function() {
var self = this;
// Declare observables
self.persons = ko.observableArray();
// Prints a serialized string which could be sent to the server
self.printFormElements = function() {
alert(ko.mapping.toJSON(self));
}
// Hold selected person
self.selectedPersons = ko.observableArray();
};
A few things to note:
The "print" function now uses the mapping plugin, which is great for serializing view models;
It's much, much shorter. The "CreatePersonSelector" and "changePersonSelectValue" functions won't be needed anymore, nor do the custom bindings;
The selectedPersons is now an observable, and an array at that because the view could potentially be a multi-select;
On a side note, I've placed adding the test values to outside the ViewModel;
This corresponds to the following View for starting off a template:
<div data-bind="template: { name: 'person-template' }"></div>
I've removed the data bit for now. This means each instance of this code would bind to (the same) $root view model. If you don't want that I'd suggest creating a container view model to hold several KoModels.
The template looks like this:
<span>Choose ID: </span>
<select data-bind="options: persons, optionsText: 'fullname', selectedOptions: selectedPersons"></select><br />
<!-- ko foreach: selectedPersons -->
<span>ID:</span> <span data-bind="text: id"></span><br />
<span>Full Name: </span> <span data-bind="text: fullname"></span><br />
<!-- /ko -->
Here's the jist:
The data-bind is much simpler. You don't need to fiddle with value attributes because Knockout will bind each option to a specific item in your array;
This leaves you free to use the fullname for the text;
The selectedOptions bit tells Knockout where to store selected items in your view model;
The selected options are shown in a foreach because the select could potentially be multiple select.
Now the ko.mapping.toJSON(self) call in the view model will generate something like this:
{
"persons": [{
"id": 1,
"fullname": "Martin Smith"
}, {
"id": 2,
"fullname": "Andy Gregersen"
}, {
"id": 3,
"fullname": "Thomas Peep"
}],
"selectedPersons": [{
"id": 2,
"fullname": "Andy Gregersen"
}]
}
As you can see the list of selected persons is there, to be sent to the server. The rest is there by default, but the mapping plugin can be configured to great detail.
Hope this helps and solves your problem!

Categories