2021年1月12日 星期二

實作 angularJs 自訂驗證 select option list - 檢查 ng-model 的值有無在 option list 中

 AngularJs 原生的 select option  的 required 驗證,

只能對 ng-model 的值為 undefined , "", null , NaN來做 required error。

參考:

ngRequired

ngModel.NgModelController 的 $isEmpty

但如果 ng-model 不是 undefined , "", null , NaN ,且其值不在 select option 中,

此時希望能檢查出驗證錯誤的話 (validation) ,

就需要自己實作。


例如可能的情境為:

如果 option list 裡都是不為 0 的數字,當 ng-model 是數字 0 時 (可能是程式常態 assign 賦值的),此時 required (或 ng-required) 就無法驗證出 required 的 validation error。


以下為我寫出來的簡單實作,

建立了一個名為 selectOptionCheckValidator 的 directive 來進行驗證:



html : 

<div ng-app="app" ng-controller="controller as ctrl">
  <form name="form">
    <label><input type="radio" name="optionType" ng-value="1" ng-model="ctrl.optionType" /> Set option list 1</label>
    <label><input type="radio" name="optionType" ng-value="2" ng-model="ctrl.optionType" /> Set option list 2</label>

    <select name="mySelector" ng-model="ctrl.selectedVal" ng-options="selectOption.optionValue.id as selectOption.title for selectOption in ctrl.selectOptionList" select-option-check-validator ng-required="true">
    </select>
    
    <button ng-click="ctrl.selectedVal = 0">
      Set selectedVal to 0
    </button>

    <div>
      ctrl.selectedVal : {{ctrl.selectedVal}}
    </div>

    <div>
      form.mySelector.$error.selectedValueInOptionList : {{form.mySelector.$error.selectedValueInOptionList ? "invalid" : "valid"}}
    </div>
    <div>
      form.mySelector.$error.selectedValueInOptionList : {{form.mySelector.$error.required ? "invalid" : "valid"}}
    </div>

  </form>
</div>



Javascript :
angular.module("app", [])
  .controller("controller", ["$scope", function($scope) {
    var self = this;
    self.optionType = 1;
    self.selectedVal = 0;

    self.optionList1 = [{
      optionValue: {
        id: 11
      },
      title: "id 11"
    }, {
      optionValue: {
        id: 12
      },
      title: "id 12"
    }];

    self.optionList2 = [{
      optionValue: {
        id: 21
      },
      title: "id 21"
    }, {
      optionValue: {
        id: 22
      },
      title: "id 22"
    }];

    self.selectOptionList = [];

    $scope.$watch(angular.bind(this, function() {
      return this.optionType;
    }), function(newVal, oldVal) {
      //if value of selectedValue can not be found in self.selectOptionList,
      // selectedValue will be assigned to null.
      if (newVal == 1) {
        self.selectOptionList = self.optionList1;
      } else if (newVal == 2) {
        self.selectOptionList = self.optionList2;
        self.selectedVal = 21;
      }
    });
  }]).directive('selectOptionCheckValidator', ["$parse", function($parse) {
    return {
      require: 'ngModel',
      link: function(scope, element, attrs, ngModel) {

        var regex = /(.+)\sas\s(.+)\sfor\s(.+)\sin\s(.+)/;
        var matchString = regex.exec(attrs.ngOptions);
        var collectionExpression = matchString[4]; // "ctrl.selectOptionList"
        var arryItemVarableName = matchString[3]; // "selectOption"
        var valueExpression = matchString[1]; // "selectOption.optionValue.id"

        scope.$watch(collectionExpression, function(newValue, oldValue) {
          checkIsSelectedValueInOptionList();
        });

        element.on("change", function() {
          scope.$apply(function() {
            checkIsSelectedValueInOptionList();
          });
        });

        function checkIsSelectedValueInOptionList() {
          var isSelectedValueInOptionList = false;
          var selectedValue = $parse(attrs.ngModel)(scope);
          var optionList = $parse(collectionExpression)(scope);

          if (optionList) {
            isSelectedValueInOptionList = optionList.some(function(option) {
            
              // create a custom scope object to render optionValue from valueExpression
              var optionTempObject = new function(){
              	this[arryItemVarableName] = option;
              };
              
              var optionValue = $parse(valueExpression)(optionTempObject);
              
              return optionValue == selectedValue;
            });
            
            ngModel.$setValidity("selectedValueInOptionList", isSelectedValueInOptionList);
          }          
        }
      }
    };
  }]);



在這個例子裡,設置了兩個 option list 作為 <select> 的 option 選項,
當按下例子中的兩個 input type="radio" 時會改變 optionType ,
而 optionType 被改變時,會根據所勾選的值來重新設置不同的 option 選項給 <select>。

在 html <select> 的下方,我把 selectedVal 和 <select> 的 validation error 印出來以利觀察。

可以看到的是,
一開始 selectedVal 被設定成 0,但兩個 option 選項中都並沒有 0 這個值存在,
如果我們希望當 selectedVal 不在 option 選項中時能夠被驗證出來產生 validation error,
就必須自己實作 directive 來達成,即在這個例子中的 selectOptionCheckValidator directive。

AngularJs 原生的 required (或 ng-required) 在 selectedVal = 0 時,
會無法產生 validation error,因為它的實作是在當 selecctedVal = undefined, null, "", NaN 時才會產生 validation required error.
-------------------------------------------------------------------------------------------------------------------
另一個可以注意到的是,
我在這裡對按下第二個 input type="radio" 時,對 selectedVal 賦與 option list 2 中的值,
而按下第一個 input type="radio" 時沒有賦值。
此時可以觀察到當在一個 AngularJs Render 生命週期中,
如果 option list 被改變了,且 selectedValue 在 option list 中找不到的話,
selectedValue 就會被重新設定成 null。

Note

此例中用來解析 ng-option 字串的正規表達式較為簡易,

如果想知道完整的正規表達式,也就是可以解析 AngularJs ng-option 所有可能字串的正規達式,

可以參考官方源碼,其中可以看到正規表達式為:

^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([$\w][$\w]*)|(?:\(\s*([$\w][$\w]*)\s*,\s*([$\w][$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$

圖例



沒有留言 :

張貼留言