Angular 2 Observable Input not updating UI after Subscribe

1.3k views Asked by At

I have a child component that is a search box. A user types in some input and presses a search button. This emits a 'search' event that the parent component then calls a search service with the search parameters and assigns the result to an Observable input property of the child component. In the setter of the child component input property I subscribe to the Observable and then map the results. My issue is that the UI will not refresh when the results are set until an event such as a mouse move or button click occurs. I think I need to call the subscribe using ngZone my question is why do I have to do this? In an alternate flavor of my search component the parent component is responsible for calling subscribe and then assigns the results to an input property of the child component and this works as expected. Would really appreciate some explanation as to what is different with these two approaches and the best way to resolve.

Below is my search box (child) component.

import {
    AfterViewInit, ChangeDetectorRef, ChangeDetectionStrategy, Component,
    ElementRef, EventEmitter, Input, OnChanges,
    Output, SimpleChanges, ViewChild }                                  from '@angular/core';
import { ControlValueAccessor }                                         from '@angular/forms';
import {
    ApplicationService, PromptActions, PromptType,
    SearchItem, SearchItemType, SearchType, SearchArgs,
    UserPromptRequest
} from '../../../core';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { PanelBarItemModel }                                            from '@progress/kendo-angular-layout';

@Component({
    moduleId: module.id,
    selector: 'search-box',
    templateUrl: 'search-box.component.html',
    styleUrls: ['search-box.component.css'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SearchBoxComponent implements ControlValueAccessor {

    constructor(private cd: ChangeDetectorRef, private applicationService: ApplicationService) { }

    @Input() searchText: string;

    @Output() search: EventEmitter<SearchArgs> = new EventEmitter();

    private _searchObservable: Observable<SearchItemType[]>;
    @Input() 
    set searchObservable(val: Observable<SearchItemType[]>) {
        this._searchObservable = val;
        if (this._searchObservable) {
            this.searchSubscription = this._searchObservable.subscribe(r => this.resp(r));
        }
    }

    private _selectedItem: SearchItem;
    @Input()
    set selectedItem(val: SearchItem) {
        this._selectedItem = val;
        this.propagateChange(this._selectedItem);
    }

    get selectedItem(): SearchItem {
        return this._selectedItem;
    }

    searchResultsModel: PanelBarItemModel[];

    private _searchResults: SearchItemType[];
    @Input()
    set searchResults(val: SearchItemType[]) {
        this._searchResults = val;
        this.setUpPanelBarModel();
    }
    get searchResults(): SearchItemType[] {
        return this._searchResults;
    }

    private setUpPanelBarModel() {
        this.isSearching = false;
        this.cd.markForCheck();
    }   

    onSearch() {
        this.isResultsPopupVisible = true;
        this.searchResults = undefined;
        let searchArgs = new SearchArgs(this.searchText, this.selectedSearchTypeValue);
        this.search.emit(searchArgs);
        this.isSearching = true;
        this.cd.markForCheck();
    }

    resp(r: any) {
        this.searchResults = r;
        this.cd.markForCheck();
    }

    writeValue(obj: any): void {
        this.selectedItem = obj;
    }

    private propagateChange = (_: any) => { };

    registerOnChange(fn: any): void {
        this.propagateChange = fn;
    }        

    registerOnTouched(fn: any): void { } // Don't need

}

Below is the Parent component code. If I swap round the commented and uncommented line in onSearch my results display instantly. Otherwise I need to trigger another Angular UI tick by moving the mouse, clicking or pressing a key. Many thanks for any help!

import { ChangeDetectorRef, ChangeDetectionStrategy, Component }    from '@angular/core';
import { Observable }                                               from 'rxjs/Observable';
import { SettingsEntityListComponentBase }                          from '../../component-bases';
import { ClientType }                                               from '../../models/ClientType';
import { ClientTypeService }                                        from '../../services/client-type.service'
import {
    ApplicationService, NotificationContext,
    Permission, UserService, WindowType, SearchArgs,
    SearchItem, SearchItemType, SearchTypeValues
}                                                                   from '../../../core';

@Component({
    moduleId: module.id,
    selector: 'client-type-list',
    templateUrl: 'client-type-list.component.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ClientTypeListComponent extends SettingsEntityListComponentBase<ClientType> {
    constructor(
        changeDetector: ChangeDetectorRef,
        appService: ApplicationService,
        clientTypeService: ClientTypeService,
        userService: UserService) {
        super(changeDetector, appService, clientTypeService, userService, Permission.SettingsClientTypes,
            Permission.GlobalClientTypes, WindowType.ClientTypeDetail, NotificationContext.ClientTypeChanged);
    }

    searchText: string = '4455';

    selectedSecurity: SearchItem;

    searchObservable: Observable<SearchItemType[]>;

    searchResults: SearchItemType[];

    onSearch(args: SearchArgs): void {
        this.searchObservable = this.appService.search(args.searchText, SearchTypeValues.Securities);
        //this.appService.search(args.searchText, SearchTypeValues.Securities).subscribe(r => this.resp(r));
    }
    resp(r: any) {
        this.searchResults = r;
        this.changeDetector.markForCheck();
    }
}
1

There are 1 answers

1
Sasxa On

By using ChangeDetectionStrategy.OnPush you're limited to change detection via @Input and @Output properties. Otherwise you have to notify Angular about the change manually.

Try connecting search results from the service to an @Input property you are using to display results (ClientTypeListComponent.searchResults ??). Also, I'm not sure how/if change detection works with getter/setter from SearchBoxComponent.searchResults; you can try rewriting it as normal @Input property.