angular - 三态复选框的支持字段被(重新)评估得太频繁
问题描述
我有一个用于多行、la GMail 和类似应用程序的三态复选框选择的“天真”实现。您可以选择单个行,并且有一个顶级三态复选框,指示:
- 状态“检查所有行”
- 状态“检查了一些但不是所有行”(中级)
- 状态“检查了零行”
我说“天真”,因为支持该顶级复选框的字段经常被重新评估,我觉得我可能需要Subject
或Observable
字段来支持它。
这是我当前实现的重现。
ng new obstest --minimal
(角度 5 CLI)cd obstest
ng generate service search
并将其添加到app.module
providers
将此模拟方法添加到服务中:
search(query: Observable<string>) { // Fake search, ignore query for demo return of<any[]>([ { isSelected: false, id: 1, txt: 'item 1' }, { isSelected: false, id: 2, txt: 'item 2' }, { isSelected: false, id: 3, txt: 'item 3' }, ]); }
通常这将使用
HttpClient
从搜索 API 端点获取结果。将此添加到
app.component.ts
文件中:enum TriState { NothingSelected = '[ ]', IntermediateSelection = '[-]', EverythingSelected = '[X]', }
将组件的装饰更改为:
@Component({ selector: 'app-root', template: ` <div><input (keyup)="query$.next($event.target.value)"></div> <div (click)="onMultiSelectChange()">{{selectionState}}</div> <ul *ngFor="let item of results"> <li (click)='item.isSelected = !item.isSelected'> {{item.isSelected ? '[X]' : '[ ]'}} {{item.txt}} </li> </ul>`, })
用这个替换组件的代码:
export class AppComponent { results: any[]; query$ = new Subject<string>(); public get selectionCount() { console.warn('Getting count at', new Date().toISOString()); return this.results.filter(r => r.isSelected).length; } public get selectionState() { console.warn('Getting state at', new Date().toISOString()); if (this.selectionCount === 0) { return TriState.NothingSelected; } if (this.selectionCount === this.results.length) { return TriState.EverythingSelected; } return TriState.IntermediateSelection; } constructor (service: SearchService) { service.search(of('fake query')).subscribe(r => this.results = r); } onMultiSelectChange() { if (this.selectionState === TriState.EverythingSelected) { this.results.forEach(r => r.isSelected = false); } else { this.results.forEach(r => r.isSelected = true); } } }
import
每个文件中的相关内容ng serve --open
(重新)加载应用程序并打开控制台窗口,然后:
- 结果:控制台上有八个警告(在我的实际应用程序中,这甚至更多),并且当您选择/取消选择项目时继续流式传输,即使当观察到的变化没有关系/影响时也是如此。
- 预期:加载时控制台上的两个警告,两个其他字段的相关更改。
在 KnockoutJS 中,我知道如何使用“计算的 observables”(可能是纯计算的)来做到这一点,而且我确信这可以使用 Angular 5+ 来完成(可能在 rxjs 的帮助下?)。我只是不确定如何。
我将如何更改selectionCount
以及selectionState
视图可以数据绑定到它们的方式,但仅在需要时(重新)评估它们?
谁能告诉我惯用的 Angular 和/或 RxJs 解决方案?
解决方案
this.results
从 开始null
,因此它在生命周期中有两个分配:首先null
是 ,然后是[ ... mock data ... ]
您提供的数组。
调查你的吸气剂:
public get selectionCount() {
console.warn('Getting count at', new Date().toISOString());
return this.results.filter(r => r.isSelected).length;
}
public get selectionState() {
console.warn('Getting state at', new Date().toISOString());
if (this.selectionCount === 0) { return TriState.NothingSelected; }
if (this.selectionCount === this.results.length) { return TriState.EverythingSelected; }
return TriState.IntermediateSelection;
}
当selectionState
被调用时,它会调用一个警告,然后调用selectionCount
两次,因此每次调用selectionState
. Angular 不会对 getter 进行任何缓存。由于 的两个分配,它们在整个生命周期中被调用两次this.results
,这占负载警告的六个。我不确定剩下的两个是从哪里来的。
编写此类的一种更 RxJS 的方式是避免状态突变并使用可观察的方式完成所有操作,例如:
export class AppComponent {
results$: Observable<any[]>;
selections$ = new BehaviorSubject<boolean[]>([]);
selectionCount$: Observable<number>;
selectionState$: Observable<TriState>;
query$ = new Subject<string>();
constructor (service: SearchService) {
this.results$ = service.search(of('fake query')).pipe(shareReplay(1));
this.selectionCount$ = combineLatest(this.results$, this.selections$).pipe(
map(([results, selections]) => results.filter((result, i) => selections[i])),
map(results => results.length),
);
this.selectionState$ = of(TriState.IntermediateSelection).pipe(concat(this.results.pipe(map(
results => {
if (this.selectionCount === 0) { return TriState.NothingSelected; }
if (this.selectionCount === this.results.length) { return TriState.EverythingSelected; }
}))));
}
toggle(i) {
selections$.value[i] = !selections$.value[i];
selections$.next(selections$.value);
}
toggleAll() {
combineLatest(this.selectionState$, this.results$).pipe(
first(),
map(([state, results]) => {
return results.map(() => state === TriState.EverythingSelected);
}))
.subscribe(this.selections$);
}
}
上面可能有错误,我没有测试它,但希望它传达了这个想法。对于模板,您必须使用| async
管道,例如:
@Component({
selector: 'app-root',
template: `
<div><input (keyup)="query$.next($event.target.value)"></div>
<div (click)="toggleAll()">{{selectionState | async}}</div>
<ul *ngFor="let item of results | async">
<li (click)='toggle($index)'>
{{item.isSelected ? '[X]' : '[ ]'}} {{item.txt}}
</li>
</ul>`,
})
不幸的是,Angular 没有像 Redux 那样提供任何标准化的状态管理来强制执行这种模式,所以你要么必须有足够的纪律才能自己做,要么可以接受额外的调用。
或者,您也可以有一个包装器组件来处理Observable
相关的状态并且没有模板,并且让子组件只呈现状态。这将避免所有状态转换,您只需要async
观察结果即可。我认为这被称为重/轻组件模式?这是一种非常流行的模式,可以避免到处处理 observables,但是我认为我的名字弄错了。
推荐阅读
- kivy - 试图在 kivy 上制作一个拉伸图像大小的按钮
- php - 通过浏览器语言重定向,在 php 和 .htaccess 中都给出错误:重定向太多
- ruby-on-rails - 在 Rails 电子商务网络应用程序上评论 ruby 时出现错误 400
- c# - 在 C# 中将多个图像添加到表单中
- ngrx - NGRX:当我们必须从两个列表中表示数据时组合状态
- css - SASS - 除了一个属性外,如何为两个类分配相同的样式?
- lua - lua字符串连接得到一个奇怪的字符串
- java - 使用大集合或包含集合的等效地图是否很好?
- sql-server - 带有 SQL Server 后端和 pyodbc 的气流调度程序
- php - 如何在 Odoo Web 服务 API 上获取销售订单行描述?