如何升级Angular排序过滤器
介绍
AngularJS 最初提供的最有用的功能之一是能够仅使用模板变量和过滤器来过滤和排序页面上的数据。 双向数据绑定赢得了许多转换为 AngularJS 的人。
然而,今天,许多前端开发人员更喜欢单向数据绑定,而那些 orderBy
和 filter
过滤器随着 Angular 的出现而被淘汰。
注意: 在本文中,“AngularJS”将用于指代 1.x,“Angular”将用于指代 2+。
在本文中,您将使用 ngUpgrade 重新应用 orderBy
和 filter
。
第 1 步 — 设置项目
我们将逐步更新新重写的组件的模板。 然后,我们将添加排序和过滤以恢复它在 AngularJS 中的所有功能。 这是为 ngUpgrade 流程开发的一项关键技能。
首先,请花点时间克隆我们将使用的示例项目。
git clone https://github.com/upgradingangularjs/ordersystem-project.git
查看此提交作为我们的起点:
git checkout 9daf9ab1e21dc5b20d15330e202f158b4c065bc3
这个示例项目是一个使用 AngularJS 1.6 和 Angular 4 的 ngUpgrade 混合项目。 它有一个有效的 Express API 和一个用于开发和生产的 Webpack 构建。 随意探索、分叉并在您自己的项目中使用这些模式。
不要忘记在 public
文件夹中运行 npm install
:
cd public npm install
和 server
文件夹:
cd server npm install
如果您想查看使用 Angular 5 的这个项目的版本,请查看 this repo。 就本教程而言,两个版本之间的差异无关紧要。
第 2 步 — 替换 AngularJS 语法
在我们的应用程序的这个阶段,我们的订单组件在 Angular 中被重写,它的所有依赖项都被注入和解析。 但是,如果我们要尝试运行我们的应用程序,我们会在控制台中看到错误,表明我们的模板存在问题。 这就是我们需要首先解决的问题。 我们将替换订单模板 (orders/orders.html
) 中的 AngularJS 语法,以便我们可以获取路由加载和页面上显示的订单。 接下来我们将修复过滤和排序。
我们需要做的第一件事是删除此模板中所有 $ctrl
的实例。 它们在 Angular 中不再是必需的。 我们可以做一个查找和替换来查找 $ctrl.
(注意点),然后什么都不替换。
现在让我们替换第 13 行按钮中的 data-ng-click
。 在 Angular 中,我们只使用 click
事件而不是 ng-click
事件,用括号表示它是一个事件。 括号表示输入,括号表示输出或事件。
<button type="button" (click)="goToCreateOrder()" class="btn btn-info"> Create Order </button>
我们只是在这里说,在点击事件中,在我们的订单组件上触发 goToCreateOrder
函数。
在我们继续之前,让我们花一点时间来证明我们的组件实际上正在加载。 注释掉加载我们订单的整个 div
(从第 17 行开始)。 要运行应用程序,请打开终端并运行以下命令:
cd server npm start
这将启动 Express 服务器。 要运行 Webpack dev
服务器,请打开另一个终端并运行:
cd public npm run dev
您可以在本教程的剩余部分保持这些进程运行。
您应该会看到我们的应用程序再次加载。 如果您转到订单路线,您会看到订单组件正确显示。
我们也可以点击 Create Order 按钮,它会正确地将我们发送到我们的 Create Order 路线和表格。
好的,让我们回到 HTML。 取消注释 div
(我们的应用程序将再次被破坏)。
让我们用 (click)
事件处理程序替换所有其余的实例 data-ng-click
。 您可以使用“查找和替换”或使用编辑器的快捷方式来选择所有匹配项(在 Visual Studio Code for Windows 中,这是 CTRL+SHIFT+L
)。
接下来,将所有出现的 data-ng-show
替换为 *ngIf
。 在 Angular 中实际上没有直接等效于 ng-show
的方法,但没关系。 最好使用 *ngIf
因为这样你实际上是在 DOM 中添加和删除元素,而不是仅仅隐藏和显示它们。 因此,我们需要做的就是找到我们的 data-ng-show
并将它们替换为 *ngIf
。
最后,我们需要做两件事来修复我们的表体。 首先,将 data-ng-repeat
替换为 *ngFor="let order of orders"
。 请注意,我们还删除了该行中的 orderBy
和 filter
过滤器,以便整个 tr
看起来像这样:
<tr *ngFor="let order of orders">
其次,我们可以删除指向订单详情路由的 href
链接之前的 data-ng
前缀。 AngularJS 仍然在这里处理路由,但我们不再需要使用该前缀,因为它现在是一个 Angular 模板。
如果我们再次查看应用程序,您可以看到订单在屏幕上正确加载:
当然,它有一些问题。 排序链接不再起作用,现在我们的货币有点混乱,因为 Angular 中的货币管道与其 AngularJS 对应物略有不同。 我们会解决的。 现在,这是一个好兆头,因为这意味着我们的数据正在到达组件并加载到页面上。 因此,我们已经将该模板的基础知识转换为 Angular。 现在我们准备好处理我们的排序和过滤了!
第 3 步 - 添加排序
我们已经在屏幕上加载了我们的订单,但是我们还没有办法对它们进行排序或排序。 在 AngularJS 中,使用内置的 orderBy
过滤器对页面上的数据进行排序是很常见的。 Angular 不再有 orderBy
过滤器。 这是因为现在强烈鼓励将这种业务逻辑移动到组件中,而不是将其放在模板上。 所以,这就是我们要在这里做的。
注意: 我们将在这里使用普通的旧函数和事件,而不是反应形式的方法。 这是因为我们只是试图采取一些小步骤来理解这些东西。 一旦你掌握了基础知识,请随时使用 observables 更进一步!
在组件中排序
当我们将其更改为 *ngFor
时,我们已经从 ng-repeat
中删除了 orderBy
过滤器。 现在我们将在订单组件上创建一个排序功能。 我们可以使用表格标题上的点击事件来调用该函数并传入我们想要排序的属性。 我们还将让该功能在升序和降序之间来回切换。
让我们打开订单组件 (./orders/orders.component.ts
) 并向类添加两个公共属性。 这些将匹配我们的模板已经引用的两个属性。 第一个是 string
类型的 sortType
。 第二个将是 boolean
类型的 sortReverse
,我们将默认值设置为 false。 sortReverse
属性只是跟踪是否翻转顺序 - 不要将其视为升序或降序的同义词。
所以你现在应该在类中声明标题之后拥有这个:
sortType: string; sortReverse: boolean = false;
接下来,我们将添加我们将在 JavaScript 中与 Array.sort 原型函数一起使用的函数。 在 goToCreateOrder
函数之后添加这个(但仍在类中):
dynamicSort(property) { return function (a, b) { let result = (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0; return result; } }
此动态排序函数将比较数组中对象的属性值。 嵌套三元函数乍一看可能有点难以理解,但它基本上只是说如果我们的 A 的属性值小于 B,则返回 -1。 否则,如果它更大,则返回 1。 如果两者相等,则返回 0。
现在,这不是一个超级复杂或深度的比较。 您可以编写更复杂的辅助函数来为您排序,并随意尝试如何破解它。 不过,它可以满足我们的目的,您可以将这个逻辑替换为您喜欢的任何自定义排序逻辑。
这就是我们的辅助函数。 可以向 Array 原型上的 sort 函数传递一个函数,然后它可以使用该函数来比较数组中的项目。 让我们在我们的类上创建一个名为 sortOrders
的函数,利用新的 dynamicSort
函数来利用它:
sortOrders(property) { }
我们需要做的第一件事是设置我们类的 sortType
属性等于传入的属性。 然后我们要切换 sortReverse
属性。 我们会有这个:
sortOrders(property) { this.sortType = property; this.sortReverse = !this.sortReverse; }
现在我们可以在 this.orders
上调用 sort
函数,但是通过我们的属性传入我们的动态排序函数:
sortOrders(property) { this.sortType = property; this.sortReverse = !this.sortReverse; this.orders.sort(this.dynamicSort(property)); }
我们还需要做最后一件事。 我们需要稍微修改一下我们的 dynamicSort
函数,以便能够反转数组的升序或降序顺序。 为此,我们将 dynamicSort
的结果绑定到类的 sortReverse
属性。
我们要做的第一件事是声明一个变量:
let sortOrder = -1;
然后,我们可以检查我们类的 sortReverse
属性是真还是假。 如果为真,我们将设置排序顺序变量等于 1:
if (this.sortReverse) { sortOrder = 1; }
我们像这样将我们的函数绑定在一起,因为为了演示,我们在排序函数中进行了切换。 更彻底地说,另一种方法是使用一个名为 sortDescending
的变量,而不是通过单独的函数控制的 sortReverse
。 如果你走这条路,你会做相反的事情——除非 sortDescending
为真,否则 sortOrder
将为 1。
我们也可以将最后两件事组合成一个三元表达式,但为了清楚起见,我将让它稍微冗长一些。 然后为了使我们的结果与通常的结果相反,我可以将 result
乘以我们的 sortOrder
。 所以我们的 dynamicSort
函数现在看起来像这样:
dynamicSort(property) { let sortOrder = -1; if (this.sortReverse) { sortOrder = 1; } return function(a, b) { let result = a[property] < b[property] ? -1 : a[property] > b[property] ? 1 : 0; return result * sortOrder; }; } }
同样,这是排序的演示实现,以便您了解在组件上使用自定义排序功能的关键概念。
检查排序
到目前为止,我们已经为我们的类添加了一个 dynamicSort
辅助函数和一个 sortOrders
函数,这样我们就可以对我们的组件而不是我们的模板进行排序。
要查看这些函数是否正常工作,让我们为我们的 ngOnInit
函数添加一个默认排序。
在我们的 forkJoin
订阅中,在我们添加客户名称属性的 forEach
之后,我们调用 this.sortOrders
并传入总项目属性:
this.sortOrders('totalItems');
当屏幕刷新时,您应该看到订单正在按总项目排序。
现在我们只需要通过调用表头链接中的 sortOrders
函数在我们的模板上实现这种排序。
向模板添加排序
我们的 sortOrders
函数在我们的订单组件上正常工作,这意味着我们现在准备将其添加到我们的模板中,以便再次点击表格标题。
在我们这样做之前,让我们将 ngOnInit
函数中的默认排序更改为 ID:
this.sortOrders('id');
这比使用总项目更正常一点。
现在我们可以处理我们的模板了。 我们要做的第一件事是在所有点击事件中调用 sortOrders
函数。 您可以选择 sortType =
的实例并将其替换为 sortOrders(
。 然后,您可以将 ; sortReverse = !sortReverse
的实例替换为 )
。
我们还需要修复我们在此处以及 *ngIf
实例中传入的两个属性名称。 将 orderId
的 3 个实例替换为 id
,将 customername
的 3 个实例替换为 customerName
。
我需要做的最后一件事是将每个 href
标记包装在括号中的标题中,以便 Angular 接管并且这些链接实际上不会去任何地方。 点击事件将被触发。 因此,标题应遵循以下模式:
<th> <a [href]="" (click)="sortOrders('id')"> Order Id <span *ngIf="sortType == 'id' && !sortReverse" class="fa fa-caret-down"></span> <span *ngIf="sortType == 'id' && sortReverse" class="fa fa-caret-up"></span> </a> </th>
跳到浏览器并测试所有表格标题链接。 您应该看到我们的每个属性现在都按升序和降序排序。 惊人的!
这很好,但我们确实丢失了一件事——我们的光标是一个选择器,而不是一个指针。 让我们用一些 CSS 来解决这个问题。
固定光标
我们的订单页面上的排序工作正常,但是我们的光标现在是一个选择器而不是一个指针,这很烦人。
我们可以通过几种不同的方式使用 CSS 来解决这个问题:
- 我们可以在我们的主应用程序 SCSS 文件中创建一个类。
- 我们可以编写内联 CSS,尽管这几乎是不可取的。
- 我们可以使用组件装饰器中的样式选项来利用 Angular 的作用域 CSS
我们将使用最后一个选项,因为我们需要做的就是为这个特定组件的样式添加一个规则。
再次打开订单组件类。 在组件装饰器中,我们可以添加一个名为 styles
的新属性。 Styles 是一个字符串数组,但字符串是 CSS 规则。 要修复我们的光标,我们需要做的就是写出一条规则,说明在表格行中,如果我们有链接,则将光标属性更改为指针。 我们的装饰器现在看起来像这样:
@Component({ selector: 'orders', template: template, styles: ['tr a { cursor: pointer; }'] })
现在,当我们将鼠标悬停在行标题上时,您会看到我们有指针光标。 这种方法很酷的是这个 CSS 规则不会影响任何其他组件。 它只适用于我们的订单组件!
现在,让我们看看我们是否可以对过滤做点什么。 那个“过滤器过滤器”已经从 Angular 中移除了,所以我们必须要有创意,想出一种方法在我们的组件上实现它。
第 4 步 - 添加过滤
我们已准备好替换我们的过滤器框,该过滤器框曾经使用 AngularJS 过滤器根据我们正在搜索的字符串来搜索订单集合。 AngularJS 过滤器存在于我们的模板中,不需要我们的控制器或组件中的任何代码。 如今,不鼓励模板中的那种逻辑。 最好对我们的组件类进行这种排序和过滤。
添加过滤功能
回到我们的组件中,我们将创建一个名为 filteredOrders
的新订单数组。 然后我们将 orders
数组传递给设置 filteredOrders
数组的过滤器函数。 最后,我们将在我们的 *ngFor
中的模板上使用 filteredOrders
而不是我们的原始数组。 这样我们就不会修改从服务器返回的数据,我们只是使用它的一个子集。
我们要做的第一件事是在我们的类上声明新属性:
filteredOrders: Order[];
然后,在设置原始订单数组的 forkJoin
中,我们可以将 filteredOrders
的初始状态设置为订单数组:
this.filteredOrders = this.orders;
现在我们已经准备好添加我们的函数,它实际上会为我们进行过滤。 将此函数粘贴到组件底部的排序函数之后:
filterOrders(search: string) { this.filteredOrders = this.orders.filter(o => Object.keys(o).some(k => { if (typeof o[k] === 'string') return o[k].toLowerCase().includes(search.toLowerCase()); }) ); }
让我们谈谈这个函数中发生了什么。 首先,我们给函数一个字符串属性 search
。 然后,我们遍历我们的订单,然后找到对象的所有键。 对于所有键,我们将查看这些属性的 some
值是否与我们的搜索词匹配。 这段 JavaScript 代码乍一看可能有点令人困惑,但基本上就是这样。
请注意,在我们的 if
语句中,我们明确地测试字符串。 在我们现在的示例中,我们只是将查询限制为字符串。 我们不会尝试处理嵌套属性、数字属性或类似的东西。 我们的搜索词将匹配我们的客户名称属性,如果我们选择显示我们的地址或任何其他字符串属性,它也会搜索这些属性。
当然,我们也可以修改这个函数来测试数字,或者查看另一层嵌套对象,这完全取决于你。 就像我们的排序一样,我们将从一个演示实现开始,让你发挥你的想象力,让它变得更复杂。
说到 sortOrders
函数,在我们继续之前,我们需要对组件做最后一件事。 我们只需要修改 sortOrders
现在使用 filteredOrders
而不是我们原来的 orders
,因为我们希望过滤器优先于排序。 只需将其更改为:
sortOrders(property) { this.sortType = property; this.sortReverse = !this.sortReverse; this.filteredOrders.sort(this.dynamicSort(property)); }
现在我们准备在模板上实现这个过滤。
向模板添加过滤
让我们回到我们的模板并修复它以使用我们的过滤器。
我们需要做的第一件事是替换 data-ng-model
。 取而代之的是,我们将使用 keyup
事件,因此我们将编写“keyup”并将其括在括号中 ((keyup)
)。 这是 Angular 中的一个内置事件,它允许我们在输入的按键上运行一个函数。 由于我们将函数命名为 filterOrders
,它曾经是我们传递给 AngularJS 过滤器的属性的名称,我们只需要在它旁边添加括号。 到目前为止,我们的输入看起来像这样:
<input type="text" class="form-control" placeholder="Filter Orders (keyup)="filterOrders()">
但是我们将什么传递给过滤订单函数? 嗯,默认情况下,事件传递一个叫做 $event
的东西。 这包含称为 target
的东西,然后包含输入的值。 使用 $event
有一个问题。 跟踪这些模糊的类型非常困难,因为 target.value
真的可以是任何东西。 这使得调试或知道预期的值类型变得困难。 相反,Angular 有一件我们可以做的非常好的事情,就是为这个输入分配一个 模板变量 。
幸运的是,Angular 为我们提供了一种方法来做到这一点。 在我们的输入标签之后,我们可以添加井号 (#
),然后是我们想要的模型的名称。 我们称它为 #ordersFilter
。 你把它放在标签的哪个位置或你叫它什么并不重要,但我喜欢把它放在输入之后,这样如果我只是浏览页面,你就会发现哪个模型与哪个输入相关联。
现在我可以在 keyup
事件上将该变量传递给我们的 filterOrders
函数。 我们不需要它前面的井号,但我们确实需要添加 .value
。 这将传递模型的实际值,而不是整个模型本身。 我们完成的输入如下所示:
<input #ordersFilter type="text" class="form-control" placeholder="Filter Orders" (keyup)="filterOrders(ordersFilter.value)">
最后,我们需要修改我们的 *ngFor
以使用 filteredOrders
数组而不是常规的 orders
数组:
<tr *ngFor="let order of filteredOrders">
- 检查产品
现在我们的过滤和排序都在组件中,您可以看到我们的模板变得多么干净。
现在让我们在浏览器中检查一下。 如果您在框中输入一些文本,您应该会看到我们的订单正在发生变化,并且排序在其之上进行:
太棒了,我们已经替换了另一个 AngularJS 功能!
现在我们只需要在这个组件上做最后一件事——修复货币管道。
第 5 步 — 修复货币管道
最后一步是更新以前的货币过滤器,现在在 Angular 中称为货币 pipe。 我们只需要在模板中的管道中添加几个我们不必在 AngularJS 中指定的参数。 如果您使用的是 Angular 4 或 Angular 5,这部分会有所不同:
在 Angular 4 中,执行以下操作:
<td>{{order.totalSale | currency:'USD':true}}</td>
在 Angular 5+ 中,执行以下操作:
<td>{{order.totalSale | currency:'USD':'symbol'}}</td>
第一个选项是货币代码(有很多,你不限于美元)。 第二个是符号显示。 在 Angular 4 中,这是一个布尔值,指示是使用货币符号还是代码。 在 Angular 5+ 中,选项是 symbol
、code
或 symbol-narrow
作为字符串。
您现在应该看到预期的符号:
我们完成了! 要查看完成的代码,查看这个提交。
结论
坚持到最后你做得很好! 以下是我们在本指南中完成的内容:
- 用 Angular 语法替换 AngularJS 模板语法
- 将排序移动到组件
- 使用作用域 CSS 样式
- 将过滤移动到组件
- 用 Angular 货币管道替换 AngularJS 货币过滤器
你应该从这里去哪里? 你可以做很多事情:
- 使排序更复杂(例如:当用户单击新标题时,排序应该重置还是保持不变?)
- 使过滤更复杂(搜索数字或嵌套属性)
- 改用被动的方法。 您可以收听可观察到的值变化,而不是
keyup
函数,并在其中进行排序和过滤。 使用 observables 还可以让你做一些很酷的事情,比如去抖动输入!