Angular ng-options selected object - javascript

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"
};
};

Related

How to set a select option as selected when the data source is dynamically generated in angular js

I have the following code
<select ng-model="someObj.SomeEligibleStatusId" ng-options="st.someId as st.someDescription for st in someFunctionThatGeneratesData()"></select>
where someFunctionThatGeneratesData(param1, param2, param3) returns an array of objects such as
[
{
"someId":1,
"someDescription":"Test 1"
},
{
"someId":2,
"someDescription":"Test 2"
}
]
and I'm trying to change the selected option in javascript as
someObj.SomeEligibleStatusId = 1
without luck. I try also to add "track by", bind it to an object item of the array, and a couple of more scenarios... I'm losing it here guys. Can someone please help me out?
$scope.selectedsomeId = $someFunctionThatGeneratesData.options[1];
<select data-ng-model="selectedsomeId"
ng-options="st.someId as st.someDescription for st in someFunctionThatGeneratesData()"></select>

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.

angular select not get getting set from ng-model assignment

So I have a select set that I want to default to the state of a logged in user. The returned JSON for the state is the StateCode e.g. "MI"
<div>
<select
name="ParentState"
ng-model="selectedState"
ng-options="s.StateCode for s in cStates track by s.StateID | orderBy:'StateCode'">
</select>
</div>
and in my controller
var init = function() {
angular.forEach(xStates, function(xS) {
if (xS.StateCode == xParentState) {
$scope.selectedState = xS.StateID;
$scope.vName = "It's working!" + $scope.selectedState;
}
});
}
init();
The last line in the init function just writes out to {{ vName }} so that I could see the function was working and the value of $scope.selectedState is correct, for instance "MI" would be 23 but the select when the page is loaded is not set to "MI".
I think a couple things could be going wrong here, and I took somewhat of a best guess as to your desired outcome. Firstly, s.StateCode for s... in your markup is going to fill your <option>'s with StateCode. You mention you wish to see "MI", so lets indtead change this to StateName. Secondly, to default $scope.selectedState as our model, there is no need to specify StateId - just select the object and AngularJS will be satisfied to default our ng-options. Here is a completed example with some interpretations as to what I think you are after.
<select
name="ParentState"
ng-model="selectedState"
ng-options="s.StateName for s in xStates track by s.StateID | orderBy:'StateCode'">
app.controller('ctrl', ['$scope', function($scope) {
$scope.xStates = [{'StateCode': 1, 'StateID': 'A', 'StateName': 'AL'},
{'StateCode': 23, 'StateID': 'B', 'StateName': 'MI'},
{'StateCode': 50, 'StateID': 'C', 'StateName': 'Hawaii'}];
angular.forEach($scope.xStates, function (xS) {
if (xS.StateCode === 23) { // -- for demo simplicity
$scope.selectedState = xS;
}
});
}]);
JSFiddle Link - working demo

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'

On click How can I cycle through JSON one by one in AngularJS

Creating my first directive as an exercise in angular —making more or less a custom carousel to learn how directives work.
I've set up a Factory with some JSON data:
directiveApp.factory("Actors", function(){
var Actors = {};
Actors.detail = {
"1": {
"name": "Russel Brand",
"profession": "Actor",
"yoga": [
"Bikram",
"Hatha",
"Vinyasa"
]
},
"2": {
"name": "Aaron Bielefeldt",
"profession": "Ambassador",
"yoga": [
"Bikram",
"Hatha",
"Vinyasa"
]
},
"3": {
"name": "Adrienne Hengels",
"profession": "Ambassador",
"yoga": [
"Bikram",
"Hatha",
"Vinyasa"
]
}
};
return Actors;
});
And an actors controller:
function actorsCtrl($scope, Actors) {
$scope.actors = Actors;
}
And am using ng-repeat to display the model data:
<div ng-controller="actorsCtrl">
<div ng-repeat="actor in actors.detail">
<p>{{actor.name}} </p>
</div>
<div ng-click="next-actor">Next Actor</div>
</div>
1) How do I only display the actor's name in the first index of my angular model actors.detail?
2) How do I properly create a click event that will fetch the following index and replace the previous actor.name
User flow:
Russell Brand is Visible
click of next-actor ->Russell Brand's name is replaced with Aaron Bielefeldt
Since you only want the current user, the ng-repeat is not what you want to use, since that would be for each element in the data;
You would want to keep track of the index you are looking at in the scope, and increment that.
<div ng-controller="TestController">
{{data[current].name}}
<div ng-click="Next();"> NEXT! </div>
</div>
Where in the controller we also have these set up, where data is your actors:
$scope.current = 0;
$scope.Next = function() {
$scope.current = ($scope.current + 1) % $scope.data.length;
};
Here's a fiddle where it's done.
I would change my serivce to return a single actor and maintain the index in the controller.
something like this. This is an incomplete solution - you need to take care of cycle etc...

Categories