How to render <option value="?"></option> as an error in AngularJS?

2.3k Views Asked by At

I have a select with options. I would like to initialize the select value regarding to the user logged ng-init="own = credentials.user_id".

It works when the user_id is equals to the value in the select. But when it's not equals, the value selected is "?". So there is no error whereas it should be.

I would like to not selected this "?" and invalidate the form.

Do you have a solution ?

<div ng-class="{ 'error-select': myForm.lead.$error.required == true }"><span>Leader</span></div>

<select ng-model="lead" name="lead" ng-options="leader.user_id as leader.user_display_name for leader in leaders" ng-init="lead = credentials.user_id" required></select> 

<div ng-class="{ 'error-select': myForm.own.$error.required == true }"><span>Leader</span></div>

<select ng-model="own" name="own" ng-options="own.user_id as own.user_display_name for own in owns" ng-init="own = credentials.user_id" required></select> 

OUTPUT : user_id = 4

  <select t ng-model="lead" name="lead" ng-options="leader.user_id as leader.user_display_name for leader in leaders" ng-init="lead = credentials.user_id" required="" class="ng-pristine ng-valid ng-valid-required">
   <option value="0">Alexandre</option>
   <option value="1">Antoine</option>
   <option value="2">Zaaza</option>
   <option value="3">toro</option>
   <option value="4"selected="selected">Steffi</option>
  </select>

  <select t ng-model="own" name="own" ng-options="own.user_id as leader.user_display_name for own in owns" ng-init="own = credentials.user_id" required="" class="ng-pristine ng-valid ng-valid-required">
   <option value="?" selected="selected"></option>
   <option value="1">Alexandre</option>
   <option value="2">Steffi</option>
  </select>
2

There are 2 best solutions below

0
On BEST ANSWER

This requirement is not so trivial and there could be several approaches to it. (I am not really sure the chosen one is the best.)

TL;DR
Here is the working demo.


What's going on ?

First things first: we need to understand what is actually happening.
A control's validity state ($valid/$invalid) depends on the $modelValue or $viewValue. The key-point here is that is does not depend on the elements actual value property.

When you try to set the $modelValue to something that is not in the users array (e.g. 3), i.e. that there is no valid <option> element available, then Angular will set $modelValue and $viewValue to 3, but the <select>'s value will be either ? (if you haven't added an empty <option> element) or whatever the value of the explicitely added empty <option>.

But since tha validation is not based on the elements value everything seems $valid from a required standpoint.


A solution

One solution is to create a custom directive that has access to the elements ngModelController and "hooks" itself to the $render() function.
This directive will check if the element's value is ? (which indicates an invalid $modelValue) and if so, set the $modelValue/$viewValue to something actually empty, which will correctly update the validity of the control (i.e. an invalid $modelValue will trigger our directive's handler to reset that to something empty and the required constaint won't be fulfilled).

Here is the code:

app.directive('select', function () {
    return {
        restrict: 'E',
        require: '?ngModel',
        priority: 10,
        link: function postLink(scope, elem, attrs, ctrl) {
            if (!ctrl) { return; }
            var originalRender = ctrl.$render.bind(ctrl);
            ctrl.$render = function () {
                originalRender();
                if ((elem.val() === '?') && !ctrl.$isEmpty(ctrl.$modelValue)) {
                    ctrl.$setViewValue(undefined);
                }
            };
        }
    };
});

See, also, this short demo.


A few points to note regarding the above implementation:

  1. I chose to name my directive select so it will be applied to all select elements (alongside the native Angular select directive - yes that's possible). If you prefer to apply it to specific controls only, then rename it, restrict it to attributes (restrict: 'A') and add it as an attribute to your elements.

  2. Due (or thanks) to require: '?ngModel', the control will be ignored if it is not bound to an ngModel.

  3. Since our directive augments the ngModelController.$render() method, which is defined by the select directive (executing at priority 0), we need to force our directive's link function to be executed after the select directive. Thus, it needs a higher priority, because "post-link functions are run in reverse [priority] order.".

3
On

If you use ng-required , then angular will accomplish the task for you. So, when ng-required is set true for the select, and if no value is set for select then $error.required of that model in the form will be set to true.

<select ng-model="lead" name="lead" ng-options="leader.user_id as leader.user_display_name for leader in leaders" ng-init="lead = credentials.user_id" ng-required="true"></select> 

Please find fiddle for the same. In the script if you comment the code

$scope.credentials=$scope.leaders[0];

the error flag will be set and form will be invalid and vice-versa.