RxJS和angular.io:可观察的<;数组<;T>&燃气轮机;其中T包含一个可观测数组
事实上,我确实试图找到一个类似于这个问题的问题,因为我发现很难接受以前没有人问过这个问题(尽管它可能是反模式的)。让我们从这个组件开始:RxJS和angular.io:可观察的<;数组<;T>&燃气轮机;其中T包含一个可观测数组,angular,typescript,rxjs,Angular,Typescript,Rxjs,事实上,我确实试图找到一个类似于这个问题的问题,因为我发现很难接受以前没有人问过这个问题(尽管它可能是反模式的)。让我们从这个组件开始: @Component({...}) export class MyParent { myGroups: Observable<Array<MyItemGroup>>; } 首先,这种安排是某种反模式吗?在angular.io中是否还有另一种分组设计被认为是正确的?为了避免经典答案“视情况而定”,让我们更进一步,看看: expo
@Component({...})
export class MyParent {
myGroups: Observable<Array<MyItemGroup>>;
}
首先,这种安排是某种反模式吗?在angular.io中是否还有另一种分组设计被认为是正确的?为了避免经典答案“视情况而定”,让我们更进一步,看看:
export class MyGroupedItem {
title: string;
uri: string;
}
在MyParent
级别,我想通过title
过滤MyGroupedItem
,当这个过滤器将MyItemGroup.group
的计数减少到零时,我想完全过滤掉MyItemGroup
。这强烈建议我必须将mySubject
添加到MyParent
:
@Component({...})
export class MyParent {
myGroups: Observable<Array<MyItemGroup>>;
mySubject: Subject<string> = new Subject<string>();
filterMyGroups(particle: string): void {
this.mySubject.next(particle);
}
}
我只能希望俘虏能够理解我的问题,并且让他们明白。您不需要将这些项目设置为可观察项 以下是您可以做的事情:
- 您可以查看数据API的
,并且搜索过滤器输入发生更改。然后在上面运行observableCombineTest
。map
将采用一个字符串(过滤器)和一个组数组map
- 在可观察的
映射中,您需要基本的数组映射和过滤。
如我所述,您可以获得一个组数组,因此,您可以:- 调用数组
函数以返回新的组数组,每个组都是相同的,只是其映射
数组使用过滤器字符串进行过滤项
- 然后获取结果并调用数组
,以便只返回过滤器
items.length>0
- 调用数组
- 然后,您的组组件只处理一个简单的组对象,传递给它,与那里的可观察对象无关。您可以选择将项目拆分为自己的组件,但不必这样做
@Component({
selector: 'app-parent',
template: `
<p>
Start editing to see some magic happen :)
</p>
<form [formGroup]="form">
<label>
Search
<input type="search" formControlName="search" />
</label>
</form>
<h2>Items</h2>
<app-group
*ngFor="let group of groups$ | async"
[group]="group" ></app-group>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ParentComponent {
constructor(
private service: GroupsService
) { }
groupData$ = this.service.getGroups();
form = new FormGroup({
search: new FormControl('')
});
searchFilter$ = this.form.valueChanges.pipe(
// the map gets an arg in shape of `{search: 'value of search input'}`
map((newFormValue: { search: string }) => newFormValue.search)
);
// `combineLatest` returns values from observables passed to it * as array *
groups$ = combineLatest(
this.groupData$,
// We need to show data without waiting for filter change
this.searchFilter$.pipe(startWith(''))
).pipe(
map(([groups, searchFilter]) =>
groups
.map(group => this.filterItemsInGroup(group, searchFilter))
.filter(group => group.items.length > 0)
)
);
private filterItemsInGroup(group: ItemGroup, searchFilter: string) {
if (!searchFilter) {
return group;
}
return {
// Copy all properties of group
...group,
// This is NOT observable filter, this is basic array filtering
items: group.items.filter(item =>
item.title.toLowerCase().indexOf(searchFilter.toLowerCase())
>= 0
)
};
}
}
现在让我们回到列表问题。我们需要做的是:
- 向具有筛选器的窗体添加新的分组类型下拉列表
- 使用
或任何东西将表单连接到服务,并获得一个可观察的条目、搜索过滤器和分组类型switchMap
- 将从调用数组或表单更改的结果中获得的每个数组映射到由搜索筛选器输入值筛选的新数组
- 使用“分组种类”下拉列表中的值和,将过滤后的项目数组映射到组数组,并将其传递给UI模板
- 如果用手进行分组,具体的逻辑可能会很复杂(就像我在示例中所做的那样),但可以使用Lodash之类的方法大大减少
groups$
属性的设置有多么简单。唯一的复杂性是真正应用分组
@Component({
selector: 'app-parent',
template: `
<p>
Start editing to see some magic happen :)
</p>
<form [formGroup]="form">
<label>
Group
<select formControlName="groupingKind">
<option
*ngFor="let grouping of GroupingKinds"
value="{{grouping}}">{{grouping}}</option>
</select>
</label>
<label>
Filter
<input type="search" formControlName="search" />
</label>
</form>
<h2>Items</h2>
<app-group
*ngFor="let group of groups$ | async"
[group]="group" ></app-group>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ParentComponent {
constructor(
private service: GroupsService
) { }
// To make the template see it
readonly GroupingKinds = Object.keys(GroupingKind)
.map(key => GroupingKind[key]);
form = new FormGroup({
search: new FormControl(''),
// Defaults to ByDate
groupingKind: new FormControl(GroupingKind.ByDate)
});
// This will make a new HTTP request with every form change
// If you don't want that, just switch the order
// of `this.form.valueChanges` and `this.service.getItems()`
groups$ = this.form.valueChanges.pipe(
// initial load
startWith(this.form.value as {search: string, groupingKind: GroupingKind}),
switchMap((form) =>
this.service.getItems().pipe(
// Take every array the `getItems()` returns
// (which is why we use observable `map` not observable `filter`)
// And then transform it into another array
// that happenes to be the same array but filtered
map(items => this.filterItems(items, form.search)),
// Then map the result into group
map(items => this.createGroups(items, form.groupingKind))
)
),
);
private filterItems(items: ItemGroupItem[], searchFilter: string) {
return items.filter(item =>
item.Title.toLowerCase().indexOf(searchFilter.toLowerCase())
>= 0
);
}
private createGroups(src: ItemGroupItem[], groupingKind: GroupingKind) {
const groupsList = [] as ItemGroup[];
src.reduce((groupsObject, item) => {
// Topic groups values are an array, date grouping value is a string,
// So we convert date grouping value to array also for simplicity
const groupNames = groupingKind == GroupingKind.ByTopic
? item.ItemCategory.topics
: [ item.ItemCategory.dateGroup ];
for(const groupName of groupNames) {
if(!groupsObject[groupName]) {
const newGroup: ItemGroup = {
displayName: groupName,
items: []
};
groupsObject[groupName] = newGroup;
groupsList.push(newGroup);
}
groupsObject[groupName].items.push(item);
}
return groupsObject;
}, {} as { [name:string]: ItemGroup} );
return groupsList;
}
}
@组件({
选择器:“应用程序父级”,
模板:`
开始编辑以查看发生的奇迹:)
组
{{分组}}
滤器
项目
mySubject
的目的是什么,为什么你真的需要MyItemGroup.group
成为一个可观察的对象?为了避免“视情况而定”,这个问题应该包含一个真实世界的例子,解释为什么你最终选择了这种设计。如果可以改进,那么就很清楚了。angular.io是网站名称。Framework的名称是角度。我将尝试改变概念,并使用forkJoin订阅一大系列的Observables@Eliseo是的,答案很接近:@estus这是我的作品(但有缺陷),现实世界的例子:因此,我的第一个问题的答案显然是肯定的:将可观察对象附加到普通的旧JS对象在角度上是一种反模式,因为Meligy清楚地表明,FormGroup
和FormControl
将HTML表单变成了一个反应式主题,并根据普通的旧JS对象传递到可观察对象。它不是表单sp具体地说,就是通常如果你觉得你想要一个可观察的子属性,你需要的往往是父可观察的,可能是与另一个可观察的混合(通过mergeMap/switchMap/Zip等)——在这种情况下是形式,然后转换整个对象(通过映射并将更改应用于其中的子属性)。因此,用更少的话来说:与其为子属性提供可观测值,不如只映射父可观测值,您可能需要将其与其他可观测值混合,以获得足够的映射参数
@Component({
selector: 'app-parent',
template: `
<p>
Start editing to see some magic happen :)
</p>
<form [formGroup]="form">
<label>
Search
<input type="search" formControlName="search" />
</label>
</form>
<h2>Items</h2>
<app-group
*ngFor="let group of groups$ | async"
[group]="group" ></app-group>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ParentComponent {
constructor(
private service: GroupsService
) { }
groupData$ = this.service.getGroups();
form = new FormGroup({
search: new FormControl('')
});
searchFilter$ = this.form.valueChanges.pipe(
// the map gets an arg in shape of `{search: 'value of search input'}`
map((newFormValue: { search: string }) => newFormValue.search)
);
// `combineLatest` returns values from observables passed to it * as array *
groups$ = combineLatest(
this.groupData$,
// We need to show data without waiting for filter change
this.searchFilter$.pipe(startWith(''))
).pipe(
map(([groups, searchFilter]) =>
groups
.map(group => this.filterItemsInGroup(group, searchFilter))
.filter(group => group.items.length > 0)
)
);
private filterItemsInGroup(group: ItemGroup, searchFilter: string) {
if (!searchFilter) {
return group;
}
return {
// Copy all properties of group
...group,
// This is NOT observable filter, this is basic array filtering
items: group.items.filter(item =>
item.title.toLowerCase().indexOf(searchFilter.toLowerCase())
>= 0
)
};
}
}
@Component({
selector: 'app-group',
template: `
<h3
class="group-name"
(click)="showItems = ! showItems"
>{{group.displayName}}</h3>
<ng-container *ngIf="showItems">
<app-item
*ngFor="let item of group.items"
[item]="item"
></app-item>
</ng-container>
`
})
export class GroupComponent {
@Input() group: ItemGroup;
showItems = false;
}
@Component({
selector: 'app-parent',
template: `
<p>
Start editing to see some magic happen :)
</p>
<form [formGroup]="form">
<label>
Group
<select formControlName="groupingKind">
<option
*ngFor="let grouping of GroupingKinds"
value="{{grouping}}">{{grouping}}</option>
</select>
</label>
<label>
Filter
<input type="search" formControlName="search" />
</label>
</form>
<h2>Items</h2>
<app-group
*ngFor="let group of groups$ | async"
[group]="group" ></app-group>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ParentComponent {
constructor(
private service: GroupsService
) { }
// To make the template see it
readonly GroupingKinds = Object.keys(GroupingKind)
.map(key => GroupingKind[key]);
form = new FormGroup({
search: new FormControl(''),
// Defaults to ByDate
groupingKind: new FormControl(GroupingKind.ByDate)
});
// This will make a new HTTP request with every form change
// If you don't want that, just switch the order
// of `this.form.valueChanges` and `this.service.getItems()`
groups$ = this.form.valueChanges.pipe(
// initial load
startWith(this.form.value as {search: string, groupingKind: GroupingKind}),
switchMap((form) =>
this.service.getItems().pipe(
// Take every array the `getItems()` returns
// (which is why we use observable `map` not observable `filter`)
// And then transform it into another array
// that happenes to be the same array but filtered
map(items => this.filterItems(items, form.search)),
// Then map the result into group
map(items => this.createGroups(items, form.groupingKind))
)
),
);
private filterItems(items: ItemGroupItem[], searchFilter: string) {
return items.filter(item =>
item.Title.toLowerCase().indexOf(searchFilter.toLowerCase())
>= 0
);
}
private createGroups(src: ItemGroupItem[], groupingKind: GroupingKind) {
const groupsList = [] as ItemGroup[];
src.reduce((groupsObject, item) => {
// Topic groups values are an array, date grouping value is a string,
// So we convert date grouping value to array also for simplicity
const groupNames = groupingKind == GroupingKind.ByTopic
? item.ItemCategory.topics
: [ item.ItemCategory.dateGroup ];
for(const groupName of groupNames) {
if(!groupsObject[groupName]) {
const newGroup: ItemGroup = {
displayName: groupName,
items: []
};
groupsObject[groupName] = newGroup;
groupsList.push(newGroup);
}
groupsObject[groupName].items.push(item);
}
return groupsObject;
}, {} as { [name:string]: ItemGroup} );
return groupsList;
}
}