控件 (input, select, textarea) 是用户输入数据的一种方式。一个表单就是多个控件的集合,用来组织相关的控件。

表单和控件提供验证服务,在用户输入信息有误的时候进行提示。 这提升了用户体验,因为我们在第一时间告知用户什么地方出错了、如何修正错误。 记住,尽管客户端(浏览器)验证在用户体验方面起了重要作用,但是,它很容易被绕过,因此是不能信任的。 为了应用的安全,服务端的验证仍然是必须的。


简单的表单

理解双向绑定的关键指令是ngModel. 指令ngModel通过维护“数据到视图”的同步以及“视图到数据”的同步实现了双向绑定。 另外,它还提供了API来让其他指令扩展其行为。

源码

<!doctype html>
<html ng-app>
  <head>
    <script src="http://code.angularjs.org/1.2.25/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Controller">
      <form novalidate class="simple-form">
        Name: <input type="text" ng-model="user.name" /><br />
        E-mail: <input type="email" ng-model="user.email" /><br />
        Gender: <input type="radio" ng-model="user.gender" value="male" />male
        <input type="radio" ng-model="user.gender" value="female" />female<br />
        <button ng-click="reset()">RESET</button>
        <button ng-click="update(user)">SAVE</button>
      </form>
      <pre>form =  {{user | json}} </pre>
      <pre>master =  {{master | json}} </pre>
    </div>
  </body>
</html>
function Controller($scope) {
  $scope.master = {};

  $scope.update = function(user) {
    $scope.master = angular.copy(user);
  };

  $scope.reset = function() {
    $scope.user = angular.copy($scope.master);
  };

  $scope.reset();
}

效果

其中 novalidate 属性用于禁用浏览器自带的表单验证功能。


使用 CSS 类

为了允许对表单和控件自定义样式, ngModel 增加了如下的CSS类: - ng-valid - ng-invalid - ng-pristine - ng-dirty

接下来的例子中使用这些CSS类来表明每一个表单控件是否有效。 在例子中,user.nameuser.email 都是必需的(required),它们仅在成为脏数据(dirty)时才会显示红色背景,但不会在初始状态下(也为空)显示。 这样就防止了用户在尚未与该控件进行交互,而该控件的当前状态恰好不符合验证条件的时候,出现不恰当的错误提示。

源码

<!doctype html>
<html ng-app>
  <head>
    <script src="http://code.angularjs.org/1.2.25/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Controller">
      <form novalidate class="css-form">
        Name:
          <input type="text" ng-model="user.name" required /><br />
        邮箱: <input type="email" ng-model="user.email" required /><br />
        性别: <label><input type="radio" ng-model="user.gender" value="male" />男</label>
        <label><input type="radio" ng-model="user.gender" value="female" />女</label><br />
        <button ng-click="reset()">重置</button>
        <button ng-click="update(user)">保存</button>
      </form>
    </div>
    
    <style type="text/css">
      .css-form input.ng-invalid.ng-dirty {
        background-color: #FA787E;
      }
    
      .css-form input.ng-valid.ng-dirty {
        background-color: #78FA89;
      }
    </style>
  </body>
</html>
function Controller($scope) {
  $scope.master = {};

  $scope.update = function(user) {
    $scope.master = angular.copy(user);
  };

  $scope.reset = function() {
    $scope.user = angular.copy($scope.master);
  };

  $scope.reset();
}

效果


与表单绑定和控制状态

一个表单是一个 FormController 的实例。 这个表单实例可以通过 'name' 属性装载到scope中去。

类似的,一个拥有 api/ng.directive:ngModel 指令的输入控件,包含一个NgModelController实例。 这样一个控件的实例可以使用 'name' 属性装载到表单实例中去,作为表单实例的一个字段。name属性指定了其在表单实例中的字段名。

这意味着,在视图中使用基本的数据绑定形式就可以访问到表单和控件中的内部状态。

这让我们可以为上面的例子扩展以下功能:

  • 重置按钮 只在表单有改变的时候才可用
  • 保存按钮 只在表单有改变且数据有效的时候才可用
  • user.emailuser.agree 自定义错误提示信息

源码

<!doctype html>
<html ng-app>
  <head>
    <script src="http://code.angularjs.org/1.2.25/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Controller">
      <form name="form" class="css-form" novalidate>
        Name:
          <input type="text" ng-model="user.name" name="uName" required /><br />
        E-mail:
          <input type="email" ng-model="user.email" name="uEmail" required/><br />
        <div ng-show="form.uEmail.$dirty && form.uEmail.$invalid">Invalid:
          <span ng-show="form.uEmail.$error.required">Tell us your email.</span>
          <span ng-show="form.uEmail.$error.email">This is not a valid email.</span>
        </div>
    
        Gender: <input type="radio" ng-model="user.gender" value="male" />male
        <input type="radio" ng-model="user.gender" value="female" />female<br />
    
        <input type="checkbox" ng-model="user.agree" name="userAgree" required />
        I agree: <input ng-show="user.agree" type="text" ng-model="user.agreeSign"
                  required /><br />
        <div ng-show="!user.agree || !user.agreeSign">Please agree and sign.</div>
    
        <button ng-click="reset()" ng-disabled="isUnchanged(user)">RESET</button>
        <button ng-click="update(user)"
                ng-disabled="form.$invalid || isUnchanged(user)">SAVE</button>
      </form>
    </div>
  </body>
</html>
function Controller($scope) {
  $scope.master = {};

  $scope.update = function(user) {
    $scope.master = angular.copy(user);
  };

  $scope.reset = function() {
    $scope.user = angular.copy($scope.master);
  };

  $scope.isUnchanged = function(user) {
    return angular.equals(user, $scope.master);
  };

  $scope.reset();
}

效果


自定义验证

Angular提供了一些常用的html5输入控件的验证实现:(text, number, url, email, radio, checkbox), 以及一些用于验证的指令 (required, pattern, minlength, maxlength, min, max).

定义你自己的验证器可以首先定义一个指令,这个指令为 'ngModel' 控制器添加自定义验证方法。 我们通过指定一个依赖来获得对这个控制器的引用,从下面的例子中可以看出。

我们的验证在两个时机触发:

下面的示例中我们创建了两个指令。

  • 第一个指令是 'integer' 整形数字,它验证了输入是否是一个合法的整形数字。 例如,'1.23'是一个非法值,因为它包含小数部分。 注意,我们并没有对数组进行压栈操作,而是插入数组的开头。 这是因为我们希望指令能成为第一个解析和处理被控制的值,而不希望在我们执行验证前,值已经在其他环节被转换成了数字。
  • 第二个指令是 'smart-float' 智能浮点数。 它能解析 '1.2' 和 '1,2' 并将其转换成合法的浮点数 '1.2'. 注意,我们此时不能在HTML5的浏览器中使用 'number' 类型,因为在这种情况下,浏览器不会允许用户输入像 '1,2' 这种被认为是不合法的数字。

源码

<!doctype html>
<html ng-app="form-example1">
  <head>
    <script src="http://code.angularjs.org/1.2.25/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Controller">
      <form name="form" class="css-form" novalidate>
        <div>
          大小 (整数 0 - 10):
          <input type="number" ng-model="size" name="size"
                 min="0" max="10" integer /><br />
          <span ng-show="form.size.$error.integer">这是无效的数字!</span>
          <span ng-show="form.size.$error.min || form.size.$error.max">
            值必需在0到10之间!</span>
        </div>
    
        <div>
          长度 (浮点):
          <input type="text" ng-model="length" name="length" smart-float />
          <br />
          <span ng-show="form.length.$error.float">
            这是无效的浮点数!</span>
        </div>
      </form>
    </div>
  </body>
</html>
var app = angular.module('form-example1', []);

var INTEGER_REGEXP = /^\-?\d+$/;
app.directive('integer', function() {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {
      ctrl.$parsers.unshift(function(viewValue) {
        if (INTEGER_REGEXP.test(viewValue)) {
          // 验证通过
          ctrl.$setValidity('integer', true);
          return viewValue;
        } else {
          // 验证不通过 返回 undefined (不会有模型更新)
          ctrl.$setValidity('integer', false);
          return undefined;
        }
      });
    }
  };
});

var FLOAT_REGEXP = /^\-?\d+((\.|\,)\d+)?$/;
app.directive('smartFloat', function() {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {
      ctrl.$parsers.unshift(function(viewValue) {
        if (FLOAT_REGEXP.test(viewValue)) {
          ctrl.$setValidity('float', true);
          return parseFloat(viewValue.replace(',', '.'));
        } else {
          ctrl.$setValidity('float', false);
          return undefined;
        }
      });
    }
  };
});

效果


实现自定义form控件(使用 'ngModel')

Angular实现了所有基本的HTML表单控件((input, select, textarea),它们在大多数情况下都很有效。 然而,如果你需要更多的灵活性,你可以使用指令来实现你的自定义表单控件。

为了能让自定义控件能够与'ngModel'正常工作,达到双向绑定的效果,它需要:

  • 实现 '$render' 方法,它负责在数据传递给方法NgModelController#$formatters之后渲染数据。
  • 调用 '$setViewValue' 方法,在任何用户与控件交互后,模型需要更新的时候调用。这通常在一个DOM事件监听器里完成。

查看 $compileProvider.directive 获得更多的信息。

接下来的例子展示了如何为一个可编辑元素添加双向绑定。

Source

<!doctype html>
<html ng-app="form-example2">
  <head>
    <script src="http://code.angularjs.org/1.2.25/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div contentEditable="true" ng-model="content" title="Click to edit">Some</div>
    <pre>model = </pre>
    
    <style type="text/css">
      div[contentEditable] {
        cursor: pointer;
        background-color: #D0D0D0;
      }
    </style>
  </body>
</html>
angular.module('form-example2', []).directive('contenteditable', function() {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {
      // 视图 -> 模型
      elm.on('blur', function() {
        scope.$apply(function() {
          ctrl.$setViewValue(elm.html());
        });
      });

      // 模型 -> 视图
      ctrl.$render = function() {
        elm.html(ctrl.$viewValue);
      };

      // 从DOM中初始化数据
      ctrl.$setViewValue(elm.html());
    }
  };
});

效果