RxJS和angular.io:一个Observable <array <t>&gt;其中T包含一个可观察的数组

时间:2017-12-21 03:32:58

标签: angular typescript rxjs

事实上,我确实试图找到一个类似的问题,因为我发现很难接受它之前没有被问过(即使它可能是一个反模式)。让我们从这个组件开始:

@Component({...})
export class MyParent {
    myGroups: Observable<Array<MyItemGroup>>;
}

其中MyItemGroup是:

export class MyItemGroup {
    groupDisplayName: string;
    group: Observable<Array<MyGroupedItem>>;
}

首先,这种安排是否有某种反模式?是否有另一种分组设计在angular.io中被认为是正确的?为了避免经典答案,“这取决于”,让我们进一步看看:

export class MyGroupedItem {
    title: string;
    uri: string;
}

MyParent级别,我想按MyGroupedItem过滤title,当此过滤器将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);
    }
}

在这里,我假设我需要与RxJS一起去城镇,将myGroupsmySubject结合起来。怎么做到这一点?用户界面将通过此标记引用mySubject

<input #myFilter (keyup)="filterMyGroups(myFilter.value)" />

我只能希望俘虏的希望能使我的问题得到理解和明确。

1 个答案:

答案 0 :(得分:3)

您不需要将这些项目设置为可观察项目。

您可以做以下事情:

  • map数据API observable和搜索过滤器输入发生了变化。然后在其上运行可观察的map map将采用字符串(过滤器)和一组数组。
  • 在可观察的map内,您需要基本的数组映射和过滤 如上所述,您可以获得一组组,因此,您可以:
    • 调用数组 items函数返回新的组数组,每个组都相同,除了使用过滤字符串过滤其filter数组
    • 然后获取结果并在其上调用数组 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 ) }; } }
    • 的组
  • 然后你的组件只处理一个简单的组对象,传递给它,与那里的observable没什么关系。您可以选择将项目拆分为自己的组件,但不必

这是第二步的一些代码:

OnPush

这是一个包含所有部分的完整工作样本:

https://stackblitz.com/edit/angular-geujrq?embed=1&file=app/app.module.ts&hideExplorer=1&hideNavigation=1

该示例添加了一些优化(例如将项目分隔到其自己的组件中,并使用trackBy更改跟踪以获得稍快的输出。可以添加更多优化,例如使用@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; }

更新

最初的海报来自Twitter和explained his situation was more complex

他想要

  • 要从项目列表中动态创建的组
  • 下拉菜单,根据
  • 选择要对项目进行分组的字段
  • 默认情况下隐藏的每个组中的项目,以及单击组名称时可见的

这实际上使隐藏组的问题更容易,因为您可以在对项目进行分组之前对其进行过滤。

隐藏物品也不一定是这个问题的一部分。如果你的组在它自己的组件中,你可以在该组件中有一个默认隐藏项目的布尔值,然后在组名称点击时将其翻转。

让我们先看看这个位的代码,因为它最简单:
(省略了样式和其他位,但我将链接到下面更新的完整示例)

switchMap

现在让我们回到上市问题。我们需要做的是:

  • 向具有过滤器
  • 的表单添加新的分组类型下拉列表
  • 使用groups$或其他任何方式将表单连接到服务,并使用项目,搜索过滤器和分组类型获取一个可观察的
  • 将我们从调用数组或表单更改结果获得的每个数组映射到按搜索过滤器输入值过滤的新数组
  • 使用分组类型下拉列表中的值将过滤后的项目数组映射到组数组,然后将其移交给UI模板
    • 如果手工完成,分组的具体逻辑可能很复杂(就像我在样本中所做的那样),但使用像Lodash这样的东西可以大大减少

让我们看看整个代码:

请注意@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; } } 属性的设置有多简单。唯一的复杂性是真正应用分组。

{{1}}

正如所承诺的,这里有完整的更新样本:

https://stackblitz.com/edit/angular-19z4f1?embed=1&file=app/app.module.ts&hideExplorer=1&hideNavigation=1