Ownaginatious Ownaginatious - 6 months ago 270
Javascript Question

How do I properly handle change detection on a component that relies on a ViewChild and data from an observable in Angular 2?

So presently, I have a component that fits into a larger dashboard for rendering a graph of a node's immediate parent and child relationships. This component is supposed to refresh its graph every time the

node_id
input is changed externally.

I've included a simplified version of my code.

@Component({
selector: 'relations',
template: `
<div [class]="'panel panel-' + (_loading ? 'default' : 'primary')">
<div class="panel-heading">Child relations</div>
<div class="panel-body">
<div class="loading" *ngIf="_loading" style="text-align: center">
<img src="./loading.gif" height="100px" width="100px" />
</div>
<div class="graph_container" [style.display]="_loading ? 'none': 'block'" #my_graph></div>
</div>
</div>
`
})
export class GraphComponent implements OnChanges {

@Input('node_id') node_id;
@ViewChild('my_graph') graphDiv;

private _loading: boolean = true;
private _current_node: Node;
private _parent: Node;
private _children: Node[];

constructor(
private _nodeService: NodeService
) {}

ngOnChanges(changes){
this.getRelations();
}

getRelations() {
this._loading = true;
Observable.combineLatest(
this._nodeService.getEvent(this.node_id),
this._nodeService.getChildren(this.node_id),
this._nodeService.getParent(this.node_id)
).subscribe(v => {
this._current_node = v[0];
this._children = v[1];
this._parent = v[2];
this._loading = false
this.renderGraph();
});
}

renderGraph() {
...
}
}


Now the issue I'm having is a race condition; the
renderGraph()
method relies on the
@ViewChild('my_graph') graphDiv
variable to know where it should drop the canvas element for rendering the graph. Because of this, when the observable resolves, it may try to call
renderGraph()
before the
@ViewChild
component has initialized.

I've tried playing with the lifecycle hooks by doing things such as:

ngAfterViewInit(){
if (!this._loading){
this.renderGraph();
}
}


That only helps if the observable finishes before the view is loaded, and causes no graph to be rendered should the view finish rendering first.

So my question is, how can I properly achieve what I want? That is to say, re-rendering the graph following the observable resolving in response to a change to
node_id
.

I'm very new at Angular 2 (and front end in general), and my intuition tells me I'm not using the observable in a way it's intended to be used, but I've had difficulty in finding any examples similar to what I want.

Any help/guidance/advice would be greatly appreciated.

Thanks!

Answer

I would use BehaviorSubject which is just a special type of Observable. Snippet from the docs:

It stores the latest value emitted to its consumers, and whenever a new Observer subscribes, it will immediately receive the "current value" from the BehaviorSubject.

The reason for preferring BehaviorSubject is because it always emits the last node_id value no matter when the subscription actually happens. In case it was set before viewInit. Also, because it will always have the latest value, we don't need to have node_id property on GraphComponent. We just need a setter for it that will emit the passed value to subscribers and automatically keep it saved on the subject, so every new subscriber will get the current value.

import {BehaviorSubject} from 'rxjs/BehaviorSubject';
...
export class GraphComponent implements OnChanges {
    @ViewChild('my_graph') graphDiv;
    ...
    private _nodeIdSubject = new BehaviorSubject(-1);

    constructor(...) {}

    @Input('node_id') 
    set node_id(id){  // this is the same as ngOnChanges but will only be triggered if node_id changed
        this._nodeIdSubject.next(id);
    }
    ngAfterViewInit(){  // subscribe to node_id changes after view init
        this._nodeIdSubject.subscribe(nodeId=> this.getRelations(nodeId));
    }

    getRelations(nodeId) {
        ...
    }

    renderGraph() {
        ...
    }
}

This is probably not be the best approach, but I like it because now you have a stream of node_id that you can manipulate freely.

Comments