Running Angular Material 2 and TS 2.4.
Overview
- Currently, I have a cdk-table that is generated by iterating over a JSON[] that I retrieve from
ThisServiceand add to a localmockDbaccording to these docs. ThisDataSourceis responsible for asynchronously filtering the data of any of the fields via thefilter.expandRow()shows the name of whatever row was clicked on in the generated window.
Issue
The issue that I am running into concerns the function of expandRow()
after any filtering has taken place. If there is anything in the filter bar, the index of the expandRow() is still correct, but the name that appears in the expanded row is always based on the index of the unfiltered array of values.
My guess is that this happens because I don't understand how to use trackBy despite reading the documentation and other resources that I cannot link because I have no reputation yet on StackOverflow.
Code is here:
my.component.ts
import { OnInit } from '@angular/core';
import { filter } from 'rxjs/operator/filter';
import { Observable } from 'rxjs/Observable';
import { MyObject } from '../../models/myObject';
import { MdPaginator } from '@angular/material';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { MyService } from '../../services/myservice.service';
import { DataSource, CdkTable, CdkRowDef } from '@angular/cdk';
import { Headers, Http, RequestMethod, RequestOptions, Response } from '@angular/http';
import {
ComponentRef,
ComponentFactory,
ViewChildren,
ViewContainerRef,
Input,
Component,
ComponentFactoryResolver,
ElementRef,
ViewChild
} from '@angular/core';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/merge';
import 'rxjs/add/operator/startWith';
@Component({
selector: 'app-inline-message',
template: 'Detail: {{ user }} ',
styles: [`
:host {
display: block;
padding: 24px;
color: red;
background: rgba(0,0,0,0.1);
}
`]
})
export class InlineMessageComponent {
@Input() user: string;
}
@Component({
selector: 'activate-this',
templateUrl: './activate.this.component.html',
providers: [ThisService],
styleUrls: ['../../../../expanding-table.css']
})
export class Component {
displayedColumns = ['foo', 'bar', 'qux', 'grault'];
mockDb = new MockDatabase(this.service);
dataSource: MockDataSource | null;
expandedRow: number;
constructor(private service: MyService, private resolver: ComponentFactoryResolver) { }
@ViewChild('filter') filter: ElementRef;
@ViewChildren('cdkrow', { read: ViewContainerRef }) containers: any;
ngOnInit() {
this.dataSource = new MockDataSource(this.mockDb);
Observable.fromEvent(this.filter.nativeElement, 'keyup')
.debounceTime(100)
.distinctUntilChanged()
.subscribe(() => {
if (!this.dataSource) { return; }
this.dataSource.filter = this.filter.nativeElement.value;
});
}
expandRow(index: number) {
if (this.expandedRow != null) {
// clear old message
this.containers.toArray()[this.expandedRow].clear();
}
if (this.expandedRow === index) {
this.expandedRow = null;
} else {
const container = this.containers.toArray()[index];
const factory: ComponentFactory<any> = this.resolver.resolveComponentFactory(InlineMessageComponent);
const messageComponent = container.createComponent(factory);
messageComponent.instance.user = this.mockDb.data[index].foo;
this.expandedRow = index;
}
}
trackByFilter(index: any, item: any) {
return index;
}
}
export class MockDatabase {
myObjects: MyObject[];
/** Stream that emits whenever the data has been modified. */
dataChange: BehaviorSubject<MyObject[]> = new BehaviorSubject<MyObject[]>([]);
get data(): MyObject[] { return this.dataChange.value; }
constructor(private service: MyService) {
// Fill up the local database with the
service.getAllUnactivatedMyObject().subscribe(myObject => {
for (let object of myObject.json())
{ this.addUnactivatedmyObject(myObject); }
});
}
// Add a myObject to the database
addUnactivatedMyObject(myObject: MyObject) {
const copiedData = this.data.slice();
copiedData.push(myObject);
this.dataChange.next(copiedData);
}
}
/**
* Data source to provide what data should be rendered in the table. Note that the data source
* can retrieve its data in any way. In this case, the data source is provided a reference
* to a common data base, MockDatabase. It is not the data source's responsibility to manage
* the underlying data. Instead, it only needs to take the data and send the table exactly what
* should be rendered.
*/
export class MockDataSource extends DataSource<any> {
_filterChange = new BehaviorSubject('');
get filter(): string { return this._filterChange.value; }
set filter(filter: string) { this._filterChange.next(filter); }
constructor(private _mockDb: MockDatabase) {
super();
}
/* Connect function called by the table to retrieve one stream containing the data to render. */
/* it is only called once, regardless of filtering */
connect(): Observable<MyObject[]> {
const displayDataChanges = [
this._mockDb.dataChange,
this._filterChange,
];
//here is the filter
return Observable.merge(...displayDataChanges).map(() => {
return this._mockDb.data.slice()
.filter((myObject: MyObject) => {
let searchStr = (myObject.qux + myObject.foo + myObject.grault + myObject.bar).toLowerCase();
return searchStr.indexOf(this.filter.toLowerCase()) != -1;
});
});
}
disconnect() { }
}
my.component.html
<div class="container">
<div>
<h3>Activate MyObjects</h3>
</div>
<div class="header">
<md-input-container floatPlaceholder="never">
<input mdInput #filter placeholder="Filter myObjects">
</md-input-container>
</div>
<div #insertionPoint></div>
<cdk-table #table [dataSource]="dataSource" class="example-table">
<ng-container cdkColumnDef="foo">
<cdk-header-cell *cdkHeaderCellDef class="example-header-cell"> Foo </cdk-header-cell>
<cdk-cell *cdkCellDef="let row;" class="example-cell"> {{row.foo}} </cdk-cell>
</ng-container>
<ng-container cdkColumnDef="bar">
<cdk-header-cell *cdkHeaderCellDef class="example-header-cell"> Bar </cdk-header-cell>
<cdk-cell *cdkCellDef="let row" class="example-cell"> {{row.bar}} </cdk-cell>
</ng-container>
<ng-container cdkColumnDef="qux">
<cdk-header-cell *cdkHeaderCellDef class="example-header-cell"> Qux</cdk-header-cell>
<cdk-cell *cdkCellDef="let row" class="example-cell"> {{row.qux}} </cdk-cell>
</ng-container>
<ng-container cdkColumnDef="grault">
<cdk-header-cell *cdkHeaderCellDef class="example-header-cell">Grault</cdk-header-cell>
<cdk-cell *cdkCellDef="let row" class="example-cell">
{{row.grault}}
</cdk-cell>
</ng-container>
<cdk-header-row *cdkHeaderRowDef="displayedColumns" class="example-header-row"></cdk-header-row>
<cdk-row *cdkRowDef="let row; columns: displayedColumns; let index=index" class="example-row" (click)="expandRow(index)"
#cdkrow>
</cdk-row>
</cdk-table>
</div>
What do you suggest? If this cannot be solved with trackBy, what would you do instead?
Thanks!