2
votes

Topic:

md-table ui that implements the cdk-table in Angular Material 2

Problem:

unable to get connect to emit after a user-invoked http call returns a response

Approach:

create a hot observable out of a behavior subject in a service. parent component invokes a method in the service that feeds an array of objects into the behavior subject. the child component subscribes to the behavior subject's hot observable in it's constructor. the child component recreates the reference to the datasource in the subscription method with the newly received array of objects

Expected Behavior:

each time the behavior subject is fed new data via .next(), connect should fire

Observed Behavior:

the connect method fires only on initialization of the child component


Parent Component:

import { Component }                 from '@angular/core';

import { InboundMessagesService }       from '../messages/services/inbound/inbound.service';
import { Message }                      from '../messages/model/message';

@Component({
    selector: 'search-bar',
    templateUrl: './search-bar.component.html',
    styleUrls: [ './search-bar.component.css'],
    providers: [ InboundMessagesService ]
})

export class SearchBarComponent {
    hac: string = "";

    constructor( private inboundMessagesService: InboundMessagesService ) { }

    onSubmit( event: any ): void {
        this.hac = event.target.value;
        this.inboundMessagesService.submitHac( this.hac );
    }
}

Service:

import { Injectable }                 from '@angular/core';
import { Headers, 
         Http, 
         RequestMethod, 
         RequestOptions, 
         Response }                   from '@angular/http';
import { HttpErrorResponse }          from "@angular/common/http";
import { Observable }                 from 'rxjs/Rx';
import { Subject }                    from 'rxjs/Subject';
import { BehaviorSubject }            from 'rxjs/BehaviorSubject';
import { ReplaySubject }              from 'rxjs/ReplaySubject';
import { Subscription }               from 'rxjs/Subscription';
import "rxjs/add/operator/mergeMap";
import { Message }                    from '../../model/message'; 
import { LookupService }         from '../../../lookup/lookup.service';
@Injectable()
export class InboundMessagesService {
    dataChange: BehaviorSubject<Message[]> = new BehaviorSubject<Message[]>([]);
    dataChangeObservable = Observable.from( this.dataChange ).publish();
    messages: Message[];
    get data(): Message[] { 
        return this.dataChange.value; 
    }
    baseUrl: string = 'http://foobar/query?parameter=';
    headers = new Headers();
    options = new RequestOptions({ headers: this.headers });
    response: Observable<Response>;

    constructor( private http: Http, 
                 private lookupService: LookupService ) {
        console.log( "inboundService constructor - dataChange: ", this.dataChange );
        this.dataChangeObservable.connect()        
    }
    submitHac( hac: string ) {
        console.log( "submitHac received: ", hac );    

        this.getMessages( hac )
            .subscribe( ( messages: any ) => {
                this.dataChange.next( messages )
            }),
            ( err: HttpErrorResponse ) => {
                if ( err.error instanceof Error ) {
                    // A client-side or network error occurred. Handle it accordingly.
                    console.log( 'An error occurred:', err.error.message );
                } else {
                    // The backend returned an unsuccessful response code.
                    // The response body may contain clues as to what went wrong,
                    console.log( `Backend returned code ${ err.status }, body was: ${ err.error }` );
                    console.log( "full error: ", err );
                }
            };
    }
    getMessages( hac: string ) {
        console.log( "inboundService.getMessages( hac ) got: ", hac );
        return this.lookupService
            .getMailboxUuids( hac )
            .switchMap( 
                ( mailboxUuidsInResponse: Response ) => {
                    console.log( "lookup service returned: ", mailboxUuidsInResponse );
                    return this.http.get( this.baseUrl + mailboxUuidsInResponse.json(), this.options )
                })
            .map(
                ( messagesInResponse: any ) => {
                    console.log( "request returned these messages: ", messagesInResponse );
                    messagesInResponse.forEach( 
                        (message: any ) => {
                            this.messages.push( 
                                this.createMessage( message )
                    )});

                    return this.messages;
            })
    }
    createMessage( message: any ): Message {
        return new Message(
            message.name,
            message.type,
            message.contentType,
            message.state,
            message.source,
            message.target,
            message.additionalData
        )
    }
}

Child Component:

import { Component }                  from '@angular/core';
import { HttpErrorResponse }          from "@angular/common/http";
import { DataSource, CdkTable }       from '@angular/cdk';
import { Observable }                 from 'rxjs/Observable';

import { Message }                    from '../../../messages/model/message';
import { InboundMessagesService }     from '../../../messages/services/inbound/inbound.service';
import { SearchBarComponent }         from '../../../search_bar/search-bar.component';

@Component({
    selector: 'inbound-messages',
    templateUrl: './../inbound-messages.component.html',
    styleUrls: [ 
        'app/mailboxes/mailboxes-layout.css',
        './../inbound-messages.component.css'      
    ],
    providers: [ InboundMessagesService ]
})

export class InboundMessagesComponent {
    dataSource: InboundDataSource | null;
    displayedColumns = [ 'name', 'type', 'contentType', 'state', 'source', 'target', 'additionalData' ];

    constructor( private inboundMessagesService: InboundMessagesService ) { 
        console.log( "inbound component constructor (this): ", this );
        this.inboundMessagesService.dataChangeObservable.connect();
    } 

    ngOnInit() {
        console.log( "inbound component ngOnInit()" );
        this.dataSource = new InboundDataSource( this.inboundMessagesService );        
    }
}

export class InboundDataSource extends DataSource<Message> {
        constructor( private inboundMessagesService: InboundMessagesService ) {
            super();
            console.log( "InboundDataSource constructor" );
        }

        connect(): Observable<Message[]> {
            console.log( "CONNECT called" );
            return this.inboundMessagesService.dataChangeObservable
        }

        disconnect() {}
    }
1
I have to admit, there is a lot going on here that could be cleaned up. However, the place that seems most suspicious is the this.dataChange.next(messagesObservable.switch()). In your last flatMap in getMessages you should just return messages which will remove the need for switch(). Then you should do messagesObservable.subscribe(messages => {this.dataChange.next(messages);}); The next() method is supposed to take in an item, not a stream. In this case it should be receiving a Message[]. However, you are passing in Observable<Message[]>.Pace
I have same problem. Could you find a solution?Luiz Mitidiero
@Pace thanks for your help! I cleaned the code up a bit and refactored the returned object from get messages, but i'm not sure where you mean to subscribe to the hot observable. I've posted the updated codetyler2cr
@LuizMitidiero I think it has to do with using publish() and connect(), or maybe replay() and connect(). This thread gave me some understanding: stackoverflow.com/questions/40164752/…tyler2cr
@tyler2cr there is another bug in getMessages(). The first flatMap should be switchMap. And the second flatMap should just be map since it is returning new values, not a new Observable.Will Howell

1 Answers

0
votes

I've simplified some of the details specific to your application, but this shows how to render the table immediately with an empty data set, and then fetch new data from the service when onSubmit is called in SearchBarComponent.

Search component

@Component({ ... })
export class SearchBarComponent {

  constructor(private inboundMessagingService: InboundMessagingService) { }

  onSubmit(event): void {
    this.inboundMessagingService.submitHac(event.target.value);
  }
}

Service

@Injectable()
export class InboundMessagingService {

  messageDataSubject$ = new BehaviorSubject<Message[]>([]);

  get data$(): Observable<Message[]> {
    return this.messageDataSubject$.asObservable();
  }

  constructor(
    private http: Http,
    private addressBookService: AddressBookService
  ) { }

  submitHac(hac: string): void {
    this.getMessages(hac)
      .subscribe((messages: Message[]) => this.messageDataSubject$.next(messages));
  }

  getMessages(hac: string): Observable<Message[]> {
    return this.addressBookService
      .getMailboxUuids(hac)
      .switchMap(x => this.http.get(x))
      .map(messagesInResponse => messagesInResponse.map(m => this.createMessage(m)))
  }


}

Table component

@Component({ ... })
export class InboundMessagesComponent {

  dataSource: InboundDataSource | null;

  displayedColumns = [ ... ];

  constructor(private inboundMessagesService: InboundMessagesService) { }

  ngOnInit() {
    this.dataSource = new InboundDataSource(this.inboundMessagesService);
  }
}

export class InboundDataSource extends DataSource<Message> {

  constructor(private inboundMessagesService: InboundMessagesService) { }

  /**
  * This is only called once, when `dataSource` is provided to the md/cdk-table. To
  * update the table rows, you must make sure the source observable emits again.
  * The way it is setup, this will emit whenever `messageDataSubject$.next()` is called
  * in the service.
  */
  connect(): Observable<Message[]> {
    // Since messageDataSubject$ is a behavior subject, it will immediately emit an empty array
    // when subscribed to. This will show as an empty table.
    return this.inboundMessagesService.data$;
  }

  diconnect() { }
}

Other notes

  • Lots of people like to add $ to the end of observable variable names to distinguish them. I've used that convention here.
  • Since you are adding InboundMessagesService to each of your component providers, you will end up with multiple instances of the service. You should providing this service at the module level, and if you want to make sure this service exists only once in the lifetime of the app, add it to the root module providers.