Angularjs Select with group and initial value - javascript

I have a form that includes a SELECT element. I load the possible values in my controller from a function that returns the data from a database. I want to group the options by a group name.
I have the list of options loading properly showing the grouping. My problem, is the initial value is not displaying - this is based on a data model. It shows as blank. If I choose any option in the list, it does properly display.
I followed the example from this solution Populate a Dropdown list by grouping using AngularJs
From the various other examples that I have seen, this should work...I'm guessing it is some little thing I accidentally overlooked.
This loads the possible values for the drop down:
$http.get("api/getIndustry").success(function(data){
$rootScope.industryData = [];
$.each(data, function (i, data) {
$rootScope.industryData.push({
group: data.group,
id: data.id,
text: data.text
});
});
});
For now, I am trying to initially set a selected value (eventually it will be set from reading a record):
$scope.example3model = {group: 'Energy and Natural Resources', id: '25', text: 'Utilities'};
And this is a portion of my view.
<td colspan="4" ng-hide="editableForm.$visible">
<select ng-model="example3model" class="form-control input-md" ng-options="industry.text group by industry.group for industry in industryData" >
</select></br>
{{example3model}} <- did this to see what was chosen
</td>
I'm not sure what else to try to get this to work...the only problem I see right now is that the list is not showing the 'default' value of what is initially in example3model (so the list shows as blank). If I choose a value in the list it is displayed correctly.
Any suggestions would be greatly appreciated.

The problem is that you're trying to set the initial value to an object literal, and even though it may look the same as one inside the select options it is not.
This happens because of how Javascript and AngularJS both work to set that initial object-value (note that this wouldn't happen if options was an array of primitives such as numbers and strings): {} and {} look the same from a human perspective, but they're clearly not the same in JS, try doing this in the browser console:
{} == {}
// this will be false
{ a: 1 } == { a: 1 }
// this will be false as well
Now, what Angular does behind the scenes is checking if the ngModel matches any reference inside ngOptions, that's why we need to set the initial value specifically referenced from the options array.
The initialization, in your example, must be something like this in the specific case you provided (note that I'll be hard-coding the id to match the needs of your post, but you could change it to match whatever you need)
const defaultId = 25;
$scope.example3model = $rootScope.industryData.find(data => +data.id === defaultId)
Now the ngModel value is pointing to the referenced array object that we want.
* Take a look at the official documentation about complex models for ngOptions
[note that this will not work if none of the objects in the ngOptions array has that defaulted id as it will not match any of them]

Related

label/data mechanism in "OO.ui.ComboBoxInputWidget"

OOUI's OO.ui.ComboBoxInputWidget allows to set an array of
OO.ui.MenuOptionWidget objects in its menu.items config field.
Such an item can have a label and a data field. The data field can
be of type object 1.
Now, if I use a data field of typeobject the value of the
OO.ui.ComboBoxInputWidget will be "[Object object]", as it tries to cast
the data value to a string when a user selects an option item.
So it looks like OO.ui.ComboBoxInputWidget allows only data of type
string in its options. Is that correct?
That would also mean that there is no "label/data" mechanism of the input
field itself. If I've got the following options
[
{ label: "Rot", data: "red" },
{ label: "Gelb", data: "yellow" },
{ label: "GrĂ¼n", data: "green" }
]
and the user selects the option with label "Gelb" the input field shows
"yellow", not "Gelb". The code example in the official documentation shows this behavior [2]. Did I miss something? Is it possible to show a
label to the user but retrieve the data (object) when calling
getValue on such a field?
1 https://doc.wikimedia.org/oojs-ui/master/js/#!/api/OO.ui.MenuOptionWidget-cfg-data
[2] https://doc.wikimedia.org/oojs-ui/master/js/#!/api/OO.ui.ComboBoxInputWidget
This question was originally posted on the wikitech-l mailing list. You can find the thread here: http://markmail.org/message/fesegc3yljqcytzt
In general, the 'data' property for items inside the GroupElement can be strings or objects, as they represent some state of your item. In OO.ui.mixin.GroupElement, the method getItemFromData can then return a specific item based on its data property. If you use an Object for the data, OOUI will use its OO.getHash() to basically stringify your object, so it can make sure it retrieves the right one.
The property 'data' actually comes all the way up the hierarchy chain from OO.ui.Element (if you look at that method, the description of that parameter is the same) -- and at that level, it definitely allows for any sort of data, be it a string or an object.
However, when dealing with specific cases, like that of the ComboBoxInputWidget (the terminology of "input widget" usually suggests something inside a form in OOjs-UI) means that putting the data as an object doesn't make sense usually. Unless your use case requires something very different, we usually want ComboBoxInputWidget to have the 'value' => 'label' pair, so it uses its 'data' property as the 'value' and expects a string.
As for the second part of your question, I am not 100% sure I understood it (please correct me if so) but from what I understand, if you set your OO.ui.MenuOptionWidget items with their data as the value ('red' / 'yellow' / 'green' etc) and the label as the mw.msg that the user sees, then it should work out of the box.
So if you look at the example given in the docs for ComboBoxInputWidget you can set your item's data to the color (value) and the label to the word you want to display, and when the user picks an option, the label needs to show in the ComboBoxInputWidget.
Be aware, though, that if you listen to 'choose' or 'select' event from this input widget, you get the selected item, so if you're projecting that choice into some other input, you should ask for the label (item.getLabel()) and not the data.
Thanks for your explanations.
It looks like OOUI does not support something like that (yet).
An example: You've got a option value "Q7186" and a label value "Marie
Curie". When the user selects "Marie Curie" the user interface will
say: "Q7186". A user might be confused.
I know that some UI frameworks handle this by showing the label of
the selected item to the user in a DIV element or something else and
storing the actual value in an internal variable. When it comes to form
submission and the need to provide an actual "value string" they use a
hidden field and maybe a valueField config option in case the "value"
is not a string but an object (thus allowing to set the string value of
the hidden field from a field within the value object).
Maybe that's something for future development.

Why does min attribute cause ngChange to be called?

I have the following input field:
<input type="number"
class="menu-control validate"
style="width: 50px;"
ng-disabled="!ctrl.editable()"
min="1"
ng-change="ctrl.updateBookingPriceRequest()"
ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 2000, 'blur': 0 }}"
ng-model="extra.quantity" />
My problem is the min directive. While it's there, angular starts repeatedly calling ng-change without the input having changed at all (not good since I'm performing an ajax call on change). If I remove min it works just fine, and I don't have the same problem with max.
It doesn't seem to matter if the model is above or below min initially.
Can anyone see something that I can't?
Edit:
I tried making my change function do nothing, and it stopped the problem, so it must be due to my code. But what I don't understand is why it works fine without min!
this.updateBookingPriceRequest = function () {
_this.prices.getBookingPrice(_this.bookingPrice).then(function (response) {
if (response.successful) {
_this.bookingPrice = response.data;
_this.bookingPrice.mooringExtras.bookingExtras.forEach(function (extra) {
var feature = _this.features.filter(function (f) { return f.featureId === extra.mooringFeatureId; })[0];
extra.unitOfMeasureId = feature.unitOfMeasureId;
extra.pricedQty = feature.pricedQuantity;
extra.pricingType = feature.pricingType;
});
if (_this.bookingPrice.mooringDiscounts) {
_this.bookingPrice.mooringDiscounts.forEach(function (discount) {
discount.discountName = _this.harborDiscounts.filter(function (x) { return x.id === discount.discountModelId; })[0].name;
});
}
}
else
_this.Error.showErrorMessage('Error getting booking price: ' + response.message);
});
};
The "extra" object on which the model is a property is changed in the function, however the "quantity" property remains the same. Could this cause ng-change to be triggered?
Edit to show how objects are defined (see comment by valepu):
The extra object(s) are in an array (my input field is inside a repeater, but in my current test there is only one object in the array), which is defined on a property called mooringExtras, which in turn is a property of a bookingPrice object, which is updated via an http request when ng-change gets called (see code). I know it gets complicated, my apologies for not knowing how to simplify it better.
The extra object contains a number of properties, with "quantity", a number, being the model for the input.
Here is an JSON of the extra object:
{"id":401,"bookableFeatureId":13,"mooringFeatureId":4,"featureName":"Wi-fi","bookingId":1104,"booked":true,"price":100.00,"totalAmount":300.00,"days":8,"quantity":3,"currencyUnit":"SEK","created":1460542055177}
Every time ng-change is called the bookingPrice object is changed, however, the value of extra.quantity remains the same.
I have just realized that in your onChange function you do this:
_this.bookingPrice = response.data;
Which, according to what you wrote in your question, is the object containing the array you iterate on to create your inputs.
When you completely replace the object, ng-repeat will create the inputs from scratch. When you have min set in your input this will trigger ng-change on input creation if the starting input is not valid (angular will set the ng-model to undefined in this case), which will change the whole array, which will trigger ng-repeat again, recreating inputs with a min attribute, which will trigger ng-change again and so on...
Normally ng-repeat generates an hash of the object to track changes on the data it's iterating on, if you completely replace it then it will think you deleted the old object and put in a new one (even though they have the same data), by using track by extra.id will tell ng-repeat that even though you replaced the object, they actually didn't change (they still have the same .id) and won't recreate the objects from scratch but, this is a fix but it's probably a good practice to just replace the values of the current array.
I have managed to recreate your issue in this plunkr: http://plnkr.co/edit/XyEyGTvuYKyz1GGmWjuP?p=preview
if you remove the line:
ctrl.objects = [{quantity: 0, price: 0, booked: true}, {quantity: 0, price: 0, booked: true}];
it will work again
I'm still not quite sure why the problem only occurred with the min attribute on the field, but by adding "track by extra.id" to the ng-repeat that wrapped the input field, I solved the problem. I guess when the "extra" object, on which the model was a property, changed, angular regenerated the input field, triggering ng-change. By tracking by an unchanging id, angular doesn't need to regenerate the input field since the id remains the same, thus not triggering ng-change.
I'll accept this as my answer, but if anyone can explain why it worked without min, I will happily accept their answer instead.

Two way binding with AngularJS select for a child object

I have an angular form with this select element:
<select ng-model="expense.category" ng-options="category.code for category in categories"></select>
where expense is my object to create/edit in the form and expense.category points to child object category (with properties "id" and "code"), which is to be selected by this control.
This all works perfect for creating new expenses - category gets saved just the way it should be. When loading an expense for editing everything is fine also - the expense has its category in the controller. But on the form the select does not get pre-filled with the correct category-entry, it is empty, although all the categories are loaded into the select element. So binding works only one way (from form to model) but not in the other way (from model to form) for the select field. All the other fields (properties of expense) get filled correctly into the edit form though.
Any ideas how to solve this?
The expense.category model must be an existing item in the categories array.
What you can do to make this happen is to search for the right item in the array (with the help of the code property) and replace your existing copy of the category with the reference in the categories array.
This could be done like this:
myApp.controller('ctrl', function($scope) {
$scope.categories = [{code: 'sport'}, {code: 'music'}, {code: 'culture'}];
$scope.expense = {category: {code: 'sport'}};
function replaceWithReference(expense) {
var code = expense.category.code;
for(var i=0; i < $scope.categories.length; i++) {
if($scope.categories[i].code === code) {
expense.category = $scope.categories[i];
return;
}
}
}
replaceWithReference($scope.expense);
});
Here is the working jsfiddle-demo
Comparison between ng-options and ng-model is done by reference, not value. I suspect your expense.category, when loaded, is a separate entity from the ones in the categories array.
You should probably populate your expense.category with a category from categories.
Another option would be to replace the select with a dropdown. This would represent a bit more code (you would have to handle the binding yourself on the ng-click) but would give you more control on comparison and display.

Why does AngularJS fail to initialize Select Option (drop list) when ngmodel is used with nested object?

I have a complex object which contains some nested arrays of objects.
Inside one of those inner objects is a value which is the id for an item in another list.
The list is just a look-up of Codes & Descriptions and looks like the following:
[
{ "id": 0, "value": "Basic"},
{ "id": 1, "value": "End of Month (EOM)"},
{ "id": 2, "value": "Fixed Date"},
{ "id": 3, "value": "Mixed"},
{ "id": 4, "value": "Extra"}
]
However, I only carry the value in the nested object.
The Select Option list (drop list) will display all of the values in the previous list so the user can make his/her selection.
Binding Via ng-model
I then bind the value returned from the Select/Option directly to the nested object.
That way, when the user makes a selection my object should be updated so I can just save (post) the entire object back to the server.
Initialization Is The Problem
The selection does work fine and I can see that the values are all updated properly in my nested object when a user selects. However, I couldn't get the UI (select/option) to be initialized to the proper value when I retrieved the (nested) object from the server.
Input Type Text Was Binding Properly
My next step was to add an text box to the form, bind it to the same ng-model and see if it got initialized. It did.
This is a large project I was working on so I created a plnkr.co and broke the problem down. You can see my plnkr in action at: http://plnkr.co/edit/vyySAmr6OhCbzNnXiq4a?p=preview
My plunker looks like this:
Not Initialized
I've recreated the exact object from my project in Sample 1 and as you can see the drop list is not selected properly upon initialization since the value(id) is actually 3, but the drop list doesn't show a selected value.
Keep In Mind: They Are Bound And Selecting One Does Update Values
If you try the plunker you will see that the values are bound to the select/option list because even in the samples which do not initialize properly, when you select an item the other bound items are instantly updated.
Got It Working : Hack!
I worked with it a long time and kept created fake objects to see which ones work and which don't.
It only works, once I changed the value object to one that looks like the following:
$scope.x = {};
$scope.x.y = 3;
Now, I can bind x.y (ng-model="x.y") to select/option and it initializes the select/option list as you would expect. See Sample 2 in the plunker and you will see that "mixed" (id value 3) is chosen as expected.
Additional One Works
I also learned that the following will work:
$scope.lastObj = {};
$scope.lastObj.inner = [];
$scope.lastObj.inner.push(3);
More Nesting
In that case I can bind lastObj.inner to the select/option list and again you can see in Example 3 that it still works. That is an object which contains an array which contains the value.
Minimal Nesting That Fails
However, Sample 4 in the plunker displays the final amount of nesting which does not work with the AngularJS binding.
$scope.thing = {};
$scope.thing.list=[];
$scope.thing.list.push({"item":"3"});
This is an object which contains an array which contains an object with a value. It fails to bind properly on the select/option but not the text box.
Can Anyone Explain That, Or Is It A Bug, Or Both?
Can anyone explain why the select/option fails to bind / initialize properly in this case?
A Final Note
Also, be strong and do not try to explain why you think the nesting of my objects should be different. That's not under discussion here, unless you can tell me that JavaScript itself does not support that behavior.
However, if you can explain that Angular cannot handle this deep of nesting and why, then that is a perfectly valid answer.
Thanks for any input you have.
You are messed up with primitive types. It means you should insert
$scope.vm.currentDocument.fieldSets[0].fields.push({"value":3});
instead of
$scope.vm.currentDocument.fieldSets[0].fields.push({"value":"3"});
Note the difference of {"value":3} and {"value":"3"}
First one defines an object with property "value" with Integer type, and the second one defines an object with property "value" with String type. As Angular checks type match, it becomes that ("3" === 3) evaluates as false, this is why angular cant find selected option.
This is how it supposed to work.
Also note that - as Armen points out - objects are passed by reference as opposed to primitives which are pass-by-value.
Because of this fact, normally initializing a select box via ngModel from JSON (say, from a $resource record) you will need to set the model value to the specific array element/object property that is being internally checked for equality by Angular to the elements in the ngOptions (or repeated options elements with ng-values assigned to the same record objects). No two distinct objects in JS are considered equal, even if they have identical property names/values.
Angular has one way around this: use the "track by" clause in your ngOptions attribute. So long as you have a guaranteed-unique value (such as a record index from a db) Angular will check the value of the property between the model value and the records in ngOptions.
See https://docs.angularjs.org/api/ng/directive/select for more.

How can I bind the selected text of a Select box to an object's attribute with Knockout JS, or anything else?

I have a select box pull down that I'm populating with a JSON list returned from a stored procedure, but unfortunately when I update the linked object I need to return the selected text of the pulldown, not the selected index like one would think (poor database design, but I'm stuck with it for now and cannot change it).
Does anyone have any ideas what I can do to keep the selected text synced with the appropriate javascript object's attribute?
You could keep both, the value and the text, if you use subscribers.
For instance, if each of your javascript objects look like this:
var optionObject = {
text:"text1"
value: 1
}
Then your binding would look like:
Where 'OptionsObjects' is a collection of optionObject and selectedOption
has two observable properties: text and value.
Finally you subscribe to the value property of the selectedOption:
viewModel.selectedOption.value.subscribe(function(newValue){
var optionText = viewModel.OptionsObjects[newValue].text;
viewModel.selectedOption.text(optionText);
});
Then if you want to see the new selected option text when the value is changed,
you could have a binding as follows:
<span data-bind:"text:selectedOption.text"></span>
In your particular case you would return selectedOption.text().
So yes, you got what I was getting at. Use the text as the value for the select options rather than using an index. The value really should be something useful, I can't think of any case where I've ever used an index. A number sure, but a number that relates to the application's models in some way (like an id from a database), not to the number of items in the select box.
Well done.

Categories

Resources