# 八、通用用户界面模式

## 过滤和排序列表

### 问题

您希望筛选和排序一个相对较小的项目列表，所有这些项目都可以在客户端上获得。

### 溶液

对于这个例子，我们将使用`ng-repeat`指令呈现一个好友列表。使用内置的`filter`和`orderBy`过滤器，我们将在客户端对好友列表进行过滤和排序:

```js
    <body ng-app="MyApp">
    <div ng-controller="MyCtrl">
    <form class="form-inline">
    <input ng-model="query" type="text"
    placeholder="Filter by" autofocus>
    </form>
    <ul ng-repeat="friend in friends | filter:query | orderBy: 'name' ">
    <li>{{friend.name}}</li>
    </ul>
    </div>
    </body>

```

纯文本输入字段用于输入过滤查询并绑定到`filter`。因此，任何更改都将直接用于筛选列表。

控制器定义默认的朋友数组:

```js
    app.controller("MyCtrl", function($scope) {
    $scope.friends = [
    { name: "Peter", age: 20 },
    { name: "Pablo", age: 55 },
    { name: "Linda", age: 20 },
    { name: "Marta", age: 37 },
    { name: "Othello", age: 20 },
    { name: "Markus", age: 32 }
    ];
    });

```

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

### 讨论

链接过滤器是实现这种用例的一种极好的方式，只要您在客户端上有所有可用的数据。

[过滤器](http://docs.angularjs.org/api/ng.filter:filter) Angular.js 过滤器处理一个数组，并返回一个项目子集作为新数组。它支持字符串、对象或函数参数。在这个例子中，我们只使用了 String 参数，但是考虑到`$scope.friends`是一个对象数组，我们可以想到使用 Object 参数的更复杂的例子。例如:

```js
    <ul ng-repeat="friend in friends |
    filter: { name: query, age: '20' } |
    orderBy: 'name' ">
    <li>{{friend.name}} ({{friend.age}})</li>
    </ul>

```

这样，我们可以同时按姓名和年龄进行过滤。最后，您可以调用控制器中定义的函数，为您进行过滤:

```js
    <ul ng-repeat="friend in friends |
    filter: filterFunction |
    orderBy: 'name' ">
    <li>{{friend.name}} ({{friend.age}})</li>
    </ul>

    $scope.filterFunction = function(element) {
    return element.name.match(/^Ma/) ? true : false;
    };

```

`filterFunction`必须返回`true`或`false`。在本例中，我们使用以`Ma`开头的名称正则表达式来过滤列表。

## 通过客户端数据分页

### 问题

您有一个完全客户端的数据表，并且希望对数据进行分页。

### 溶液

使用带有`ng-repeat`指令的 HTML 表格元素，只呈现当前页面的项目。所有的分页逻辑都应该在定制的过滤器和控制器实现中处理。

让我们从为表和分页元素使用 Twitter Bootstrap 的模板开始:

```js
    <div ng-controller="PaginationCtrl">
    <table class="table table-striped">
    <thead>
    <tr>
    <th>Id</th>
    <th>Name</th>
    <th>Description</th>
    </tr>
    </thead>
    <tbody>
    <tr ng-repeat="item in items |
    offset: currentPage*itemsPerPage |
    limitTo: itemsPerPage">
    <td>{{item.id}}</td>
    <td>{{item.name}}</td>
    <td>{{item.description}}</td>
    </tr>
    </tbody>
    <tfoot>
    <td colspan="3">
    <div class="pagination">
    <ul>
    <li ng-class="prevPageDisabled()">
    <a href ng-click="prevPage()">« Prev</a>
    </li>
    <li ng-repeat="n in range()"
    ng-class="{active: n == currentPage}" ng-click="setPage(n)">
    <a href="#">{{n+1}}</a>
    </li>
    <li ng-class="nextPageDisabled()">
    <a href ng-click="nextPage()">Next »</a>
    </li>
    </ul>
    </div>
    </td>
    </tfoot>
    </table>
    </div>

```

`offset`过滤器负责选择当前页面的项目子集。它使用给定起始参数的数组上的`slice`函数作为索引:

```js
    app.filter('offset', function() {
    return function(input, start) {
    start = parseInt(start, 10);
    return input.slice(start);
    };
    });

```

控制器管理实际的`$scope.items`数组，并处理启用/禁用分页按钮的逻辑:

```js
    app.controller("PaginationCtrl", function($scope) {

    $scope.itemsPerPage = 5;
    $scope.currentPage = 0;
    $scope.items = [];

    for (var i=0; i<50; i++) {
    $scope.items.push({
    id: i, name: "name "+ i, description: "description " + i
    });
    }

    $scope.prevPage = function() {
    if ($scope.currentPage > 0) {
    $scope.currentPage--;
    }
    };

    $scope.prevPageDisabled = function() {
    return $scope.currentPage === 0 ? "disabled" : "";
    };

    $scope.pageCount = function() {
    return Math.ceil($scope.items.length/$scope.itemsPerPage)-1;
    };

    $scope.nextPage = function() {
    if ($scope.currentPage < $scope.pageCount()) {
    $scope.currentPage++;
    }
    };

    $scope.nextPageDisabled = function() {
    return $scope.currentPage === $scope.pageCount() ? "disabled" : "";
    };

    });

```

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

### 讨论

这种分页解决方案的最初想法可以通过查看`ng-repeat`的用法来最好地解释，该用法用于呈现每个项目的表行:

```js
    <tr ng-repeat="item in items |
    offset: currentPage*itemsPerPage |
    limitTo: itemsPerPage">
    <td>{{item.id}}</td>
    <td>{{item.name}}</td>
    <td>{{item.description}}</td>
    </tr>

```

`offset`滤波器使用`currentPage*itemsPerPage`计算阵列切片操作的偏移量。这将生成一个从偏移量到数组末尾的数组。然后，使用内置的`limitTo`过滤器将数组子集化为`itemsPerPage`的数量。所有这些都是在客户端完成的，只有过滤器。

控制器负责支持一个`nextPage`和`prevPage`动作以及伴随的功能，通过`ng-class`指令:`nextPageDisabled`和`prevPageDisabled`检查这些动作的禁用状态。`prevPage`功能在递减`currentPage,`之前首先检查它是否还没有到达第一页，而`nextPage`对最后一页进行同样的操作；相同的逻辑适用于禁用的检查。

这个例子已经很复杂了，我故意省略了对上一个和下一个按钮之间链接的解释。[全面实施](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter8/recipe2)虽然在线，但供您调查。

## 通过服务器端数据分页

### 问题

您希望在大型服务器端结果集中分页。

### 溶液

您不能将前面的方法与筛选器一起使用，因为这将要求客户端上的所有数据都可用。相反，我们只使用带有控制器的实现。

模板没有太大变化，只有`ng-repeat`指令现在看起来更简单了:

```js
    <tr ng-repeat="item in pagedItems">
    <td>{{item.id}}</td>
    <td>{{item.name}}</td>
    <td>{{item.description}}</td>
    </tr>

```

为了简化示例，我们将通过为以下项目提供 Angular 服务实现来伪造服务器端服务:

```js
    app.factory("Item", function() {

    var items = [];
    for (var i=0; i<50; i++) {
    items.push({
    id: i, name: "name "+ i, description: "description " + i
    });
    }

    return {
    get: function(offset, limit) {
    return items.slice(offset, offset+limit);
    },
    total: function() {
    return items.length;
    }
    };
    });

```

该服务管理一个项目列表，并具有检索给定偏移量的项目子集的方法，包括项目的限制和总数。

控制器使用依赖注入来访问`Item`服务，并且包含与我们之前的配方几乎相同的方法:

```js
    app.controller("PaginationCtrl", function($scope, Item) {

    $scope.itemsPerPage = 5;
    $scope.currentPage = 0;

    $scope.prevPage = function() {
    if ($scope.currentPage > 0) {
    $scope.currentPage--;
    }
    };

    $scope.prevPageDisabled = function() {
    return $scope.currentPage === 0 ? "disabled" : "";
    };

    $scope.nextPage = function() {
    if ($scope.currentPage < $scope.pageCount() - 1) {
    $scope.currentPage++;
    }
    };

    $scope.nextPageDisabled = function() {
    return $scope.currentPage === $scope.pageCount() - 1 ? "disabled" : "";
    };

    $scope.pageCount = function() {
    return Math.ceil($scope.total/$scope.itemsPerPage);
    };

    $scope.$watch("currentPage", function(newValue, oldValue) {
    $scope.pagedItems =
    Item.get(newValue*$scope.itemsPerPage, $scope.itemsPerPage);
    $scope.total = Item.total();
    });

    });

```

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

### 讨论

当您选择下一页/上一页时，您将更改`$scope.currentPage`值，并触发`$watch`功能。它获取当前页面的新项目以及项目总数。因此，在客户端，我们只有 5 个项目可用，如`itemsPerPage`中所定义的，并且在分页时，我们扔掉上一页的项目并获取新的项目。

如果你想用一个真正的后端来尝试这个，你只需要换出`Item`服务实现。

## 使用无限结果分页

### 问题

您希望通过“加载更多”按钮对服务器端数据进行分页，该按钮会不断追加更多数据，直到没有更多数据可用。

### 溶液

让我们从查看如何使用`ng-repeat`指令渲染项目表开始:

```js
    <div ng-controller="PaginationCtrl">
    <table class="table table-striped">
    <thead>
    <tr>
    <th>Id</th>
    <th>Name</th>
    <th>Description</th>
    </tr>
    </thead>
    <tbody>
    <tr ng-repeat="item in pagedItems">
    <td>{{item.id}}</td>
    <td>{{item.name}}</td>
    <td>{{item.description}}</td>
    </tr>
    </tbody>
    <tfoot>
    <td colspan="3">
    <button class="btn" href="#" ng-class="nextPageDisabledClass()"
    ng-click="loadMore()">Load More</button>
    </td>
    </tfoot>
    </table>
    </div>

```

控制器使用与前一配方相同的`Item`服务，并处理“加载更多”按钮的逻辑:

```js
    app.controller("PaginationCtrl", function($scope, Item) {

    $scope.itemsPerPage = 5;
    $scope.currentPage = 0;
    $scope.total = Item.total();
    $scope.pagedItems = Item.get($scope.currentPage*$scope.itemsPerPage,
    $scope.itemsPerPage);

    $scope.loadMore = function() {
    $scope.currentPage++;
    var newItems = Item.get($scope.currentPage*$scope.itemsPerPage,
    $scope.itemsPerPage);
    $scope.pagedItems = $scope.pagedItems.concat(newItems);
    };

    $scope.nextPageDisabledClass = function() {
    return $scope.currentPage === $scope.pageCount()-1 ? "disabled" : "";
    };

    $scope.pageCount = function() {
    return Math.ceil($scope.total/$scope.itemsPerPage);
    };

    });

```

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

### 讨论

该解决方案实际上类似于之前的配方，并且再次仅使用控制器。最初检索`$scope.pagedItems`来渲染前五个项目。

当按下“加载更多”按钮时，我们获取另一组增加`currentPage`的项目来改变`Item.get`功能的`offset`。新项目将使用数组`concat`功能与现有项目连接。对`pagedItems`的更改将由`ng-repeat`指令自动呈现。

`nextPageDisabledClass`通过计算`pageCount`中的总页数并将其与当前页面进行比较来检查是否有更多的数据可用。

## 显示闪光通知/故障信息

### 问题

您希望在用户成功提交表单后显示一条快速确认消息。

### 溶液

在像 Ruby on Rails 这样的网络框架中，表单提交将导致一个带有 flash 确认消息的重定向，依赖于浏览器会话。对于我们的客户端方法，我们绑定路由更改并管理一个闪存消息队列。

在我们的示例中，我们使用带有表单的主页，在表单提交时，我们导航到另一个页面并显示 flash 消息。我们使用`ng-view`指令，并将这两页定义为脚本标签:

```js
    <body ng-app="MyApp" ng-controller="MyCtrl">

    <ul class="nav nav-pills">
    <li><a href="#/">Home</a></li>
    <li><a href="#/page">Next Page</a></li>
    </ul>

    <div class="alert" ng-show="flash.getMessage()">
    <b>Alert!</b>
    <p>{{flash.getMessage()}}</p>
    </div>

    <ng-view></ng-view>

    <script type="text/ng-template" id="home.html">
    <h3>Home</h3>

    <form ng-submit="submit(message)" class="form-inline">
    <input type="text" ng-model="message" autofocus>
    <button class="btn">Submit</button>
    </form>

    </script>

    <script type="text/ng-template" id="page.html">
    <h3>Next Page</h3>

    </script>

    </body>

```

请注意，闪烁消息(就像导航一样)始终显示，但根据是否有可用的闪烁消息而有条件地隐藏。

路线定义定义了页面；对我们来说没什么新鲜的:

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

    app.config(function($routeProvider) {
    $routeProvider.
    when("/home", { templateUrl: "home.html" }).
    when("/page", { templateUrl: "page.html" }).
    otherwise({ redirectTo: "/home" });
    });

```

有趣的部分是`flash`服务，它处理一个消息队列，并监听路由更改，以提供从队列到当前页面的消息:

```js
    app.factory("flash", function($rootScope) {
    var queue = [];
    var currentMessage = "";

    $rootScope.$on("$routeChangeSuccess", function() {
    currentMessage = queue.shift() || "";
    });

    return {
    setMessage: function(message) {
    queue.push(message);
    },
    getMessage: function() {
    return currentMessage;
    }
    };
    });

```

控制器处理表单提交并导航到另一个页面:

```js
    app.controller("MyCtrl", function($scope, $location, flash) {
    $scope.flash = flash;
    $scope.message = "Hello World";

    $scope.submit = function(message) {
    flash.setMessage(message);
    $location.path("/page");
    }
    });

```

flash 服务是依赖注入到控制器中的，并且对作用域可用，因为我们希望在模板中使用它。

当您按下提交按钮时，您将被导航到另一个页面并看到 flash 消息。请注意，使用导航在页面之间来回切换不会显示 flash 消息。

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

### 讨论

控制器使用`flash`服务的`setMessage`功能，该服务将消息存储在名为`queue`的数组中。当控制器使用`$location`服务来导航服务时，`routeChangeSuccess, the`监听器将被调用，并可以从队列中检索消息。

在模板中，我们使用`ng-show`隐藏 div 元素，使用`flash.getMessage()`隐藏 flash 消息。

由于这是一项服务，它可以在您的代码中的任何地方使用，并且它将在下一次路由更改时显示一条 flash 消息。

## 使用 HTML 5 编辑位置文本内容可编辑

### 问题

您希望使用 HTML 5 `contenteditable`属性在适当的位置编辑一个 div 元素。

### 溶液

为`contenteditable`属性实现一个指令，并使用`ng-model`进行数据绑定。

在本例中，我们使用一个 div 和一个段落来呈现内容:

```js
    <div contenteditable ng-model="text"></div>
    <p>{{text}}</p>

```

该指令特别有趣，因为它使用了`ng-model`而不是自定义属性:

```js
    app.directive("contenteditable", function() {
    return {
    restrict: "A",
    require: "ngModel",
    link: function(scope, element, attrs, ngModel) {

    function read() {
    ngModel.$setViewValue(element.html());
    }

    ngModel.$render = function() {
    element.html(ngModel.$viewValue || "");
    };

    element.bind("blur keyup change", function() {
    scope.$apply(read);
    });
    }
    };
    });

```

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

### 讨论

该指令被限制作为 HTML 属性使用，因为我们希望使用 HTML 5 `contenteditable`属性，而不是定义一个新的 HTML 元素。

需要`ngModel`控制器配合链接功能进行数据绑定。该实现绑定了一个事件侦听器，该侦听器使用 [apply](http://docs.angularjs.org/api/ng.$rootScope.Scope) 执行`read`函数。这确保了，即使我们从 DOM 事件处理程序中调用`read`函数，我们也会通知 Angular。

`read`功能根据视图的用户输入更新模型。而`$render`功能在另一个方向也在做同样的事情，每当模型发生变化的时候都会为我们更新视图。

指令出奇的简单，把`ng-model`放在一边。但是如果没有`ng-model`的支持，我们将不得不提出我们自己的模型属性处理，这将与其他指令不一致。

## 显示模态对话框

### 问题

你希望使用一个模态对话使用推特引导框架。当对话框阻止 web 应用程序的其余部分直到关闭时，它被称为模式对话框。

### 溶液

使用`angular-ui`模块不错的`modal`插件，直接支持 Twitter Bootstrap。

模板定义了一个按钮来打开模态和模态代码本身:

```js
    <body ng-app="MyApp" ng-controller="MyCtrl">

    <button class="btn" ng-click="open()">Open Modal</button>

    <div modal="showModal" close="cancel()">
    <div class="modal-header">
    <h4>Modal Dialog</h4>
    </div>
    <div class="modal-body">
    <p>Example paragraph with some text.</p>
    </div>
    <div class="modal-footer">
    <button class="btn btn-success" ng-click="ok()">Okay</button>
    <button class="btn" ng-click="cancel()">Cancel</button>
    </div>
    </div>

    </body>

```

注意，即使我们没有明确指定，模态对话框最初是通过`modal`属性隐藏的。控制器只处理按钮点击和`modal`属性使用的`showModal`值。

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

    $scope.open = function() {
    $scope.showModal = true;
    };

    $scope.ok = function() {
    $scope.showModal = false;
    };

    $scope.cancel = function() {
    $scope.showModal = false;
    };

```

不要忘记下载 angular-ui.js 文件并将其包含在脚本标签中。模块依赖关系直接定义为“ui.bootstrap.modal”。[完整示例](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter8/recipe7)在 GitHub 上提供，包括 angular-ui 模块。

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

### 讨论

模板中定义的模态直接来自推特引导[文档](http://twitter.github.com/bootstrap/javascript.html#modals)。我们可以用`modal`属性控制可见性。此外，`close`属性定义了一个`close`函数，该函数在对话框关闭时被调用。请注意，如果用户按下`escape`键或在模式外点击，可能会发生这种情况。

我们自己的取消按钮使用相同的功能来手动关闭模式，而 ok 按钮使用`ok`功能。这使得我们很容易区分是简单取消模式的用户还是实际按下 ok 按钮的用户。

## 显示加载微调器

### 问题

您希望在等待 AJAX 请求完成时显示一个加载微调器。

### 溶液

我们将使用推特搜索应用编程接口来呈现搜索结果列表。当按下按钮时，AJAX 请求将运行，微调器图像将一直显示，直到请求完成:

```js
    <body ng-app="MyApp" ng-controller="MyCtrl">

    <div>
    <button class="btn" ng-click="load()">Load Tweets</button>
    <img id="spinner" ng-src="img/spinner.gif" style="display:none;">
    </div>

    <div>
    <ul ng-repeat="tweet in tweets">
    <li>
    <img ng-src="{{tweet.profile_image_url}}" alt="">
    &#160; {{tweet.from_user}}
    {{tweet.text}}
    </li>
    </ul>
    </div>

    </body>

```

使用了一个用于所有 AJAX 调用的 Angular.js 拦截器，它允许您在实际请求开始之前和完成时执行代码:

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

    app.config(function ($httpProvider) {
    $httpProvider.responseInterceptors.push('myHttpInterceptor');

    var spinnerFunction = function spinnerFunction(data, headersGetter) {
    $("#spinner").show();
    return data;
    };

    $httpProvider.defaults.transformRequest.push(spinnerFunction);
    });

    app.factory('myHttpInterceptor', function ($q, $window) {
    return function (promise) {
    return promise.then(function (response) {
    $("#spinner").hide();
    return response;
    }, function (response) {
    $("#spinner").hide();
    return $q.reject(response);
    });
    };
    });

```

请注意，我们使用 jQuery 在配置步骤中显示微调器，并在拦截器中隐藏微调器。

此外，我们使用控制器来处理按钮点击并执行搜索请求:

```js
    app.controller("MyCtrl", function($scope, $resource, $rootScope) {

    $scope.resultsPerPage = 5;
    $scope.page = 1;
    $scope.searchTerm = "angularjs";

    $scope.twitter = $resource('http://search.twitter.com/search.json',
    { callback:'JSON_CALLBACK',
    page: $scope.page,
    rpp: $scope.resultsPerPage,
    q: $scope.searchTerm },
    { get: { method:'JSONP' } });

    $scope.load = function() {
    $scope.twitter.get(function(data) {
    $scope.tweets = data.results;
    });
    };
    });

```

不要忘记将`ngResource`添加到模块中，并通过脚本标签加载。

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

### 讨论

模板是这个食谱中简单的部分，因为它使用`ng-repeat`指令呈现了一个推文列表。让我们直接跳到拦截器代码。

拦截器使用工厂方法实现，并将其自身附加到 AJAX 响应的 promise 函数中，以便在成功或失败时隐藏微调器。请注意，在失败时，我们使用 [$q](http://docs.angularjs.org/api/ng.$q) 服务的`reject`功能，Angular 的承诺/延期执行。

现在，在`config`方法中，我们将拦截器添加到`$httpProvider`的 responseInterceptors 列表中，以便正确注册它。以类似的方式，我们将`spinnerFunction`添加到默认的`transformRequest`列表中，以便在每个 AJAX 请求之前调用它。

控制器负责使用`$resource`对象，并使用`load`功能处理按钮点击。我们在这里使用 JSONP 来允许在本地执行这段代码，即使它是由不同的域提供服务的。