Angular 中的控制器提供了处理视图行为的业务逻辑;例如,响应用户单击按钮或在表单中输入一些文本。此外,控制器为视图模板准备模型。
一般来说,控制器不应该直接引用或操作文档对象模型。这有利于简化单元测试控制器。
您希望为控制器上下文中的范围指定一个默认值。
在模板中使用ng-controller
指令:
<div ng-controller="MyCtrl">
<p>{{value}}</p>
</div>
接下来,在控制器函数中定义范围变量:
var MyCtrl = function($scope) {
$scope.value = "some value";
};
你可以在 GitHub 上找到完整的例子。
根据您使用 ng-controller 指令的位置,您可以定义它的分配范围。范围是分层的,并且遵循 DOM 节点层次结构。在我们的示例中,由于值是在MyCtrl
控制器中设置的,因此值表达式被正确地评估为some value
。请注意,如果将值表达式移出控制器的范围,这将不起作用:
<p>{{value}}</p>
<div ng-controller="MyCtrl">
</div>
在这种情况下{{value}}
根本不会被渲染,因为 Angular.js 中的表达式求值对于undefined
和null
值是宽容的。
您希望使用控制器功能将模型值增加 1。
实现一个改变范围的增量函数:
function MyCtrl($scope) {
$scope.value = 1;
$scope.incrementValue = function(increment) {
$scope.value += increment;
};
}
这个函数可以直接在表达式中调用;在我们的例子中,我们使用ng-init
:
<div ng-controller="MyCtrl">
<p ng-init="incrementValue(1)">{{value}}</p>
</div>
你可以在 GitHub 上找到完整的例子。
ng-init
指令在页面加载时执行,并调用在MyCtrl
中定义的函数incrementValue
。函数在作用域上的定义与值非常相似,但必须使用熟悉的括号语法来调用。
当然,用value = value +1
增加表达式内部的值是可能的,但是想象一下这个函数要复杂得多!将这个函数移入控制器将我们的业务逻辑与声明性视图模板分开,我们可以很容易地为它编写单元测试。
您希望通过封装模型值的函数(而不是直接从模板访问范围)来检索模型。
定义一个返回模型值的 getter 函数:
function MyCtrl($scope) {
$scope.value = 1;
$scope.getIncrementedValue = function() {
return $scope.value + 1;
};
}
然后,在模板中,我们使用一个表达式来调用它:
<div ng-controller="MyCtrl">
<p>{{getIncrementedValue()}}</p>
</div>
你可以在 GitHub 上找到完整的例子。
MyCtrl
定义getIncrementedValue
函数,该函数使用当前值并返回递增 1 的值。有人可能会说,根据用例,使用过滤器更有意义。但是有些用例是特定于控制器行为的,在这些用例中不需要通用过滤器。
您希望对模型更改做出反应,以触发一些进一步的操作。在我们的示例中,我们只想根据我们正在收听的值设置另一个模型值。
使用控制器中的$watch
功能:
function MyCtrl($scope) {
$scope.name = "";
$scope.$watch("name", function(newValue, oldValue) {
if ($scope.name.length > 0) {
$scope.greeting = "Greetings " + $scope.name;
}
});
}
在我们的示例中,我们使用文本输入值来打印友好的问候语:
<div ng-controller="MyCtrl">
<input type="text" ng-model="name" placeholder="Enter your name">
<p>{{greeting}}</p>
</div>
只要name
型号发生变化,值greeting
就会改变,并且该值不为空。
你可以在 GitHub 上找到完整的例子。
$watch
函数的第一个参数name
实际上是一个 Angular 表达式,所以可以使用更复杂的表达式(例如:[value1, value2] | json
)甚至是一个 JavaScript 函数。在这种情况下,您需要在观察器函数中返回一个字符串:
$scope.$watch(function() {
return $scope.name;
}, function(newValue, oldValue) {
console.log("change detected: " + newValue)
});
第二个参数是每当表达式求值返回不同值时调用的函数。第一个参数是新值,第二个参数是旧值。在内部,这使用angular.equals
来确定相等,这意味着两个对象或值都通过了===
比较。
您希望在嵌套的控制器层次结构之间共享一个模型。
使用 JavaScript 对象代替原语或直接$parent
范围引用。
我们的示例模板使用一个控制器MyCtrl
和一个嵌套控制器MyNestedCtrl
:
<body ng-app="MyApp">
<div ng-controller="MyCtrl">
<label>Primitive</label>
<input type="text" ng-model="name">
<label>Object</label>
<input type="text" ng-model="user.name">
<div class="nested" ng-controller="MyNestedCtrl">
<label>Primitive</label>
<input type="text" ng-model="name">
<label>Primitive with explicit $parent reference</label>
<input type="text" ng-model="$parent.name">
<label>Object</label>
<input type="text" ng-model="user.name">
</div>
</div>
</body>
app.js
文件包含控制器定义,并使用一些默认值初始化范围:
var app = angular.module("MyApp", []);
app.controller("MyCtrl", function($scope) {
$scope.name = "Peter";
$scope.user = {
name: "Parker"
};
});
app.controller("MyNestedCtrl", function($scope) {
});
摆弄各种输入字段,看看变化是如何相互影响的。
你可以在 GitHub 上找到完整的例子。
所有默认值都在MyNestedCtrl
的父级MyCtrl,
中定义。在第一个输入字段中进行更改时,这些更改将与绑定到name
变量的其他输入字段同步。只要它们只从变量中读取,它们都共享同一个范围变量。如果更改嵌套值,将在MyNestedCtrl
范围内创建一个副本。从现在开始,更改第一个输入字段只会更改嵌套的输入字段,该字段通过$parent.name
表达式显式引用父范围。
基于对象的值在这方面表现不同。无论您更改嵌套的还是MyCtrl
范围的输入字段,更改都将保持同步。在 Angular 中,范围原型通常从父范围继承属性。因此,对象是引用并保持同步,而基元类型只有在子范围内没有改变时才保持同步。
一般来说,我倾向于不使用$parent.name
,而是总是使用对象来共享模型属性。如果使用$parent.name,
,MyNestedCtrl
不仅需要特定的模型属性,还需要正确的范围层次结构。
| | 提示:Chrome 插件 Batarang 通过向您显示嵌套范围的树,简化了范围层次的调试。太棒了! |
您希望在控制器之间共享业务逻辑。
利用服务实现您的业务逻辑,并使用依赖注入在您的控制器中使用该服务。
该模板显示了从两个控制器对用户列表的访问:
<div ng-controller="MyCtrl">
<ul ng-repeat="user in users">
<li>{{user}}</li>
</ul>
<div class="nested" ng-controller="AnotherCtrl">
First user: {{firstUser}}
</div>
</div>
app.js
中的服务和控制器实现实现用户服务,控制器最初设置范围:
var app = angular.module("MyApp", []);
app.factory("UserService", function() {
var users = ["Peter", "Daniel", "Nina"];
return {
all: function() {
return users;
},
first: function() {
return users[0];
}
};
});
app.controller("MyCtrl", function($scope, UserService) {
$scope.users = UserService.all();
});
app.controller("AnotherCtrl", function($scope, UserService) {
$scope.firstUser = UserService.first();
});
你可以在 GitHub 上找到完整的例子。
factory
方法创建一个单例UserService
,返回两个函数,分别用于检索所有用户和第一个用户。控制器通过将UserService
作为参数添加到controller
功能中来注入该参数。
在这里使用依赖注入对于测试你的控制器非常好,因为你可以很容易地注入一个UserService
存根。唯一的缺点是你不能从上面缩小代码而不破坏它,因为注入机制依赖于UserService
的精确字符串表示。因此,建议使用内联注释来定义依赖关系,即使在缩小时,内联注释也能继续工作:
app.controller("AnotherCtrl", ["$scope", "UserService",
function($scope, UserService) {
$scope.firstUser = UserService.first();
}
]);
语法看起来有点滑稽,但是由于数组中的字符串在缩小过程中不会改变,它解决了我们的问题。请注意,您可以更改函数的参数名称,因为注入机制仅依赖于数组定义的顺序。
另一种方法是使用$inject
注释:
var anotherCtrl = function($scope, UserService) {
$scope.firstUser = UserService.first();
};
anotherCtrl.$inject = ["$scope", "UserService"];
这需要您使用一个临时变量来调用$inject
服务。同样,您可以更改函数参数名称。您很可能会在使用 Angular 的应用程序中看到这两个版本。
您希望对业务逻辑进行单元测试。
使用茉莉和角粒项目实施单元测试。遵循我们之前的$watch
配方,这就是我们的规格的外观:
describe('MyCtrl', function(){
var scope, ctrl;
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
ctrl = $controller(MyCtrl, { $scope: scope });
}));
it('should change greeting value if name value is changed', function() {
scope.name = "Frederik";
scope.$digest();
expect(scope.greeting).toBe("Greetings Frederik");
});
});
你可以在 GitHub 上找到完整的例子。
茉莉规格使用describe
和it
功能对规格进行分组,使用beforeEach
和afterEach
进行设置和拆码。实际期望将来自范围的问候与我们的期望Greetings Frederik
进行比较。
范围和控制器初始化稍微复杂一些。我们使用inject
来初始化作用域和控制器,尽可能接近我们的代码在运行时的行为。我们不能仅仅将作用域初始化为一个 JavaScript 对象{}
,因为那样我们就不能在其上调用$watch
。相反,$rootScope.$new()
会起作用。请注意,$controller
服务要求MyCtrl
可用,并使用对象符号传递依赖关系。
在我们更改范围后,需要调用$digest
来触发手表执行。我们需要在我们的规范中手动调用$digest
,而在运行时,Angular 会自动为我们调用。