Options Binding overrides initial View Model value - javascript

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.

Related

Why does setting an optionsValue break Knockout updating?

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.

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'

How to programmatically select ng-option value?

I have a view that is filled with dropdownlists to filter a report. I also have a view of saved filters that are displayed as links. When a user clicks on their saved filters, I want the appropriate values of the dropdownlists to be selected. The drop downs are being populated properly. On the saved filter link there is an ng-click that will call a function that iterates through the collection of saved filter values and automatically selects the correct one. I cannot figure out how to programmatically set the selected option. Any help is much appreciated!
<select uid="locSelect"
class="span12"
ng-model="reportDetail.selectedLoc"
ng-options="loc.dbid as loc.serviceName for loc in reportDetail.locList | orderBy:'name'">
<option uid="unselectedLocOption" value="">-- Select One --</option>
</select>
Here is the list of saved filters:
<div class=" well fixed-search" style="overflow-x: hidden;overflow-y: auto;">
<div class="well-header">
Saved Filters
</div>
<div ng-if="!hasSavedFilters">
<span>No saved filters</span>
</div>
<ul ng-if="hasSavedFilters" class="nav nav-list dashboard-list">
<li ng-repeat="filter in reportDetail.savedFilters">
<a uid="savedFilter" href="" ng-click="reportDetail.loadSavedFilters(filter.filters)">
<span ng-bind="filter.title"></span>
</a>
</li>
</ul>
And here is my controller
(function(){
'use strict';
var ReportDetailController = function(ReportsService, $scope){
var _locList = {};
var _hospitalStatusList = {};
var _providerStatusList = {};
var _savedFilters = [];
var _sourceTypeList = {};
var _dateRangeList = {};
var _init = function(){
ReportsService.getCurrentReportSavedFilters().then(function(data){
$scope.reportDetail.savedFilters =data;
$scope.hasSavedFilters = ReportsService.hasSavedFilters();
});
ReportsService.getLOCListForDDL().then(function(data){
$scope.reportDetail.locList = data;
//$scope.reportDetail.selectedLoc = $scope.reportDetail.locList[0];
});
ReportsService.getSelectListData()
.then(function(data){
$scope.reportDetail.sourceTypeList = data.CONNECTION_TARGET_STATUS;
$scope.reportDetail.hospitalStatusList = data.CONNECTION_SOURCE_STATUS;
});
ReportsService.getDateRangesForDDL()
.then(function(data){
$scope.reportDetail.dateRangeList = data;
});
$scope.reportDetail.providerStatusList = ReportsService.getProviderStatusForDDL();
};
var _loadSavedFilters = function(filters){
for(var i = 0, l = $scope.reportDetail.locList.length; i<l; i++){
if($scope.reportDetail.locList[i].serviceName == filters.levelOfCare){
$scope.reportDetail.selectedLoc = $scope.reportDetail.locList[i];
console.log($scope.reportDetail.selectedLoc);
}
}
}
var _isActive = function(filter){
for(var i = 0, l = $scope.reportDetail.savedFilters.length; i<l; i++){
if(filter.title == $scope.reportDetail.savedFilters[i].title){
return true;
}
return false;
}
}
var _generateReport = function(){
return ReportsService.generateReport();
};
$scope.reportDetail = {
init: _init,
selectedLoc: null,
isActive: _isActive,
locList: _locList,
selectedHospitalStatus: 'NOTIFIED',
hospitalStatusList: _hospitalStatusList,
selectedProviderStatus: 'NEW',
providerStatusList: _providerStatusList,
selectedSourceType: 'CONNECTED',
sourceTypeList: _sourceTypeList,
selectedDateRange: '',
dateRangeList: _dateRangeList,
savedFilters: _savedFilters,
loadSavedFilters: _loadSavedFilters,
generateReport: _generateReport
};
$scope.reportDetail.init();
};
app.controller('ReportDetailController', ['ReportsService', '$scope', ReportDetailController]);
})();
You just need to set the ng-model to whatever it should be, so in this case you would set reportDetail.selectedLoc to whatever loc.dbid it should be.
For example: http://jsfiddle.net/uWLua/1/
Note: Make sure they have the same type, so in your example make sure they are either both integers, or both strings, it will not know they are the same if you have one as 5073 and one as "5073"
I updated the fiddle to show that the string and number do not do the same thing.
The ng-model and the expression feeding ng-options -must- match in order for Angular to compare values and see what option is 'selected'. Just as 'dave' indicated.
Due to time constraints I ended up going a different route. I created an event bus of sorts in my service layer and subscribe to the even in my controller, updating the model, and used ng-repeat with ng-selected.
I'm still interested to understand why this was not working with ng-options. The model and ng-options types matched, and everything appeared to be wired up correctly. When I have more time i'll re-address the original issue. Thanks for all who responded!
You need custom directive, or something similar to this two approaches
<div ng-controller="MyCtrl">
<h1>Approach 1</h1>
<span ng-repeat="val in dbs">
<input type="checkbox" ng-model="val.checked">{{val.name}}
</span>
<hr/>
<h1>Approach 1</h1>
<select multiple>
<option ng-repeat="val in dbs" name="val.name" value="val.name" ng-selected="val.checked">{{val.name}}</option>
</select>
<h4>Source (note scope changes)</h4>
{{dbs}}
</div>
also you can use ng-change to do some complex ops
If I understand, in summary, you have a select filled with a list, and you want to programmatically set one of those to be selected, type it as the default right?
If so, you can easily solve this with ng-options, just associate your controller instance with scope and assign the position of the list you want to the model of select, for example:
Select HTML
<select ng-model="vm.aluno_id" name="aluno_id" ng-options="aluno.nome for aluno in alunos">
Controller
app.controller("auxiliarController", function( $scope){
//instancia controller;(Controller instance;)
var vm = this;
$scope.vm = vm;
//carregando lista no scope, que serĂ¡ utilizado pelo angular no select
//Loading list in scope, which will be used by angular in select
$scope.alunos = [{id: 1, nome: "aa"}, {id: 2, nome: "bb"}];
//setando item default no select
$scope.vm.aluno_id = $scope.alunos[0];
});
I hope I have helped

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!

Multiple Select List and KnockoutJS

I have a multi-select list that I've implemented following the instructions on the KO site. The important portions of my code currently look like this (removed unnecessary code):
function Attribute(data) {
var self = this;
self.Id = data.Id;
self.Name = data.Name;
}
// Individual Row in Table
function Criteria(data) {
var self = this;
self.Attributes = data.Attributes;
}
// Represent the ViewModel for attributes.
function CriteriaViewModel() {
var self = this;
// Catalog Data
self.availableAttributes = window.ko.observableArray([]);
$.getJSON(window.attributeListUrl, function(availableData) {
self.availableAttributes($.map(availableData.Attributes, function(item) { return new Attribute(item); }));
});
// Editable Data
self.criterion = window.ko.observableArray([]);
// Load initial state from server
$.getJSON(window.criteriaListUrl, function (availableData) {
self.criterion($.map(availableData.Criterion, function (item) { return new Criteria(item); }));
});
}
Then, in my HTML, I bind it all together (or, I at least try to):
<tbody data-bind="foreach: criterion">
<tr>
<td>
<select class="selectedAttributes"
data-bind="options: $root.availableAttributes, selectedOptions: Attributes, optionsText: 'Name', optionsValue: 'Id'"
multiple
size="6">
</select>
</td>
</tr>
</tbody>
The possible options display correctly. However, there is no apparent binding between the criteria's attributes against the possible options. From reading the guide, it seems as though KO should be able to bind objects directly. Can anybody provide guidance here?
I forgot to mention that everything works except the actual binding of the multi-select list. I am applying my bindings appropriately in general - just not with the multi-select list.
The attributes property on the Criteria object needs to be an observableArray. Here is a Jsfiddle demonstrating
function Criteria(data) {
var self = this;
self.Attributes = ko.observableArray(data.Attributes);
}
var x= $('#select1 option:selected');
if(x.length>0){
x.each(function(){
alert($(this).text());
self.selectedCategory.push(new categoryModel($(this).text()));
$('#select1 option:selected').remove();
});
}
refer http://jsfiddle.net/deepakpandey1234/wse4gdLq/

Categories