# 二、控制器

Angular 中的控制器提供了处理视图行为的业务逻辑；例如，响应用户单击按钮或在表单中输入一些文本。此外，控制器为视图模板准备模型。

一般来说，控制器不应该直接引用或操作文档对象模型。这有利于简化单元测试控制器。

## 为模型指定默认值

### 问题

您希望为控制器上下文中的范围指定一个默认值。

### 溶液

在模板中使用`ng-controller`指令:

```js
    <div ng-controller="MyCtrl">
    <p>{{value}}</p>
    </div>

```

接下来，在控制器函数中定义范围变量:

```js
    var MyCtrl = function($scope) {
    $scope.value = "some value";
    };

```

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter2/recipe1) 上找到完整的例子。

### 讨论

根据您使用 ng-controller 指令的位置，您可以定义它的分配范围。范围是分层的，并且遵循 DOM 节点层次结构。在我们的示例中，由于值是在`MyCtrl`控制器中设置的，因此值表达式被正确地评估为`some value`。请注意，如果将值表达式移出控制器的范围，这将不起作用:

```js
    <p>{{value}}</p>

    <div ng-controller="MyCtrl">
    </div>

```

在这种情况下`{{value}}`根本不会被渲染，因为 Angular.js 中的表达式求值对于`undefined`和`null`值是宽容的。

## 使用控制器功能更改模型值

### 问题

您希望使用控制器功能将模型值增加 1。

### 溶液

实现一个改变范围的增量函数:

```js
    function MyCtrl($scope) {
    $scope.value = 1;

    $scope.incrementValue = function(increment) {
    $scope.value += increment;
    };
    }

```

这个函数可以直接在表达式中调用；在我们的例子中，我们使用`ng-init`:

```js
    <div ng-controller="MyCtrl">
    <p ng-init="incrementValue(1)">{{value}}</p>
    </div>

```

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter2/recipe2) 上找到完整的例子。

### 讨论

`ng-init`指令在页面加载时执行，并调用在`MyCtrl`中定义的函数`incrementValue`。函数在作用域上的定义与值非常相似，但必须使用熟悉的括号语法来调用。

当然，用`value = value +1`增加表达式内部的值是可能的，但是想象一下这个函数要复杂得多！将这个函数移入控制器将我们的业务逻辑与声明性视图模板分开，我们可以很容易地为它编写单元测试。

## 用控制器函数封装模型值

### 问题

您希望通过封装模型值的函数(而不是直接从模板访问范围)来检索模型。

### 溶液

定义一个返回模型值的 getter 函数:

```js
    function MyCtrl($scope) {
    $scope.value = 1;

    $scope.getIncrementedValue = function() {
    return $scope.value + 1;
    };
    }

```

然后，在模板中，我们使用一个表达式来调用它:

```js
    <div ng-controller="MyCtrl">
    <p>{{getIncrementedValue()}}</p>
    </div>

```

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter2/recipe3) 上找到完整的例子。

### 讨论

`MyCtrl`定义`getIncrementedValue`函数，该函数使用当前值并返回递增 1 的值。有人可能会说，根据用例，使用过滤器更有意义。但是有些用例是特定于控制器行为的，在这些用例中不需要通用过滤器。

## 响应范围变更

### 问题

您希望对模型更改做出反应，以触发一些进一步的操作。在我们的示例中，我们只想根据我们正在收听的值设置另一个模型值。

### 溶液

使用控制器中的`$watch`功能:

```js
    function MyCtrl($scope) {
    $scope.name = "";

    $scope.$watch("name", function(newValue, oldValue) {
    if ($scope.name.length > 0) {
    $scope.greeting = "Greetings " + $scope.name;
    }
    });
    }

```

在我们的示例中，我们使用文本输入值来打印友好的问候语:

```js
    <div ng-controller="MyCtrl">
    <input type="text" ng-model="name" placeholder="Enter your name">
    <p>{{greeting}}</p>
    </div>

```

只要`name`型号发生变化，值`greeting`就会改变，并且该值不为空。

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter2/recipe4) 上找到完整的例子。

### 讨论

`$watch`函数的第一个参数`name`实际上是一个 Angular 表达式，所以可以使用更复杂的表达式(例如:`[value1, value2] | json`)甚至是一个 JavaScript 函数。在这种情况下，您需要在观察器函数中返回一个字符串:

```js
    $scope.$watch(function() {
    return $scope.name;
    }, function(newValue, oldValue) {
    console.log("change detected: " + newValue)
    });

```

第二个参数是每当表达式求值返回不同值时调用的函数。第一个参数是新值，第二个参数是旧值。在内部，这使用`angular.equals`来确定相等，这意味着两个对象或值都通过了`===`比较。

## 嵌套控制器之间共享模型

### 问题

您希望在嵌套的控制器层次结构之间共享一个模型。

### 溶液

使用 JavaScript 对象代替原语或直接`$parent`范围引用。

我们的示例模板使用一个控制器`MyCtrl`和一个嵌套控制器`MyNestedCtrl`:

```js
    <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`文件包含控制器定义，并使用一些默认值初始化范围:

```js
    var app = angular.module("MyApp", []);

    app.controller("MyCtrl", function($scope) {
    $scope.name = "Peter";
    $scope.user = {
    name: "Parker"
    };
    });

    app.controller("MyNestedCtrl", function($scope) {
    });

```

摆弄各种输入字段，看看变化是如何相互影响的。

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter2/recipe5) 上找到完整的例子。

### 讨论

所有默认值都在`MyNestedCtrl`的父级`MyCtrl,`中定义。在第一个输入字段中进行更改时，这些更改将与绑定到`name`变量的其他输入字段同步。只要它们只从变量中读取，它们都共享同一个范围变量。如果更改嵌套值，将在`MyNestedCtrl`范围内创建一个副本。从现在开始，更改第一个输入字段只会更改嵌套的输入字段，该字段通过`$parent.name`表达式显式引用父范围。

基于对象的值在这方面表现不同。无论您更改嵌套的还是`MyCtrl`范围的输入字段，更改都将保持同步。在 Angular 中，范围原型通常从父范围继承属性。因此，对象是引用并保持同步，而基元类型只有在子范围内没有改变时才保持同步。

一般来说，我倾向于不使用`$parent.name`，而是总是使用对象来共享模型属性。如果使用`$parent.name,`，`MyNestedCtrl`不仅需要特定的模型属性，还需要正确的范围层次结构。

| ![](img/tip.png) | 提示:Chrome 插件 [Batarang](https://github.com/angular/angularjs-batarang) 通过向您显示嵌套范围的树，简化了范围层次的调试。太棒了！ |

## 使用服务在控制器之间共享代码

### 问题

您希望在控制器之间共享业务逻辑。

### 解

利用[服务](http://docs.angularjs.org/guide/dev_guide.services)实现您的业务逻辑，并使用依赖注入在您的控制器中使用该服务。

该模板显示了从两个控制器对用户列表的访问:

```js
    <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`中的服务和控制器实现实现用户服务，控制器最初设置范围:

```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](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter2/recipe6) 上找到完整的例子。

### 讨论

`factory`方法创建一个单例`UserService`，返回两个函数，分别用于检索所有用户和第一个用户。控制器通过将`UserService`作为参数添加到`controller`功能中来注入该参数。

在这里使用依赖注入对于测试你的控制器非常好，因为你可以很容易地注入一个`UserService`存根。唯一的缺点是你不能从上面缩小代码而不破坏它，因为注入机制依赖于`UserService`的精确字符串表示。因此，建议使用内联注释来定义依赖关系，即使在缩小时，内联注释也能继续工作:

```js
    app.controller("AnotherCtrl", ["$scope", "UserService",
    function($scope, UserService) {
    $scope.firstUser = UserService.first();
    }
    ]);

```

语法看起来有点滑稽，但是由于数组中的字符串在缩小过程中不会改变，它解决了我们的问题。请注意，您可以更改函数的参数名称，因为注入机制仅依赖于数组定义的顺序。

另一种方法是使用`$inject`注释:

```js
    var anotherCtrl = function($scope, UserService) {
    $scope.firstUser = UserService.first();
    };

    anotherCtrl.$inject = ["$scope", "UserService"];

```

这需要您使用一个临时变量来调用`$inject`服务。同样，您可以更改函数参数名称。您很可能会在使用 Angular 的应用程序中看到这两个版本。

## 测试控制器

### 问题

您希望对业务逻辑进行单元测试。

### 解

使用[茉莉](http://pivotal.github.com/jasmine/)和[角粒](https://github.com/angular/angular-seed)项目实施单元测试。遵循我们之前的`$watch`配方，这就是我们的规格的外观:

```js
    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](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter2/recipe7) 上找到完整的例子。

### 讨论

茉莉规格使用`describe`和`it`功能对规格进行分组，使用`beforeEach`和`afterEach`进行设置和拆码。实际期望将来自范围的问候与我们的期望`Greetings Frederik`进行比较。

范围和控制器初始化稍微复杂一些。我们使用`inject`来初始化作用域和控制器，尽可能接近我们的代码在运行时的行为。我们不能仅仅将作用域初始化为一个 JavaScript 对象`{}`，因为那样我们就不能在其上调用`$watch`。相反，`$rootScope.$new()`会起作用。请注意，`$controller`服务要求`MyCtrl`可用，并使用对象符号传递依赖关系。

在我们更改范围后，需要调用`$digest`来触发手表执行。我们需要在我们的规范中手动调用`$digest`，而在运行时，Angular 会自动为我们调用。