问题
I wish it was just plug and play :-) I've been thrashing around with this for hours with none of my little experiments working. The md data table is new, so there is almost no divine knowledge on the Web yet. Finding a good way to connect Firebase to the table would be a good start. Any ideas?
Currently I have this setup. My code works great without the table with the standard Angular setup and code, using ngFor and creating a list from a template. So the code delivers the data from Firebase with AngularFire 2. Trying out the new md data table is the problem.
First, the template won't render. I know I have NgModule setup correctly so my suspicion is that the data source isn't connecting and creating this error. This is the error in the Chrome console.
Template parse errors:
Can't bind to 'dataSource' since it isn't a known property of 'md-table'.
1. If 'md-table' is an Angular component and it has 'dataSource' input, then verify that it is part of this module.
My members-search.component.html looks identical to the official docs except I changed the template binding:
<md-table #table [dataSource]="dataSource">
<ng-container cdkColumnDef="memberName">
<md-header-cell *cdkHeaderCellDef> Name </md-header-cell>
<md-cell *cdkCellDef="let row"> {{member.firstName}} {{ member?.lastName }} </md-cell>
</ng-container>
members-search.component.ts has these relevant parts:
import { DataSource } from '@angular/cdk';
@Injectable()
export class MembersAdminService {
private members$: FirebaseListObservable<Member[]>;
private dataSource: DataSource<any>;
constructor(
private af: AngularFireDatabase,
@Inject(FirebaseApp) fb) {
this.members$ = af.list('Members');
}
And I dropped these data table functions into my working code in members-search.service.ts and used the same code in the connect() that I've been using elsewhere on this service.
// md table dataSource functions.
public connect(): FirebaseListObservable<any> {
return this.af.list('Members', {
query: {
orderByChild: 'lastName'
}
});
// return this._exampleDatabase.dataChange;
}
public disconnect() {}
The data table docs and plunker create a data source and database in the component but it seems that most of that isn't necessary if I already have Firebase. I'm learning all this so I'm far from an expert at anything and maybe I'm missing something.
If you haven't got into this new setup before then here are the docs. The md table is built on top of the cdk table to give the cdk table the styling.
https://material.angular.io/components/table/overview
https://material.angular.io/guide/cdk-table
回答1:
I've added the code for connecting to Firebase when using the MD Paginator for the MD Data Table. The Paginator makes the code in the service more complicated. Most of the code is in the service where it belongs. Enjoy!
member-admin.service.ts
import { AngularFireDatabase, FirebaseListObservable } from 'angularfire2/database';
import { FirebaseApp } from 'angularfire2';
import { Inject, Injectable } from '@angular/core';
import { MemberModel } from './member-admin.model';
import { SuccessService } from '../../../shared/success.service';
// Data Table imports.
import { MdPaginator } from '@angular/material';
import { DataSource } from '@angular/cdk';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import 'rxjs/add/operator/startWith';
import 'rxjs/add/observable/merge';
import 'rxjs/add/observable/combineLatest';
import 'rxjs/add/operator/map';
@Injectable()
export class MembersAdminService {
private membersData$: FirebaseListObservable<MemberModel[]>;
constructor(
public af: AngularFireDatabase,
private successService: SuccessService,
// For Create and Update functions.
@Inject(FirebaseApp) fb) {
this.membersData$ = af.list('Members');
}
// ... CRUD stuff not relevant to the MD Table ...
// *** MD DATA TABLE SERVICES. ***
@Injectable()
export class MemberDatabase {
/* Stream that emits whenever the data has been modified. */
public dataChange: BehaviorSubject<MemberModel[]> = new BehaviorSubject<MemberModel[]>([]);
get data(): MemberModel[] {
return this.dataChange.value; }
// Connection to remote db.
private database = this.memberAdminService.af.list('Members', {
query: {
orderByChild: 'lastName'
}
});
public getMembers(): FirebaseListObservable<MemberModel[]> {
return this.database;
}
constructor(private memberAdminService: MembersAdminService) {
this.getMembers()
.subscribe(data => this.dataChange.next(data));
}
}
@Injectable()
export class MembersAdminSource extends DataSource<MemberModel> {
constructor(
private memberDatabase: MemberDatabase,
private paginator: MdPaginator) {
super();
}
/** Connect function called by the table to retrieve one stream containing the data to render. */
connect(): Observable<MemberModel[]> {
const displayDataChanges = [
this.memberDatabase.dataChange,
this.paginator.page,
];
return Observable
.merge(...displayDataChanges) // Convert object to array with spread syntax.
.map(() => {
const dataSlice = this.memberDatabase.data.slice(); // Data removed from viewed page.
// Get the page's slice per pageSize setting.
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
const dataLength = this.paginator.length; // This is for the counter on the DOM.
return dataSlice.splice(startIndex, this.paginator.pageSize);
});
}
disconnect() {}
}
all-members.component.ts
Did some refactoring in ngOnInit
and the class properties.
import { Component, OnInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs/Subject';
// For MD Data Table.
import { MdPaginator } from '@angular/material';
import { MembersAdminService, MembersAdminSource, MemberDatabase } from './member-admin.service';
import { ConfirmService } from '../../../shared/confirm.service';
import { MemberModel } from './member-admin.model';
@Component({
selector: 'app-all-members',
templateUrl: './all-members.component.html'
})
export class AllMembersComponent implements OnInit {
membersData: MemberModel[];
private result: boolean;
allMembers: MemberModel[];
// For search
startAt = new Subject();
endAt = new Subject();
lastKeypress: 0;
// For MD data table.
// private memberDatabase = new MemberDatabase(); // Requires a param but? Moved to constructor.
private dataSource: MembersAdminSource | null;
private displayedColumns = [
'firstName',
'lastName',
'mainSkillTitle',
'mainSkills',
'delete',
'key'
];
@ViewChild(MdPaginator)
paginator: MdPaginator;
public dataLength: any; // For member counter on DOM.
constructor(
private membersAdminService: MembersAdminService,
private memberDatabase: MemberDatabase,
private router: Router,
private confirmService: ConfirmService
) {}
ngOnInit() {
this.memberDatabase.getMembers()
.subscribe(members => {
this.dataSource = new MembersAdminSource(this.memberDatabase, this.paginator);
this.dataLength = members;
});
}
all-members.component.html
Notice I have buttons in the rows for delete and edit and they work fine. The trick is you need the database key in a hidden column.
<md-table #table [dataSource]="dataSource">
<!-- First Name Column -->
<ng-container cdkColumnDef="firstName">
<md-header-cell *cdkHeaderCellDef> First Name </md-header-cell>
<md-cell *cdkCellDef="let row"> {{row.firstName}} </md-cell>
</ng-container>
<!-- Las Name Column -->
<ng-container cdkColumnDef="lastName">
<md-header-cell *cdkHeaderCellDef> Last Name </md-header-cell>
<md-cell *cdkCellDef="let row"> {{row.lastName}} </md-cell>
</ng-container>
<!-- Title Column -->
<ng-container cdkColumnDef="mainSkillTitle">
<md-header-cell *cdkHeaderCellDef> Title </md-header-cell>
<md-cell *cdkCellDef="let row"> {{row.mainSkillTitle}} </md-cell>
</ng-container>
<!-- Main Skills Column -->
<ng-container cdkColumnDef="mainSkills">
<md-header-cell *cdkHeaderCellDef> Main Skills </md-header-cell>
<md-cell *cdkCellDef="let row"> {{row.mainSkills}} </md-cell>
</ng-container>
<!-- Delete Buttons Column -->
<ng-container cdkColumnDef="delete">
<md-header-cell *cdkHeaderCellDef> Delete / Edit </md-header-cell>
<md-cell *cdkCellDef="let row">
<button (click)="deleteMember(row.$key)">Delete</button>
<button (click)="goToDetailPage(row.$key)">Edit</button>
</md-cell>
</ng-container>
<!-- Database key Column -->
<ng-container cdkColumnDef="key">
<md-header-cell *cdkHeaderCellDef class="hiddenField"> Key </md-header-cell>
<md-cell *cdkCellDef="let row" class="hiddenField"> {{row.$key}} </md-cell>
</ng-container>
<md-header-row *cdkHeaderRowDef="displayedColumns"></md-header-row>
<md-row *cdkRowDef="let row; columns: displayedColumns;"></md-row>
</md-table>
<md-paginator #paginator
[length]="dataLength?.length"
[pageIndex]="0"
[pageSize]="5"
[pageSizeOptions]="[5, 10, 25, 100]">
</md-paginator>
回答2:
The following solution works. It took a while to figure out how to solve this question and I had skilled assistance from Will Howell on the Reddit Angular group. My service is more involved with CRUD stuff but those aren't baked yet. I'm setting this up for master-detail with buttons to show delete and edit. The final column brings the Firebase $key
onto the DOM, which I'll capture and use to access the CRUD functions in the component and service. When I get this figured out I'll post the total mess, err, code into another Stack Overflow post with a more specific title.
member-admin.service.ts
The service includes three classes if setup like the AM2 Data Table docs as I've done. I am not sure I like this but will follow the docs for now.
import { AngularFireDatabase, FirebaseListObservable } from 'angularfire2/database';
import { FirebaseApp } from 'angularfire2';
import { Inject, Injectable } from '@angular/core';
import { Member } from './member-admin.model';
import { SuccessService } from '../../../shared/success.service';
import { DataSource } from '@angular/cdk';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
@Injectable()
export class MembersAdminService {
private members$: FirebaseListObservable<Member[]>;
constructor(
private af: AngularFireDatabase,
private successService: SuccessService,
@Inject(FirebaseApp) fb) {
this.members$ = af.list('Members');
}
// CRUD stuff here in this class...
// *** MD DATA TABLE SERVICES. ***
@Injectable()
export class MemberDatabase {
/* Stream that emits whenever the data has been modified. */
public dataChange: BehaviorSubject<MemberModel[]> = new BehaviorSubject<MemberModel[]>([]);
get data(): MemberModel[] {
return this.dataChange.value; }
// Connection to remote db.
private database = this.memberAdminService.af.list('Members', {
query: {
orderByChild: 'lastName'
}
});
public getMembers(): FirebaseListObservable<MemberModel[]> {
return this.database;
}
constructor(private memberAdminService: MembersAdminService) {
this.getMembers()
.subscribe(data => this.dataChange.next(data));
}
}
export class MembersAdminSource extends DataSource<Member> {
constructor(private members: Member[]) {
super();
}
/** Connect function called by the table to retrieve one stream containing the data to render. */
connect(): Observable<Member[]> {
return Observable.of(this.members);
}
disconnect() {}
}
all-members.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { MembersAdminService } from './member-admin.service';
import { MembersAdminSource } from './member-admin.service';
import { ConfirmService } from '../../../shared/confirm.service';
import { Member } from './member-admin.model';
@Component({
selector: 'app-all-members',
templateUrl: './all-members.component.html'
})
export class AllMembersComponent implements OnInit {
members: Member[];
private selectedId: number;
private result: boolean;
allMembers: Member[];
// For MD data table.
// private dataSource: DataSource<any>;
private dataSource: MembersAdminSource | null;
private displayedColumns = [
'firstName',
'lastName',
'mainSkillTitle',
'mainSkills',
'delete',
'edit',
'key'
];
constructor(
private membersAdminService: MembersAdminService,
private router: Router,
private confirmService: ConfirmService
) {}
ngOnInit() {
// This was the code for an *ngFor setup before installing the data table.
/* this.membersAdminService.getMembers()
.subscribe(
members => this.allMembers = this.members = members
); */
this.membersAdminService.getMembers()
.subscribe(members => {
this.members = members;
this.dataSource = new MembersAdminSource(members);
});
}
all-members.component.html
<md-table #table [dataSource]="dataSource">
<!-- First Name Column -->
<ng-container cdkColumnDef="firstName">
<md-header-cell *cdkHeaderCellDef> Name </md-header-cell>
<md-cell *cdkCellDef="let row"> {{row.firstName}} </md-cell>
</ng-container>
<!-- Las Name Column -->
<ng-container cdkColumnDef="lastName">
<md-header-cell *cdkHeaderCellDef> Name </md-header-cell>
<md-cell *cdkCellDef="let row"> {{row.lastName}} </md-cell>
</ng-container>
<!-- Title Column -->
<ng-container cdkColumnDef="mainSkillTitle">
<md-header-cell *cdkHeaderCellDef> Title </md-header-cell>
<md-cell *cdkCellDef="let row"> {{row.mainSkillTitle}} </md-cell>
</ng-container>
<!-- Main Skills Column -->
<ng-container cdkColumnDef="mainSkills">
<md-header-cell *cdkHeaderCellDef> Main Skills </md-header-cell>
<md-cell *cdkCellDef="let row"> {{row.mainSkills}} </md-cell>
</ng-container>
<!-- Delete Buttons Column -->
<ng-container cdkColumnDef="delete">
<md-header-cell *cdkHeaderCellDef> Delete </md-header-cell>
<md-cell *cdkCellDef="let row"> <button (click)="deleteMember(member)">Delete</button> </md-cell>
</ng-container>
<!-- Edit button Column -->
<ng-container cdkColumnDef="edit">
<md-header-cell *cdkHeaderCellDef> Edit </md-header-cell>
<md-cell *cdkCellDef="let row"> <button class="badge"
(click)="goToDetailPage(member)">Edit</button> </md-cell>
</ng-container>
<!-- key Column -->
<ng-container cdkColumnDef="key">
<md-header-cell *cdkHeaderCellDef class="hiddenField"> Key </md-header-cell>
<md-cell *cdkCellDef="let row" class="hiddenField"> {{row.$key}} </md-cell>
</ng-container>
<md-header-row *cdkHeaderRowDef="displayedColumns"></md-header-row>
<md-row *cdkRowDef="let row; columns: displayedColumns;"></md-row>
</md-table>
回答3:
Thought I'd add my approach for anyone looking for this solution.
I've tried to make this reusable across collections. It supports retrieving data and sorting up and down by specified fields. Don't forget to add the sort fields to .indexOn
in the firebase rules.
I haven't managed to get paging working because it's just too hard to work out the startKey!
firebase-datasource.ts
Started by defining a template datasource that I can reuse for all collections that require this.
import { Component } from '@angular/core';
import { DataSource } from '@angular/cdk/collections';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { AngularFireDatabase } from 'angularfire2/database';
/**
* Sortable Interface - Used for specifying the sort order.
*/
export interface Sort {
field: string;
direction: '' | 'asc' | 'desc';
}
/**
* FirebaseDataSource is a templated datasource for Firebase. At this stage it
* allows:
* * Tracking data updates to the underlying datarecords.
* * Sorting ascending and descending
*
* We have not implemented paging controls as its too difficult with NoSQL. It also
* does not support multi-field sorting.
*/
export class FirebaseDataSource<T> extends DataSource<T> {
/**
* The datachange subscriber emits new data when the Firebase records are updated.
*/
dataChange: BehaviorSubject<T[]> = new BehaviorSubject<T[]>([]);
/**
* The sort change is updated when the sort order is changed.
*/
sortChange: BehaviorSubject<Sort> = new BehaviorSubject<Sort>({field: '', direction: ''});
/**
* Path tracks the path of the list of records. e.g. /items
*/
path: string;
/**
* Keep for cleaning up the subscription
*/
private _sub: Subscription;
/**
* Getters and setters for setting sort order.
*/
get sort(): Sort {
return this.sortChange.value;
}
set sort(sort: Sort) {
this.sortChange.next(sort);
}
/**
* Construct an instance of the datasource.
*
* @param path The Firebase Path e.g. /items
* @param db Injectable AngularFireDatabase
* @param sort Optional initial sort order for the list.
*/
constructor(
path: string,
protected db: AngularFireDatabase,
sort?: Sort) {
super();
this.path = path;
/**
* Sets up a subscriber to the path and emits data change events.
*/
this._sub = this.db.list(this.path).valueChanges<T>()
.subscribe((data) => {
this.dataChange.next(data);
});
if (sort) {
this.sort = sort;
}
}
/**
* Connect to the data source, retrieve initial data, and observe changes.
* It tracks changes to either the underlying data, or to the sort order and remaps
* the query.
*
* @returns Observable<T[]>
*/
connect(): Observable<T[]> {
const dataChanges = [
this.dataChange,
this.sortChange
];
const _that = this;
return Observable.merge(...dataChanges)
.switchMap(() => {
if (_that.sort.field !== '' && _that.sort.direction !== '') {
return this.db.list(this.path, ref => ref.orderByChild(this.sort.field)).valueChanges<T>()
.map((data: T[]) => {
if (_that.sort.direction === 'desc') {
return data.reverse();
} else {
return data;
}
});
} else {
return this.db.list(this.path).valueChanges<T>();
}
});
}
/**
* Cleans up the open subscription.
*/
disconnect() {
this._sub.unsubscribe();
}
}
Then an example usage:
roles-datasource.ts Declare this in the 'providers' of the relevant module. (code not shown)
import { FirebaseDataSource } from '../../shared/firebase-datasource';
import { Role } from './role';
import { Injectable } from '@angular/core';
import { AngularFireDatabase } from 'angularfire2/database';
@Injectable()
export class RoleDataSource extends FirebaseDataSource<Role> {
constructor(
protected db: AngularFireDatabase
) {
super('/roles', db);
}
}
Now let's look at the UI component:
all-roles.component.html
Ignoring the extraneous code for toolbars etc. The important parts to notice are the mat-table
and matSort
directives.
<!-- Toolbar -->
<div class="plr20 mb10 bb-light">
<div fxLayout="row" fxLayoutAlign="space-between center">
<h1>All Roles</h1>
<div>
<a mat-button [routerLink]="['/roles', 'new']">
<mat-icon class="cursor-pointer">add</mat-icon>New Role</a>
</div>
</div>
</div>
<! -- End Toolbar -->
<div class="plr20" fxLayout="column">
<div *ngIf="contentLoading" fxLayout="row" fxLayoutAlign="center">
<div class="spinner-container">
<mat-spinner diameter="48" strokeWidth="4"></mat-spinner>
</div>
</div>
<mat-card class="mb20">
<mat-card-content>
<mat-table #table [dataSource]="dataSource" matSort>
<!--- Note that these columns can be defined in any order.
The actual rendered columns are set as a property on the row definition" -->
<!-- Identifier Column -->
<ng-container matColumnDef="identifier">
<mat-header-cell *matHeaderCellDef mat-sort-header>Identifier</mat-header-cell>
<mat-cell *matCellDef="let role"> {{role.identifier}} </mat-cell>
</ng-container>
<!-- Title Column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header> Title </mat-header-cell>
<mat-cell *matCellDef="let role"> {{role.title}} </mat-cell>
</ng-container>
<!-- Last Updated -->
<ng-container matColumnDef="lastUpdated">
<mat-header-cell *matHeaderCellDef mat-sort-header> Last Updated </mat-header-cell>
<mat-cell *matCellDef="let role"> {{role.lastUpdated | date}} {{role.lastUpdated | date: 'mediumTime'}} </mat-cell>
</ng-container>
<!-- Actions -->
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef> Actions </mat-header-cell>
<mat-cell *matCellDef="let role"> <a mat-button [routerLink]="['/roles/', role.identifier]">View</a> </mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
</mat-card-content>
</mat-card>
</div>
all-roles.component.ts
Finally the implementation at UI Layer. As a matter of taste it captures the MatSort
updates and emits them to the data source because I don't like binding MatSort directly into the datasource layer. I've also added a simple Ajax Loader while the data is loading.
import { Component, OnDestroy, ViewChild, OnInit } from '@angular/core';
import { AngularFireDatabase, AngularFireList } from 'angularfire2/database';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { Role } from './role';
import { RoleDataSource } from './role-datasource';
import { MatSort } from '@angular/material';
@Component({
templateUrl: './all-roles.component.html',
styles: [':host {width: 100% }']
})
export class AllRolesComponent implements OnDestroy, OnInit {
roles: Observable<any>;
contentLoading: boolean;
subs: Subscription[] = [];
displayedColumns = ['identifier', 'title', 'lastUpdated', 'actions'];
@ViewChild(MatSort) sort: MatSort;
constructor(private db: AngularFireDatabase, private dataSource: RoleDataSource) {
this.contentLoading = true;
}
ngOnInit() {
const _that = this;
// simply handles hiding the AJAX Loader
this.dataSource.connect().take(1).subscribe(data => {
this.contentLoading = false;
});
this.subs.push(this.sort.sortChange.subscribe(() => {
_that.dataSource.sort = {
field: _that.sort.active,
direction: _that.sort.direction
};
}));
}
ngOnDestroy() {
this.subs.forEach((sub) => {
sub.unsubscribe();
});
}
}
来源:https://stackoverflow.com/questions/45336706/angular-material-2-data-table-connect-to-angularfire2-or-firebase-service