在指令中的输入上使用ngModel,保持ng- *兼容性

时间:2016-02-11 10:40:21

标签: javascript angularjs angularjs-directive angular-ngmodel

我正在试图制作一个自定义指令,它实际上是输入字段的包装器(以简化格式化,封装动画等)。

一个目标是使用ngModel,因此我的指令也将与ng-maxlength,ng-required和类似指令兼容,具体取决于ng-Model。

我用我当前的状态创建了这个plunkr: http://embed.plnkr.co/xV8IRqTmQmKEBhRhCfBQ/

我的问题是,ng-required似乎有效,但只会使完整表单无效(以便form.$invalid变为true),但元素本身form.element.$invalid仍然是{{} 1}}。

此外,ng-maxlength / ng-minlength似乎根本没有任何影响。

我在这里缺少什么?任何提示欢迎:)

2 个答案:

答案 0 :(得分:2)

大家好,非常感谢您的回答!

我终于找到了遗漏的部分:名称属性表单用来引用元素绝不能在内部输入字段上。

它必须驻留在携带mg模型的外部元素上,该模型也获得其他指令(与ng模型交互)。

因此,为了更详细地说明这一点,在我的模板看起来像之前:

<span class="custom-input-element">
<label for="{{elementId}}-input">{{elementLabel}}<span class="required-marker" ng-if="elementRequired">*</span></label>

 <input id="{{elementId}}-input" type="text" name="{{elementName}}" ng-trim ng-model="value" ng-init="focused = false" ng-focus="focused = true" ng-blur="focused = false"/>
</span>

使用了

 <custom-input id="foldername" name="foldername" label="Folder Name:" 
   ng-model="folder.name" ng-maxlength="15" ng-required="true">   </custom-input>

请注意name={{elementName}}基本上覆盖了我的指令标签上的name="foldername"

从指令模板中删除后,表单引用了我的指令和我的指令上的ngModel进行验证 - 输入和内部ng模型保持隐藏状态。因此,与其他指令(如ng-maxlength和mg-minlength)以及自定义指令/验证器的交互可以按预期工作。

现在,不仅表单无效,而且每个元素都以预期的方式验证。

我更新了我的plunker,现在一切正常工作:http://embed.plnkr.co/i3SzV8H7tnkUk2K9Pq6m/

感谢您的时间和非常宝贵的意见!

答案 1 :(得分:0)

我创建了一个有效的,我会尝试向您展示代码的相关部分。

真正讨厌的一点是将输入和验证重新附加到父控制器的形式。

为此,我必须从angular:

中获取一堆私有代码
 /**
   * start cc from angular.js to modify $setValidity of ngModel to retrieve the parent form...
   */
  var VALID_CLASS = 'data-ng-valid',
    INVALID_CLASS = 'data-ng-invalid',
    PRISTINE_CLASS = 'data-ng-pristine',
    DIRTY_CLASS = 'data-ng-dirty',
    UNTOUCHED_CLASS = 'data-ng-untouched',
    TOUCHED_CLASS = 'data-ng-touched',
    PENDING_CLASS = 'data-ng-pending';
  function addSetValidityMethod(context) {
      var ctrl = context.ctrl,
          $element = context.$element,
          classCache = {},
          set = context.set,
          unset = context.unset,
          parentForm = context.parentForm,
          $animate = context.$animate;
      classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS));
      ctrl.$setValidity = setValidity;
      function setValidity(validationErrorKey, state, controller) {
        if (state === undefined) {
          createAndSet('$pending', validationErrorKey, controller);
        } else {
          unsetAndCleanup('$pending', validationErrorKey, controller);
        }
        if (!isBoolean(state)) {
          unset(ctrl.$error, validationErrorKey, controller);
          unset(ctrl.$$success, validationErrorKey, controller);
        } else {
          if (state) {
            unset(ctrl.$error, validationErrorKey, controller);
            set(ctrl.$$success, validationErrorKey, controller);
          } else {
            set(ctrl.$error, validationErrorKey, controller);
            unset(ctrl.$$success, validationErrorKey, controller);
          }
        }
        if (ctrl.$pending) {
          cachedToggleClass(PENDING_CLASS, true);
          ctrl.$valid = ctrl.$invalid = undefined;
          toggleValidationCss('', null);
        } else {
          cachedToggleClass(PENDING_CLASS, false);
          ctrl.$valid = isObjectEmpty(ctrl.$error);
          ctrl.$invalid = !ctrl.$valid;
          toggleValidationCss('', ctrl.$valid);
        }
        // re-read the state as the set/unset methods could have
        // combined state in ctrl.$error[validationError] (used for forms),
        // where setting/unsetting only increments/decrements the value,
        // and does not replace it.
        var combinedState;
        if (ctrl.$pending && ctrl.$pending[validationErrorKey]) {
          combinedState = undefined;
        } else if (ctrl.$error[validationErrorKey]) {
          combinedState = false;
        } else if (ctrl.$$success[validationErrorKey]) {
          combinedState = true;
        } else {
          combinedState = null;
        }
        toggleValidationCss(validationErrorKey, combinedState);
        parentForm.$setValidity(validationErrorKey, combinedState, ctrl);
      }
      function createAndSet(name, value, controller) {
        if (!ctrl[name]) {
          ctrl[name] = {};
        }
        set(ctrl[name], value, controller);
      }
      function unsetAndCleanup(name, value, controller) {
        if (ctrl[name]) {
          unset(ctrl[name], value, controller);
        }
        if (isObjectEmpty(ctrl[name])) {
          ctrl[name] = undefined;
        }
      }
      function cachedToggleClass(className, switchValue) {
        if (switchValue && !classCache[className]) {
          $animate.addClass($element, className);
          classCache[className] = true;
        } else if (!switchValue && classCache[className]) {
          $animate.removeClass($element, className);
          classCache[className] = false;
        }
      }
      function toggleValidationCss(validationErrorKey, isValid) {
        validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
        cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true);
        cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false);
      }
    }
  function arrayRemove(array, value) {
      var index = array.indexOf(value);
      if (index >= 0) {
        array.splice(index, 1);
      }
      return index;
    }
  function isBoolean(value) {
      return typeof value === 'boolean';
    };
    var SNAKE_CASE_REGEXP = /[A-Z]/g;
    function snake_case(name, separator) {
      separator = separator || '_';
      return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
        return (pos ? separator : '') + letter.toLowerCase();
      });
    }
  function isObjectEmpty(obj) {
    if (obj) {
      for (var prop in obj) {
        if (obj.hasOwnProperty(prop)) {
          return false;
        }
      }
    }
    return true;
  };
  /**
   * end of cc
   */

然后在链接功能中:

function(scope, element, attrs, ctrl, transclude){
[...]
scope.form =  element.parent().controller('form');

var transcludedContent = transclude(scope.$parent);
// find the input
var fieldContent = findFormField(transcludedContent);

      var ngModelCtrl = angular.element(fieldContent).controller('ngModel');
      if(!ngModelCtrl){
          throw 'transcluded form field must have a ng-model';
      }

      addSetValidityMethod({
            ctrl: ngModelCtrl,
            $element: angular.element(fieldContent),
            set: function(object, property, controller) {
              var list = object[property];
              if (!list) {
                object[property] = [controller];
              } else {
                var index = list.indexOf(controller);
                if (index === -1) {
                  list.push(controller);
                }
              }
            },
            unset: function(object, property, controller) {
              var list = object[property];
              if (!list) {
                return;
              }
              arrayRemove(list, controller);
              if (list.length === 0) {
                delete object[property];
              }
            },
            parentForm: scope.form,
            $animate: $animate
          });

      scope.form.$addControl(ngModelCtrl);
      element.html(template);
       $compile(element.contents())(scope);
      element.find('.ng-form-field-content').append(transcludedContent);
      // remove the control from the form, otherwise an ng-if that hide an invalid input will block your form
      scope.$on(
              "$destroy",
              function handleDestroyEvent() {
                  scope.form.$removeControl(ngModelCtrl);
              });     

模板是一个变量,包含围绕输入的html。 (它会生成标签,如果需要可以开始,如果字段有效/无效,则显示支票或交叉标志,......)。

编辑: 根据我的指示,我可以做到:

<div my-directive>
    <input/textarea/select ng-model="", required/ng-required, ng-pattern, <custom directive validation>...
</div>

And it will give something like

<div my-directive>
   <label for=<input'sname>>Texte</label>
   <input [the input will all his attrs]/>
   [some extra content]
</div>

我甚至可以放置一些中间节点或者有多个指向相同ng模型的输入(如复选框/单选按钮),但它不适用于不同的ng模型。我没有把它推到那么远。