In our application we have a series of select filters that must be populated dynamically based on the context of the situation. On first load, the default options are inserted into the array via AJAX and it appears on the UI as expected. However, when the select list refreshes, the UI does not reflect the changes even though if you inspect the code the array appears to contain the new values.
I have written the same code for two filters but for some strange reason it only works in one of the situations, I have tried the following to resolve this to no avail:
Populating the array manually using arbitrary data
Forcing knockout to update with array.valueHasMutated()
Using the two different types of array clearing functions array.removeAll and array([])
Using push.apply and push
Saving the result to a variable and then assigning that to the array
Making the values inside the array observable
This first instance of the code works as expected when the options change:
success: function (data) {
self.filtersModel.values2.removeAll();
var serverData = $.map(data, function (value, i) {
return new SelectBoxOption(value.Description, value.Id);
});
serverData.forEach(function (value) {
self.filtersModel.values2.push(value);
});
}
This is the second function that does NOT work:
success: function (data) {
self.filtersModel.values.removeAll();
var other;
var serverData = $.map(data, function (value, i) {
// If the option is "other" save it as a variable and add to array later
if (value.Code === "OTHR") {
other = new SelectBoxOption(value.Description, value.Id);
self.filtersModel.othersValue(value);
}
else if (value.Code == "EQTY") {
var equity = new SelectBoxOption(value.Description, value.Id);
self.filtersModel.equityValue(value);
return equity;
}
else
return new SelectBoxOption(value.Description, value.Id);
});
serverData.forEach(function (value) {
self.filtersModel.values.push(value);
});
// Add "other" option to bottom of the array
if (other)
self.filtersModel.values.push(other);
}
Any help would be greatly appreciated.
UPDATE
HTML to populate the select list occurs like this:
<div class="form-group">
<label for="value">Value</label>
<select id="value" class="form-control" data-bind="value: selectedValue, options: values, optionsCaption: '-- ' + 'Select Value' + ' --', optionsValue: 'optionId', optionsText: 'optionName'"></select>
</div>
<div class="form-group">
<label for="value2">Value 2</label>
<select id="value2" class="form-control" data-bind="enable: $parent.valueIsAuthorisedAndvalueIsEquityOrOther, value: selectedValue2, options: values2, optionsCaption: '-- ' + 'Select Value 2' + ' --', optionsValue: 'optionId', optionsText: 'optionName'"></select>
</div>
Example data is returned in this format:
data = [
{optionId: 1, optionName: "Value 1"},
{optionId: 2, optionName: "Value 2"},
{optionId: 3, optionName: "Value 3"},
{optionId: 4, optionName: "Value 4"}
];
If I understand your question, the second code doesn't work because your are updating the self.filtersModel.othersValue and self.filtersModel.equityValue in every iteration.
I rewrote your code, hope it helps:
self.filtersModel.values.removeAll();
var normals = [];
var others = [];
var equities = [];
ko.utils.arrayForEach(data, function (d) {
if (d.Code === 'OTHR')
others.push(new SelectBoxOption(d.Description, d.Id);
else if (d.Code === 'EQTY')
equities.push(new SelectBoxOption(d.Description, d.Id);
else
normals.push(new SelectBoxOption(d.Description, d.Id);
});
self.filtersModel.values(normals.concat(equities).concat(others));
After much deliberation, I found that the issue was not related to the ajax function at all but rather a buggy method call I defined before the changing the select list:
self.clearFilters = function() {
self.filtersModel = new createFiltersModel();
}
function createFiltersModel() {
return {
values: ko.observableArray([])
}
};
This created a new instance of the filtersModel every time the select list changed, so any changes to the model were lost when the ajax call was made. Doh!
Related
I had a list array of ng-model on my options. Each ng-model assign with different values depends on the user inputs. It works for me when I tried to select the options box and the result produced what I want. But when I retrieved new data from API and parse the value into that options box. Is not working.
HTML
<div ng-repeat="($a_choice_index, c) in menu.additionalchoice">
<label for="max_select" class="rl-label required">Do you want maximum select?
<label for="max_select" class="select-block">
<select id="max_select" name="max_select" ng-model="c[$a_choice_index].maxSelect" ng-change="selectMaxSelect(c[$a_choice_index].maxSelect, $a_choice_index)" ng-options="o.name for o in maxSelectOptions"></select>
<svg class="svg-arrow">
<use xlink:href="#svg-arrow"></use>
</svg>
</label>
</label>
</div>
Controller
$scope.menu = {};
$scope.maxSelectOptions = [
{
name: "No",
value: 0
},
{
name: "Yes",
value: 1
}
];
$scope.selectMaxSelect = function(v, index){
$scope.menu.mealchoices[index].max_select = v;
};
MyService.GetData(function(menu){
$scope.menu = menu; // menu.additionalchoice is added into menu
});
API Data
Each menu.additionalchoice produces
{"1":{"title":"Add favorite oils","maxSelect":{"name":"No","value":0}},"title":"Add favorite oils","max_select":4,"additional_choice":1,"maxSelect":1,"meals":...
When I tried to:
1) ng-init="c[$a_choice_index].maxSelect = c.maxSelect"
It shows empty.
2) ng-init="selectMaxSelect(c.maxSelect, $a_choice_index)"
It shows some values but the value is incorrect. c[$a_choice_index].maxSelect returns {"name": "No", "value": 0} when the c.maxSelect == 1 which is YES.
You can bind the select option by value only by using ... as ... for ... in ... syntax, which is a lot easier compare to binding using object.
ng-options="o.value as o.name for o in maxSelectOptions"
The result in ng-model should now be 0 or 1 instead of an object.
I've got a question regarding checkedValue of checked binding in Knockout (http://knockoutjs.com/documentation/checked-binding.html)
Basically, look this fiddle http://jsfiddle.net/jo9dfykt/1/.
Why if I click the button "Add Item" the right choise is selected but if I click the second button "Add Item Two" this not succed?
What are the differences between addItem and addItem2 method in viewModel?
And then, why the with selectedValue shows nothing?
My goal is to bind a list of radio button to an observableArray and save the entire object into an observable. But I want to set the inital value of radio button without search it on observableArray.
Javascript
var viewModel = {
items: ko.observableArray([
{ itemName: 'Choice 1' },
{ itemName: 'Choice 2' }
]),
chosenItem: ko.observable(),
addItem: function() {
this.chosenItem(this.items()[0]);
},
addItem2: function() {
this.chosenItem({itemName: 'Choice 2'});
}
};
viewModel.chosenItem.subscribe(function(newValue){
console.log(newValue.itemName);
}),
ko.applyBindings(viewModel);
HTML
<!-- ko foreach: items -->
<input type="radio" data-bind="checkedValue: $data, checked: $root.chosenItem" />
<span data-bind="text: itemName"></span>
<!-- /ko -->
<input type="button" data-bind="click: addItem" value="Add Item"/>
<input type="button" data-bind="click: addItem2" value="Add Item Two"/>
<div>Selected Value: <span data-bind="text: chosenItem.itemName"></span></div>
Knockout computes the checked state by doing a reference check. I.e.:
checkedValue = $data; // from checkedValue: $data
checked = checkedValue === chosenItem() // from checked: $root.chosenItem
You'll probably know that, in javascript, two objects that look similar are not equal:
{ a: 1 } === { a: 1 } // returns false
That's why you need to rewrite your second button to:
this.chosenItem({itemName: this.items()[0]});
to make sure the reference check passes.
Also, to correctly show the current state, use:
<div data-bind="with: chosenItem">
Selected Value: <span data-bind="text: itemName"></span>
</div>
If you want to circumvent having to point to the elements in your items array, you can use checkedValue: itemName and addItem() { this.chosenItem("Choice 1"); } but this forces you to use unique names.
A better option would be to make viewmodels for your items that have their own add method:
var Item = function(name, selection) {
var self = this;
this.itemName = name;
this.add = function() {
selection(self);
};
};
var items = ["Choice 1", "Choice 2"].map(function(name) {
return new Item(name, vm.chosenItem);
});
In the first example you're providing a reference to the actual item you want selected. In the second you're providing an object which has the same property & value, but its not the actual item in the list merely one which looks like it.
But I want to set the inital value of radio button without search it on observableArray.
Perhaps make chosentItem a computed observable, which relies on a string observable with just the key of the item you want selected. This would allow you to say something like:
self.chosenItemName('Choice2');
I have an array of texts that I display in a <select>.
The texts may have different version no, and I want to filter the <select> based upon the latest version only.
I guess there are more elegant ways to do it (suggestions welcome), but I´ve chosen to use 2 <select>s set to alternate visibility depending on the checkbox.
The code is a hack, but the result looks pretty good. Unfortunately there´s a bug.
I have two observables indicating the selected option in their respective arrays:
self.SelectedText = ko.observable();
self.SelectedUnique = ko.observable();
Both have subscriptions, but I cannot link them together in both subscription, so I have chosen one to be indipendant on the other like this:
self.SelectedUnique.subscribe(function (text) {
if (text) {
self.SelectedText(text);
}
});
However, the get out of sync.
Scenario 1: select text 1,2,3. [OK]
Scenario 2: select text 2; check "Latest versions only"
This causes no options ("Choose…") to be displayed. Not what I want.
It gets worse.
Scenario 3: uncheck; select text 3; Then check "Latest versions only" again.
Now the select option chosen is set to select option no 2 of the unfiltered.
There´s probably a simple issue. I just can´t make it work probably. Here´s the fiddle: Fiddle: http://jsfiddle.net/h5mt51gv/6/
All help and suggestions appreciated!
I have streamlined your approach:
the <select> binds to a computed list of options (visibleTextBatches)
this computed list depends on the state of the checkbox (latestOnly), effectively toggling between the full and the filtered list
the filtered list (latestTextBatches) is another computed that holds the latest version for each group
the <select> stores the actual selected TextBatch object in an observable (selectedTextBatch)
there is a subscription to visibleTextBatches that causes the latest selectable TextBatch to become the current one when the list is filtered. When the list is unfiltered, it does nothing.
function TextBatch(data) {
this.textbatchId = data.textbatchId;
this.parentId = data.parentId;
this.version = data.version;
this.title = ko.observable(data.title);
}
function ViewModel() {
var self = this;
// read up on the mapping plugin, too
self.textBatches = ko.observableArray([
new TextBatch({textbatchId: 1, parentId: 1, version: 1, title: "TB1.1"}),
new TextBatch({textbatchId: 2, parentId: 1, version: 2, title: "TB1.2"}),
new TextBatch({textbatchId: 3, parentId: 3, version: 1, title: "TB2.1"})
]);
self.selectedTextBatch = ko.observable();
self.latestOnly = ko.observable(false);
self.latestTextBatchGroups = ko.computed(function () {
var latest = {};
ko.utils.arrayForEach(self.textBatches(), function (batch) {
if (!latest.hasOwnProperty(batch.parentId) ||
batch.version > latest[batch.parentId].version
) latest[batch.parentId] = batch;
});
return latest;
});
self.latestTextBatches = ko.computed(function () {
return ko.utils.arrayFilter(self.textBatches(), function (batch) {
return batch === self.latestTextBatchGroups()[batch.parentId];
});
});
self.visibleTextBatches = ko.computed(function () {
return self.latestOnly() ? self.latestTextBatches() : self.textBatches();
});
self.visibleTextBatches.subscribe(function () {
var selectedBatch = self.selectedTextBatch();
if (selectedBatch && self.latestOnly()) {
self.selectedTextBatch(
self.latestTextBatchGroups()[selectedBatch.parentId]
);
}
});
}
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div>
<select data-bind="
options: visibleTextBatches,
optionsText: 'title',
optionsCaption: 'Select...',
value: selectedTextBatch
" />
</div>
<div>
<input type="checkbox" id="chkLatestOnly" data-bind="checked: latestOnly" />
<label for="chkLatestOnly">Latest only</label>
</div>
<hr />
<pre data-bind="text: ko.toJSON($root, null,2)"></pre>
I have a really simple CRUD app for managing music albums. Just two fields are tracked, title and artist.
In this example, the dropdown shows a list of albums, and if I fill out the form and click Save it will be added to the list.
In the second example, selecting an album will populate the form so it can be edited and updated.
My question is, is there a way to get both functionality in the same form? Sure I could create two identical forms and have them do slightly different things, but given they're operating on the same data, it would be nice if when a current_album is selected, it updates, and when "New album..." is selected, it creates.
The major roadblock seems to be value vs ng-model. I can set the value so it populates when I pick an item from the <select> OR I can set an ng-model="newAlbum", but not both to my knowledge.
You shouldn't be using the value attribute with ng-model. This is a very bad practice.
What I would suggest is to use ng-change on your list and keep a cloned object with the editing value.
$scope.onChange = function() {
if ($scope.currentAlbum) {
$scope.editing.title = $scope.currentAlbum.title;
$scope.editing.artist = $scope.currentAlbum.artist;
} else {
$scope.editing = {};
}
};
The all you need to do when saving is checking is it a new object or not:
$scope.addOrSaveAlbum = function() {
if ($scope.currentAlbum) {
$scope.currentAlbum.title = $scope.editing.title;
$scope.currentAlbum.artist = $scope.editing.artist;
} else {
$scope.albums.push({ title: $scope.editing.title, artist: $scope.editing.artist });
}
$scope.editing = {};
};
See http://jsfiddle.net/4Zeuk/12/
(thank you to Wawy to point out ng-change instead of $scope.$watch)
You can indeed get both functionality without the need of two different forms, but you can't use the same object in the scope in ng-model for both select and form fields.
But what you can do is have two different objects in the scope, one that contains the value of the selected item and the other will contain either a new instance of an album or a copy of the selected one. Then when you click the save/update button, based on the id of the object in the form you can decide if you need to save or modify the album collection.
Here is one way of doing what I've just explained:
<div ng-app="albumShelf">
<div ng-controller="MainCtrl">
<div style="float:left;">
<select ng-options="b.title for b in albums" ng-model="current_album" ng-change=updateForm()>
<option value="">Choose album...</option>
</select>
</div>
<div style="float:left;">
<form>
<input type="text" ng-model="newAlbum.title">
<br>
<input type="text" ng-model="newAlbum.artist">
<br>
<input type="submit" value="{{ current_album ? 'Update' : 'Save' }}" ng-click="modifyAlbumCollection()">
</form>
</div>
</div>
var albumShelf = angular.module('albumShelf', [/*'ngResource'*/])
.controller('MainCtrl', ['$scope', function($scope/*, albumFactory*/) {
//$scope.albums = albumFactory.query();
$scope.albums = [
{ id: 1, title: 'Disorganized Fun', artist: 'Ronald Jenkees' },
{ id: 2, title: 'Secondhand Rapture', artist: 'MS MR' }
];
$scope.modifyAlbumCollection = function() {
if ($scope.newAlbum.id !== null) {
//We are modifying an existing album
var i, found = false;
for (i = 0; i<$scope.albums.length && !found; i++) {
if ($scope.albums[i].id === $scope.newAlbum.id) {
$scope.albums[i] = angular.copy($scope.newAlbum);
found = true;
}
}
} else {
//We are adding a new album to the collection
$scope.newAlbum.id = $scope.albums.length;
$scope.albums.push(angular.copy($scope.newAlbum));
$scope.newAlbum = { id: null, title: '', artist: '' };
}
};
$scope.updateForm = function() {
if ($scope.current_album) {
//Copy the information from the selected album into the form.
$scope.newAlbum = angular.copy($scope.current_album);
} else {
//Clear previous album info.
$scope.newAlbum = { id: null, title: '', artist: '' };
}
};
}])
//.factory('albumFactory', function($resource) {
// return $resource('/albums/:albumId.json', { albumId: '#id' }
// );
//});
Here is the jsfiddle
In my opinion it's more clear if you use a ng-change in the select rather than a $watch on the ng-model value. Because what you want is update the form when a new value is selected from the dropdown rather than watching for changes on the object in the $scope.
I am working on a drop down control that is powered by Boolean values. I have a viewmodel that consists of the lookup dataset array (values and text attributes) and also a dataset that contains a Boolean value that needs to be aligned with that value chosen in the array. So data in the viewmodel represents an actual data item, while lookupdata represents essentially a mapping of value pairs. These two items really need to stay independent of eachother for the purposes of each data item containing only information relevant to it.
I have been able to get a sample working where I brought IsActive outside of the data object as it exists in viewmodel and placed it in viewmodel, however I can't get it to work while it is inside of viewmodel. I have created a JSFiddle that demonstrates functionally what I want the user to see, as well as one where I have the data where it belongs, but the drop down does not work as expected.
Active maps to true and Inactive maps to false. Switching between those should also affect the word true/false on the screen.
http://jsfiddle.net/rodearly/xF78A/11/
<div data-bind="with: data">
<select data-bind="options: $root.lookupData.status, optionsText: 'text', optionsValue: 'value', value: IsActive"></select>
<label>Value: </label>
<span data-bind="text: IsActive"></span>
</div>
<div>Alternative</div>
<div>
<select data-bind="options: lookupData.status, optionsText: 'text', optionsValue: 'value', value: IsActive"></select>
<label>Value: </label>
<span data-bind="text: IsActive"></span>
</div>
function Item(id, name) {
this.id = ko.observable(id);
this.name = ko.observable(name);
}
function getLookupData() {
var lookupData = {};
lookupData.status = [{
text: "Active",
value: true
}, {
text: "Inactive",
value: false
}];
return lookupData;
}
CreateImplantEditViewModel = function (data, lookupData) {
var vm = {};
vm.data = ko.observable(data);
vm.IsActive = ko.observable(false);
vm.lookupData = {};
vm.lookupData.status = ko.observableArray(lookupData.status);
return vm;
};
debugger;
var editImplantVm = CreateImplantEditViewModel({
IsActive: false
},
getLookupData());
ko.applyBindings(editImplantVm);
Thanks for any help,
~David
If I understand what you are attempting to do, your problem lies in the data that you are passing in to the 'CreateImplantEditViewModel' constructor. You need to create IsActive as an observable, otherwise it will never update:
var editImplantVm = CreateImplantEditViewModel({
IsActive: ko.observable(false)
},
getLookupData());
I've updated your jsfiddle, and both dropdowns now update their associated text binding when you change the selected option.
The problem here actually is that the Boolean values are being converted into string values. To get this to work, value needs to be either a string or a number.