I have two connected dropdowns that I'm using with Knockout. Both of them I'm populating with a simple Value,Text pair that I load from an ajax call.
When I go to change the value however I always get the previous value from the selected list. So if the default RegionCode value is "-1" the first RegionCode value that will be passed to the loadLocationList when I change it to another region will be "-1" even if the actual selected value is different that what I get by inspecting the element or through JQuery
Model
define([
'util',
'locationService',
'jquery',
'knockout',
'knockoutmapping',
function(util, svc, $, ko, mapped) {
var LocationViewModel = function(regionCodes, regionCode, locationCodes, locationCode, currentYear) {
var self = this;
self.CurrentYear = currentYear;
self.RegionCode = ko.observable(regionCode)
self.RegionCodes = ko.observableArray(regionCodes)
self.LocationCode = ko.observable(locationCode)
self.LocationCodes = ko.observableArray(locationCodes)
self.loadLocationList = function() {
self.LocationCodes([]);
var locationListCallback = function(data){
for (var i = 0; i < data.length; i++) {
self.LocationCodes.push(new SelectListPair(data[i].Value, data[i].Text));
}
}
svc.getLocationsInRegion(self.CurrentYear, self.RegionCode, true, locationListCallback);
}
}
var SelectListPair = function (value, text) {
this.Value = ko.observable(value);
this.Text = ko.observable(text);
}
return function summaryViewModel() {
var self = this;
self.LocationSummaryViewModel = ko.observable();
var initViewModel = function() {
$.ajax({
url: 'Url here',
success: function(vm) {
var locationVM = vm.LocationSummaryViewModel;
var selectListArray = locationVM.LocationList;
var selectList = [];
for (var i = 0; i < selectListArray.length; i++) {
var SelectListPair = {
Value: ko.observable(selectListArray[i].Value),
Text: ko.observable(selectListArray[i].Text),
};
selectList.push(SelectListPair);
}
var location = [new SelectListPair("-1", "Please select a location")];
self.LocationSummaryViewModel(new LocationViewModel(vm.CurrentYear, selectList, "-1", location, "-1"));
},
});
}
}
self.initViewModel();
});
View
<!-- ko with: LocationSummaryViewModel() -->
<div class="panel panel-default" data-bind="visible: Visible()">
<div class="panel-heading">
<div class="row">
<div class="col-md-4">
Location Summary
</div>
<div class="col-md-4">
<select class="form-control" data-bind="
options: RegionCodes(),
disable: RegionCodes().length === 1,
optionsText: 'Text',
optionsValue: 'Value',
event: {change: loadLocationList },
value: RegionCode">
</select>
</div>
<div class="col-md-4">
<select class="form-control" data-bind="
options: LocationCodes(),
disable: LocationCodes().length === 1,
optionsText: 'Text',
optionsValue: 'Value',
value: LocationCode">
</select>
</div>
</div>
</div>
<div class="panel-body">
Neat content
</div>
<div class="panel-footer">
</div>
</div>
<!-- /ko -->
Don't bind to the change event on a select. Subscribe to the value variable instead. In Knockout, the idea is always to represent the view in the model, and then use the view exclusively.
Related
In the following code, a product (represented with productVM) has an observable property (productName) containing its name in two languages (english and french).
Once a cartItem is added, and a product is selected, I want its displayed name to be updated when the button "change language" is clicked (e.g., if "Door" is selected, and "change language" is then clicked, the displayed name should be the french version (which is simply the english word plus a french-ish suffix "eux")).
But it doesn't work: The options do change, but the selected option is changed to the caption option.
What needs to be changed/added to fix it?
var handlerVM = function () {
var self = this;
self.cartItems = ko.observableArray([]);
self.availableProducts = ko.observableArray([]);
self.language = ko.observable();
self.init = function () {
self.initProducts();
self.language("english");
}
self.initProducts = function () {
self.availableProducts.push(
new productVM("Shelf", ['White', 'Brown']),
new productVM("Door", ['Green', 'Blue', 'Pink']),
new productVM("Window", ['Red', 'Orange'])
);
}
self.getProducts = function () {
return self.availableProducts;
}
self.getProductName = function (product) {
if (product != undefined) {
return self.language() == "english" ?
product.productName().english : product.productName().french;
}
}
self.getProductColours = function (selectedProductName) {
selectedProductName = selectedProductName();
// if not caption
if (selectedProductName) {
var matched = ko.utils.arrayFirst(self.availableProducts(), function (product) {
return (self.language() == "english" ? product.productName().english : product.productName().french) == selectedProductName;
});
return matched.availableColours;
}
}
self.addCartItem = function (a, b, c, d) {
self.cartItems.push(new cartItemVM());
}
self.changeLanguage = function () {
self.language() == "english" ?
self.language("french") :
self.language("english");
}
}
self.productVM = function (name, availableColours) {
var self = this;
self.productName = ko.observable({
english: name,
french: name + "eux",
});
self.availableColours = ko.observableArray(availableColours);
}
self.cartItemVM = function () {
var self = this;
self.cartItemName = ko.observable();
self.cartItemColour = ko.observable();
}
var handler = new handlerVM();
handler.init();
ko.applyBindings(handler);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div>
<div data-bind="foreach: cartItems">
<div>
<select data-bind="options: $parent.getProducts(),
optionsText: function (item) { return $parent.getProductName(item); },
optionsValue: function (item) { return $parent.getProductName(item); },
optionsCaption: 'Choose a product',
value: cartItemName"
>
</select>
</div>
<div>
<select data-bind="options: $parent.getProductColours(cartItemName),
optionsText: $data,
optionsCaption: 'Choose a colour',
value: cartItemColour,
visible: cartItemName() != undefined"
>
</select>
</div>
</div>
<div>
<button data-bind="text: 'add cart item', click: addCartItem" />
<button data-bind="text: 'change language', click: changeLanguage" />
</div>
</div>
Your problem occurs when you change the options of your select. During the change, your value bound observable, cartItemName, contains the English string. For example: Door. As soon as you change the language, there is not a single option that returns Door for its optionsValue expression, thereby clearing the value altogether.
The best solution is to store a reference to your actual viewmodel, rather than just its string name. This does require you to move some other bits & pieces around, since you're manually updating quite a bit.
The starting point of the change:
// Remove
self.cartItemName = ko.observable();
// Add
self.cartItem = ko.observable();
// Change
<select data-bind="...
value: cartItem
" />
In a working snippet, with some other changes to make my work easier:
var handlerVM = function () {
var self = this;
self.cartItems = ko.observableArray([]);
self.language = ko.observable("english");
self.availableProducts = ko.observableArray([
new productVM("Shelf", ['White', 'Brown']),
new productVM("Door", ['Green', 'Blue', 'Pink']),
new productVM("Window", ['Red', 'Orange'])
]);
self.productNameFor = function(product) {
return product.productName()[self.language()];
};
self.addCartItem = function (a, b, c, d) {
self.cartItems.push(new cartItemVM());
}
self.changeLanguage = function () {
self.language() == "english" ?
self.language("french") :
self.language("english");
}
}
self.productVM = function (name, availableColours) {
var self = this;
self.productName = ko.observable({
english: name,
french: name + "eux",
});
self.availableColours = ko.observableArray(availableColours);
}
self.cartItemVM = function () {
var self = this;
self.cartItem = ko.observable();
self.cartItemColour = ko.observable();
}
ko.applyBindings(new handlerVM());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div>
<div data-bind="foreach: cartItems">
<div>
<select data-bind="options: $root.availableProducts,
optionsText: $root.productNameFor,
optionsCaption: 'Choose a product',
value: cartItem"
>
</select>
</div>
<div data-bind="with: cartItem">
<select data-bind="options: availableColours,
optionsCaption: 'Choose a colour',
value: $parent.cartItemColour"
>
</select>
</div>
</div>
<div>
<button data-bind="text: 'add cart item', click: addCartItem" />
<button data-bind="text: 'change language', click: changeLanguage" />
</div>
</div>
The discussion revolves around an application which can be visualized as,
As seen clearly, as the user clicks on one of the stories on the left hand side, the right-hand side fields are populated with the content corresponding to that story.
Every story has a title and a status,
service:
myModule.service('AngelloModel', function(){
var service = this;
var stories = [
{
title: 'First story',
status: 'To Do',
},
{
title: 'Second story',
status: 'Back Log',
},
{
title: 'Another story',
status: 'Code Review',
}
];
var statuses = [
{name: 'Back Log'},
{name: 'To Do'},
{name: 'In Progress'},
{name: 'Code Review'},
{name: 'QA Review'},
{name: 'Verified'},
{name: 'Done'}
];
service.getStories = function(){
return stories;
}
service.getStatuses = function(){
return statuses;
}
})
factory( a helper/utility function):
myModule.factory('AngelloHelper', function() {
var buildIndex = function(array, property) {
var tempArray = [];
for (var i = 0; i < array.length; i++) {
tempArray[array[i][property]] = array[i];
}
return tempArray;
}
return {
buildIndex : buildIndex
}
})
controller and module:
var myModule = angular.module('Angello',[]);
myModule.controller('MainCtrl',function(AngelloModel, AngelloHelper){
var main = this;
main.stories = AngelloModel.getStories();
main.statuses = AngelloModel.getStatuses();
main.statusesIndex = AngelloHelper.buildIndex(main.statuses, 'name');
main.setCurrentStory = function(story){
main.currentStory = story;
//alert(story.status); // (To Do) if I click on first story
main.currentStatus = main.statusesIndex[story.status];
//alert(main.currentStatus); // {name: 'To Do'} if I click on first story
//alert(main.currentStatus.name); // hence it will be (To Do)
}
})
html:
<body>
<div ng-controller="MainCtrl as main">
<div class="col-md-4">
<h2>Stories</h2>
<div class="callout" ng-repeat="story in main.stories"
ng-click="main.setCurrentStory(story)">
<h4>{{story.title}}</h4>
<p>{{story.description}}</p>
</div>
</div>
<div class="col-md-6 content">
<h2>Story</h2>
<form class="form-horizontal">
<div class="form-group">
<label class="control-label" for="inputTitle">Title</label>
<div class="controls">
<input type="text" class="form-control"
id="inputTitle" placeholder="Title" ng-model="main.currentStory.title" />
</div>
</div>
<div class="form-group">
<div class="controls">
<select id="inputStatus" class="form-control"
ng-model="main.currentStatus.name"
ng-options="l.name for l in main.statuses"></select>
</div>
</div>
</form>
</div>
</div>
</div>
</body>
Consider this one which is the whole point of discussion :
<select id="inputStatus" class="form-control"
ng-model="main.currentStatus.name"
ng-options="l.name for l in main.statuses"></select>
In the figure at the top, you can see the values in the drop-down field, which is done by
ng-options="l.name for l in main.statuses"
However, the current value is not reflected on selecting a story, even though I have done,
ng-model="main.currentStatus.name"
Any suggestions?
Looking at the ng-model you are trying to assign name as the unique identifier for options, so you may want to use select as i.e
ng-options="l.name as l.name for l in main.statuses"
This will make sure the ng-model (ng-model="main.currentStatus.name") is populated with the right name and your dropdown will be preselected with the value set in the ng-model property.
However if you are setting an object with array of objects as this with just one property you might as well set a unique identifier (if name is not one) or just use array of names.
Also with this you can removing the mapping logic (main.statusesIndex = AngelloHelper.buildIndex(main.statuses, 'name');) and just do:
main.currentStatus = {name: story.status};
or even set your ng-model as
<select id="inputStatus" class="form-control"
ng-model="main.currentStatus"
ng-options="l.name as l.name for l in main.statuses">
</select>
and
main.currentStatus = story.status;
So far, my code binds data to choices object, and then add field input dynamically when clicking "add course" button. Then, uses $http.get to return json data from the server and binds it to select option. Although when I choose an option, it only selects applied option.
HTML:
<div ng-app="timeTable" ng-controller="addCoursesCtrl">
<button ng-click="addNewCourse()">Add New Course</button>
<fieldset ng-repeat="choice in choices">
<select ng-model="choice.type"
ng-options="s.value as s.name for s in coursetoAdd">
<option value="{{s.value}}">{{s.value}}</option>
</select>
<input ng-model="choice.course">
</fieldset>
<button ng-click="convertAndSend()">Submit</button>
</div>
Javascript:
var timeTable = angular.module("timeTable",[]);
timeTable.controller("addCoursesCtrl", function ($scope,$http) {
$scope.choices = [{ course: '', type: '' }];
$scope.coursetoAdd = $scope.choices;
$http.get("/Semster/getSuggtedCourses").then(function (response) {
$scope.coursetoAdd = response.data;
});
$scope.addNewCourse = function () {
var newITemNo = $scope.choices.length + 1;
$scope.choices.push({ course: '', type: '' });
};
$scope.convertAndSend = function () {
var asJson = angular.toJson($scope.choices);
console.log(asJson);
$http.post('/Semster/Add', asJson);
};
});
I'm trying to dynamically set select input's value using knockoutjs data-bind. If I just put value: $parents[0].selectedSubcategory in the <select> element's attributes, it works fine. But when I try to pass the value in, it doesn't work.
My view looks like this -
<div data-bind="with: QuestionFilter">
<form>
<div data-bind="foreach: details">
<select data-bind="options: subcategories, optionsText: 'name', optionsValue: 'categoryID', value: subcategoriesValue">
</select>
</div>
</form>
</div>
<script type="text/javascript">
ko.applyBindings({
categories = <?php echo $categories; ?>,
details = ko.observableArray([])
});
</script>
And my JS looks like this -
function QuestionFilter(categories, details) {
var self = this;
self.subcategories = ko.observableArray([]);
self.selectedSubcategory = ko.observable();
function search(nameKey, myArray){
for (var i=0; i < myArray.length; i++) {
if (myArray[i].parentCategory_id === nameKey) {
self.subcategories.push(myArray[i]);
}
}
}
search(2, categories);
details.push({ firstName: self.subcategories(), lastName: self.selectedSubcategory()});
});
self.selectedSubcategory.subscribe(function(subcategory) {
function subsearch(nameKey, myArray){
for (var i=0; i < myArray.length; i++) {
if (myArray[i].parentCategory_id === nameKey) {
self.superSubcategories.push(myArray[i]);
}
}
}
subsearch(subcategory, categories);
details.push({ firstName: self.superSubcategories()});
});
In addition to the above, I have tried using lastName: $parents[0].self.selectedSubcategory(), with and without quotes. I have also tried setting the <select> element's value like this: value: $parents[0].lastName also with and without quotes and +, but no joy.
Any thoughts or clarification needed?
I have an array within an array, for example I have the following objects:
{ruleGroups: [{
rules: [{
dataField1:ko.observable()
,operator:ko.observable()
,dataField2:ko.observable()
,boolean:ko.observable()
,duration:ko.observable()
}]
}]
};
How can I edit the array within the array?
I was able to improve the issue but still have problems with adding row when adding group, the new group works but the old groups run dead:
A working example is found here (http://jsfiddle.net/abarbaneld/UaKQn/41/)
Javascript:
var dataFields = function() {
var fields = [];
fields.push("datafield1");
fields.push("datafield2");
return fields;
};
var operators = function() {
var operator = [];
operator.push("Plus");
operator.push("Minus");
operator.push("Times");
operator.push("Divided By");
return operator;
};
var booleanOperators = function() {
var operator = [];
operator.push("Equal");
operator.push("Not Equal");
operator.push("Greater Than");
operator.push("Less Than");
operator.push("Contains");
operator.push("Etc...");
return operator;
};
var ruleObj = function () {
return {
dataField1:ko.observable()
,operator:ko.observable()
,dataField2:ko.observable()
,boolean:ko.observable()
,duration:ko.observable()
}
};
var ruleGroup = function() {
return rg = {
rules: ko.observableArray([new ruleObj()]),
addRow: function() {
rg.rules.push(new ruleObj());
console.log('Click Add Row', rg.rules);
},
removeRow : function() {
if(rg.rules().length > 1){
rg.rules.remove(this);
}
}
}
};
var ViewModel = function() {
var self = this;
self.datafields = ko.observableArray(dataFields());
self.operators = ko.observableArray(operators());
self.booleanOperators = ko.observableArray(booleanOperators());
self.groupRules = ko.observableArray([new ruleGroup()]);
self.addGroup = function() {
self.groupRules.push(new ruleGroup());
};
self.removeGroup = function() {
if(self.groupRules().length > 1){
self.groupRules.remove(this);
}
};
self.save = function() {
console.log('Saving Object', ko.toJS(self.groupRules));
};
};
ko.applyBindings(new ViewModel());
HTML
<div data-bind="foreach: { data: groupRules, as: 'groupRule' }" style="padding:10px;">
<div>
<div data-bind="foreach: { data: rules, as: 'rule' }" style="padding:10px;">
<div>
<select data-bind="options: $root.datafields(), value: rule.dataField1, optionsCaption: 'Choose...'"></select>
<select data-bind="options: $root.operators(), value: rule.operator, optionsCaption: 'Choose...'"></select>
<select data-bind="options: $root.datafields(), value: rule.dataField2, optionsCaption: 'Choose...',visible: operator"></select>
<select data-bind="options: $root.booleanOperators(), value: rule.boolean, optionsCaption: 'Choose...'"></select>
<input data-bind="value: rule.duration" />
<span data-bind="click: groupRule.addRow">Add</span>
<span data-bind="click: groupRule.removeRow">Remove</span>
</div>
</div>
<span data-bind="click: $parent.addGroup">[Add Group] </span>
<span data-bind="click: $parent.removeGroup">[Remove Group]</span>
</div>
</div>
<div>
<span data-bind="click:save">[Save]</span>
</div>
I was able to fix the issue by rearranging the function of ruleGroup to:
var ruleGroup = function() {
var rg = {
rules: ko.observableArray([new ruleObj()]),
addRow: function() {
rg.rules.push(new ruleObj());
console.log('Click Add Row', rg);
},
removeRow : function() {
if(rg.rules().length > 1){
rg.rules.remove(this);
}
}
}
return rg;
};
Not exactly sure why this made a difference but I think its due to now a new var is being created and referenced.
Working JSFiddle is found here http://jsfiddle.net/abarbaneld/UaKQn/