Skip to content

Files

Latest commit

 

History

History
362 lines (231 loc) · 11.8 KB

02.md

File metadata and controls

362 lines (231 loc) · 11.8 KB

二、控制器

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 中的表达式求值对于undefinednull值是宽容的。

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

问题

您希望使用控制器功能将模型值增加 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 上找到完整的例子。

讨论

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

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

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