angular学习(十一)—— Form


control主要用于接收用户输入,例如input, select, textarea。而form是一组相关控件的集合。form和control提供验证服务,可以在用户提交表单到后端前得到无效输入的通知。

一个简单的form

form的验证要比服务端的验证好很多,因为更加及时得到反馈以便纠正错误。虽然客户端的验证体验很好,但是可以很容易做到规避验证,服务端的验证还是非常必要的,特别是那些安全要求高的应用。

理解angularjs双向绑定的关键点在于指令ngModel,ngModel提供了视图到模型,模型到视图的双向绑定,它还提供了一些api给其他指令,用于增强它的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html>
<head>
<meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="formExample">
<div ng-controller="ExampleController">
<form novalidate class="simple-form">
<label>Name: <input type="text" ng-model="user.name" /></label><br />
<label>E-mail: <input type="email" ng-model="user.email" /></label><br />
Best Editor: <label><input type="radio" ng-model="user.preference" value="vi" />vi</label>
<label><input type="radio" ng-model="user.preference" value="emacs" />emacs</label><br />
<input type="button" ng-click="reset()" value="Reset" />
<input type="submit" ng-click="update(user)" value="Save" />
</form>
<pre>user = {{user | json}}</pre>
<pre>master = {{master | json}}</pre>
</div>
<script>
angular.module('formExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.master = {};
$scope.update = function(user) {
$scope.master = angular.copy(user);
};
$scope.reset = function() {
$scope.user = angular.copy($scope.master);
};
$scope.reset();
}]);
</script>
</body>
</html>

注意novalidate可以禁用浏览器对表单进行原生性的校验。
除非输入通过校验,否则ngModel对应的值不会被设置。比如email类型的输入框必须使用user@domain的格式。

使用css class

为了表单和控件一样都有样式,ngModel增加了一些css class:

  • ng-valid:model有效
  • ng-invalid:model无效
  • ng-valid-[key]:通过$setValidity添加的每一个有效的key
  • ng-invalid-[key]:通过$setValidity添加的每一个无效的key
  • ng-pristine:控件值为初始状态
  • ng-dirty:控件的值已经做过了修改
  • ng-touched:控件已经被点击过并失去焦点
  • ng-untouched:控件还未被点击过
  • ng-pending:还有$asyncValidators未完成。

下面的例子使用css来显示控件的输入有效性。在这个例子中,user.name和user.email是必须输入的,但是只有当输入是模糊的时候(控件被点击并失去焦点之后)才会显示红色的背景,这确保了用户只有在和控件交互之后,而输入无法满足验证条件,才会提示错误,而在交换之前说不会提示错误的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!DOCTYPE html>
<html>
<head>
<meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="formExample">
<div ng-controller="ExampleController">
<form novalidate class="css-form">
<label>Name: <input type="text" ng-model="user.name" required /></label><br />
<label>E-mail: <input type="email" ng-model="user.email" required /></label><br />
Gender: <label><input type="radio" ng-model="user.gender" value="male" />male</label>
<label><input type="radio" ng-model="user.gender" value="female" />female</label><br />
<input type="button" ng-click="reset()" value="Reset" />
<input type="submit" ng-click="update(user)" value="Save" />
</form>
<pre>user = {{user | json}}</pre>
<pre>master = {{master | json}}</pre>
</div>
<style type="text/css">
.css-form input.ng-invalid.ng-touched {
background-color: #FA787E;
}
.css-form input.ng-valid.ng-touched {
background-color: #78FA89;
}
</style>
<script>
angular.module('formExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.master = {};
$scope.update = function(user) {
$scope.master = angular.copy(user);
};
$scope.reset = function() {
$scope.user = angular.copy($scope.master);
};
$scope.reset();
}]);
</script>
</body>
</html>

form和control的状态绑定

在angualr中,form是FormController的实例,form实例可以通过name属性发布到scope中。同样,带有ngModel指令的输入控件一样也是NgModelController实例,一样可以通过控件的name属性发布,作为form实例的一个属性。
这意味着form和控件的内部状态一样可以通过标准的绑定方式在视图中绑定。

我们来扩展一下上面的例子:

  • 在用户和控件交互之后,显示自定义的错误信息
  • 用户提交表单时显示自定义错误信息,即使用户和控件没有交互
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<!DOCTYPE html>
<html>
<head>
<meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="formExample">
<div ng-controller="ExampleController">
<form novalidate class="css-form" name="form">
<label>Name: <input type="text" ng-model="user.name" name="uName" required="" /></label><br />
<div ng-show="form.$submitted || form.uName.$touched">
<div ng-show="form.uName.$error.required">Tell us your name.</div>
</div>
<label>E-mail: <input type="email" ng-model="user.email" name="uEmail" required /></label><br />
<div ng-show="form.$submitted || form.uEmail.$touched">
<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: <label><input type="radio" ng-model="user.gender" value="male" />male</label>
<label><input type="radio" ng-model="user.gender" value="female" />female</label><br />
<label>
<input type="checkbox" ng-model="user.agree" name="userAgree" required="" />
I agree:
</label>
<input ng-show="user.agree" type="text" ng-model="user.agreeSign" required="" />
<br />
<div ng-show="form.$submitted || form.userAgree.$touched">
<div ng-show="!user.agree || !user.agreeSign">Please agree and sign.</div>
</div>
<input type="button" ng-click="reset(form)" value="Reset" />
<input type="submit" ng-click="update(user)" value="Save" />
</form>
<pre>user = {{user | json}}</pre>
<pre>master = {{master | json}}</pre>
</div>
<style type="text/css">
.css-form input.ng-invalid.ng-touched {
background-color: #FA787E;
}
.css-form input.ng-valid.ng-touched {
background-color: #78FA89;
}
</style>
<script>
angular.module('formExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.master = {};
$scope.update = function(user) {
$scope.master = angular.copy(user);
};
$scope.reset = function(form) {
if (form) {
form.$setPristine();
form.$setUntouched();
}
$scope.user = angular.copy($scope.master);
};
$scope.reset();
}]);
</script>
</body>
</html>

自定义model更新触发器

默认情况下,对内容对任何更改都会触发model的改变和表单的验证。你可以通过ngModelOptions指令绑定到特殊的事件列表中以重写这种行为。ng-model-options="{ updateOn: 'blur' }"表示仅仅在用户输入并且失去焦点时才会更新model并且进行表单验证。如果是多个事件,可以使用空格分开,例如:ng-model-options="{ updateOn: 'mousedown blur' }"

如果想保留默认的行为,而只是在默认事件上添加新的事件可以触发更新和验证,那么只需添加default作为一个普通的事件之一。
ng-model-options="{ updateOn: 'default blur' }"

下面是个重写立即更新对例子。只有在用户和输入框产生交互时(点击控件并且失去焦点时),输入框的更改才会更新到model中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html>
<head>
<meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<script>
angular.module('customTriggerExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.user = {name:"aa"};
}]);
</script>
<body ng-app="customTriggerExample">
<div ng-controller="ExampleController">
<form>
<label>Name:
<input type="text" ng-model="user.name" ng-model-options="{ updateOn: 'blur' }" /></label><br />
<label>
Other data:
<input type="text" ng-model="user.data" /></label><br />
</form>
<pre>username = "{{user.name}}"</pre>
<pre>userdata = "{{user.data}}"</pre>
</div>
</body>
</html>

延时model更新

你也可以通过ngModelOptions指令的debounce键延时更新和验证。这个延迟对于解析器,验证器和model的标签($dirty或$pristine.)同样起作用。

ng-model-options="{ debounce: 500 }"表示将最后一次内容的改变触发model更新和form验证之前要等上500毫秒。

如果使用了自定义的触发器,那么使用debounce对象对每一个事件加上自定义的消抖超时时间。这在某些特定情况下还是有用的,比如blur事件后需要立即更新和验证。语法如下:
ng-model-options="{ updateOn: 'default blur', debounce: { default: 500, blur: 0 } }"

注意,如果这些属性添加到某个元素中,那么这个元素的所有子元素和控件都会继承他的设定,除非子元素和控件重写这些属性。

下面的例子演示的是model更改的消抖,model将会在内容最后一次更改250毫米后进行更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html>
<head>
<meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<script>
angular.module('debounceExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.user = {};
}]);
</script>
<body ng-app="debounceExample">
<div ng-controller="ExampleController">
<form>
<label>Name:
<input type="text" ng-model="user.name" ng-model-options="{ debounce: 250 }" /></label><br />
</form>
<pre>username = "{{user.name}}"</pre>
</div>
</body>
</html>

自定义验证

AngularJS为html5最常用的输入类型(text, number, url, email, date, radio, checkbox)提供了基本的实现,以及一些用于验证的指令(required, pattern, minlength, maxlength, min, max)。

如果想自定义验证的指令,你需要在ngModelController实例的$validators对象中增加你自定义的验证函数。可以参照下面的例子。

$validators对象的每一个验证函数都有两个参数modelValue和viewValue,angularjs会在内部调用$setValidity函数,参数就是这个验证函数的返回值。每次输入发生变化($setViewValue被调用)或约束模型发生变化时,验证函数都会执行。$parsers和$formatters成功运行之后,验证会分别进行。如果验证失败,错误的key会保存在ngModelController.$error中。

此外,$asyncValidators对象处理异步的验证,比如需要通过http到后台请求验证数据的情况。添加到这个对象的验证函数必须返回一个promise,在有效时候resolve,无效时候reject。在异步验证的过程中,验证的key会被保存到ngModelController.$pending中。

下面的例子创建了两个验证指令:

  • integer指令验证输入是否是一个整数。例如,1.33就是个无效值。注意我们验证的事viewValue,控件中输入的值,而不是modleValue,这是因为类型为number的输入框会通过$parsers将viewValue转化为数字,如果我们输入的事1.00,那么modelValue将会是1,而viewValue是1.00,显然是无效的

  • username指令,异步检查用户输入的值是否已经存在,我们用$q.defer()来模拟服务器请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<!DOCTYPE html>
<html>
<head>
<meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="form-example1">
<form name="form" class="css-form" novalidate>
<div>
<label>
Size (integer 0 - 10):
<input type="number" ng-model="size" name="size"
min="0" max="10" integer />{{size}}</label><br />
<span ng-show="form.size.$error.integer">The value is not a valid integer!</span>
<span ng-show="form.size.$error.min || form.size.$error.max">
The value must be in range 0 to 10!</span>
<span ng-show="form.size.$error.require">111</span>
</div>
<div>
<label>
Username:
<input type="text" ng-model="name" name="name" username />{{name}}</label><br />
<span ng-show="form.name.$pending.username">Checking if this name is available...</span>
<span ng-show="form.name.$error.username">This username is already taken!</span>
</div>
</form>
</body>
<script>
var app = angular.module('form-example1', []);
var INTEGER_REGEXP = /^-?\d+$/;
app.directive('integer', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$validators.integer = function(modelValue, viewValue) {
if (ctrl.$isEmpty(modelValue)) {
// consider empty models to be valid
return true;
}
if (INTEGER_REGEXP.test(viewValue)) {
// it is valid
return true;
}
// it is invalid
return false;
};
}
};
});
app.directive('username', function($q, $timeout) {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
var usernames = ['Jim', 'John', 'Jill', 'Jackie'];
ctrl.$asyncValidators.username = function(modelValue, viewValue) {
if (ctrl.$isEmpty(modelValue)) {
// consider empty model valid
return $q.resolve();
}
var def = $q.defer();
$timeout(function() {
// Mock a delayed response
if (usernames.indexOf(modelValue) === -1) {
// The username is available
def.resolve();
} else {
def.reject();
}
}, 2000);
return def.promise;
};
}
};
});
</script>
</html>

修改内置验证

angularjs有一些内置的验证器,你可以很容易的移除或替换这些验证器。下面的例子演示了如何重写类型为email的验证器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!DOCTYPE html>
<html>
<head>
<meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="form-example-modify-validators">
<form name="form" class="css-form" novalidate>
<div>
<label>
Overwritten Email:
<input type="email" ng-model="myEmail" overwrite-email name="overwrittenEmail" />
</label>
<span ng-show="form.overwrittenEmail.$error.email">This email format is invalid!</span><br>
Model: {{myEmail}}
</div>
</form>
</body>
<script>
var app = angular.module('form-example-modify-validators', []);
app.directive('overwriteEmail', function() {
var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@example\.com$/i;
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
// 指定了ngModel,并且输入类型为email
if (ctrl && ctrl.$validators.email) {
// 重写angularjs的内置email验证器
ctrl.$validators.email = function(modelValue) {
return ctrl.$isEmpty(modelValue) || EMAIL_REGEXP.test(modelValue);
};
}
}
};
});
</script>
</html>

自定义form控件

angularjs实现了所有的html基本控件(input, select, textarea),大多数情况下足够用了,但是如果你需要更多的灵活性,你可以自定义form控件。

为了让自定义的控件可以支持ngModel,并且带有双向绑定,需要这样做:

  • 实现$render方法,在NgModelController.$formatters之后它负责对数据进行渲染
  • 在用户和控件交互之后,模型需要更新时,调用$setViewValue方法。通常是由DOM的事件监听器做的。

可以看下下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!DOCTYPE html>
<html>
<head>
<meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="form-example2">
<div contentEditable="true" ng-model="content" title="Click to edit">Some</div>
<pre>model = {{content}}</pre>
<style type="text/css">
div[contentEditable] {
cursor: pointer;
background-color: #D0D0D0;
}
</style>
</body>
<script>
angular.module('form-example2', []).directive('contenteditable', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
// view -> model
elm.on('blur', function() {
ctrl.$setViewValue(elm.html());
});
// model -> view
ctrl.$render = function() {
elm.html(ctrl.$viewValue);
};
// load init value from DOM
ctrl.$setViewValue(elm.html());
}
};
});
</script>
</html>
文章目录
  1. 1. 一个简单的form
  2. 2. 使用css class
  3. 3. form和control的状态绑定
  4. 4. 自定义model更新触发器
  5. 5. 延时model更新
  6. 6. 自定义验证
  7. 7. 修改内置验证
  8. 8. 自定义form控件
|