Scenario overview


Often in the project input box input, to the background to initiate a request to obtain a list or data. This simple business scenario needs to be developed with the following considerations in mind:

  • Do some validation of user input
  • Control the frequency at which requests are sent
  • When the input length is empty, the page data is restored to the default state
  • Respond to the user’s keyboard actions [such as Enter for query, ESC for clearing]
  • Ensure that the returned data is queried against the last parameter entered

Code implementation


With the business requirements in mind, we use the RXJS operator to control the input box for the above functionality

The template view test.component.html looks like this:

<div class="l-widget-notice-alarmCode">
   <input 
        nz-input 
        placeholder="Input alarm Code" 
        #noticeInput
    />
</div>
Copy the code

Next, we use the @ViewChild property decorator to get the matching element from the template view and convert an event on that element into an Observable via fromEvent:

export class NoticeOverviewComponent implements OnInit, OnDestroy, AfterViewInit  {
    @ViewChild('noticeInput', {static: true}) noticeInput: ElementRef;
    
    Angular view queries are completed before the ngAfterViewInit hook function is called
    ngAfterViewInit() {
        this.bindNoticeInputEvent();
    }
    
    private bindNoticeInputEvent(): void {
        const noticeInputEvent$ = fromEvent(this.noticeInput.nativeElement, 'keyup'); }}Copy the code

Next, we manipulate the event stream through the Pipe Pipe operator:

export class NoticeOverviewComponent implements OnInit, OnDestroy, AfterViewInit  {
    @ViewChild('noticeInput', {static: true}) noticeInput: ElementRef; Angular view queries are completed before the ngAfterViewInit hook function is calledngAfterViewInit() {
        this.bindNoticeInputEvent();
    }
    
    private bindNoticeInputEvent(): void {
        const noticeInputEvent$ = fromEvent(
            this.noticeInput.nativeElement, 
            'keyup'); noticeInputEvent$.pipe( debounceTime(300), filter((event: KeyboardEvent) => ! (event.keyCode >= 37 && event.keyCode <= 40) ), pluck('target'.'value'), ).subscribe(this.loadData); } public loadData(value: string): void { // todo => fetch data ... }}Copy the code

In the above code, we use the debounceTime operator in the PIPE pipe to discard the emitted values that are less than the specified time between outputs to achieve the buffering process, and the filter operator to filter emitted values that meet the business requirements.

Finally, we get the user’s input value by using the pluck operator to get the emitting object’s nested property, the event.value property.

Since the Observable is lazy, we need to actively trigger the function to retrieve the value. See the Angular-Observable Overview for an introduction to Observables

Code optimization


Our request method also needs to unsubscribe to avoid memory leaks that might result from subscribing.

Since Observable is also a publish/subscribe push system, at some point we need to unsubscribe to free up system memory. Otherwise, the application may experience memory leaks.

Observable returns a subscription object that unsubscribes the current Observer by calling the subscription unsubscribe method. The standard pattern for unsubscribing can be used:

export class NoticeOverviewComponent implements OnInit, OnDestroy, AfterViewInit  {
    @ViewChild('noticeInput', {static: true}) noticeInput: ElementRef; noticeInputSubscription: Subscription; Angular view queries are completed before the ngAfterViewInit hook function is calledngAfterViewInit() { this.bindNoticeInputEvent(); } // Normally we unsubscribe when the component is destroyed.OnDestroy() {
        this.noticeInputSubscription.unsubscribe();
    }
    
    private bindNoticeInputEvent(): void {
        const noticeInputEvent$ = fromEvent(
            this.noticeInput.nativeElement, 
            'keyup'); this.noticeInputSubscription = noticeInputEvent$.pipe( debounceTime(300), filter((event: KeyboardEvent) => ! (event.keyCode >= 37 && event.keyCode <= 40) ), pluck('target'.'value'), ).subscribe(this.loadData); } public loadData(value: string): void { // todo => fetch data ... }}Copy the code

But this is cumbersome, and one Subscription corresponds to one subscribe.

An observable can automatically unsubscribe by using the takeUntil operator:

export class NoticeOverviewComponent implements OnInit, OnDestroy, AfterViewInit  {
    @ViewChild('noticeInput', {static: true}) noticeInput: ElementRef; Subject private unsubscribe: Subject<void> = new Subject<void>(); Angular view queries are completed before the ngAfterViewInit hook function is calledngAfterViewInit() { this.bindNoticeInputEvent(); } // Normally we unsubscribe when the component is destroyed.OnDestroy() {
        this.unsubscribe.next();
        this.unsubscribe.complete();
    }
    
    private bindNoticeInputEvent(): void {
        const noticeInputEvent$ = fromEvent(
            this.noticeInput.nativeElement, 
            'keyup'); noticeInputEvent$.pipe( takeUntil(this.unsubscribe), debounceTime(300), filter((event: KeyboardEvent) => ! (event.keyCode >= 37 && event.keyCode <= 40) ), pluck('target'.'value'),
        ).subscribe(this.loadData);
    }
    
    public loadData(value: string): void {
        // todo => fetch data
        ...
    }
Copy the code

TakeUntil accepts an Observable. When the received Observable sends a value, the source Observable automatically completes it. Using this mechanism, it can not only cancel a single subscription, but also use the same unsubscribe in the whole component: Subject

object to unsubscribe because we are using Subject, which is a multicast pattern

This mechanism is also the basis for the unsubscribe model used in Angular for component destruction

Warm prompt


Most of the time, we can set takeUntil at the top of Pipe to handle subscriptions, but in some higher-order streams, the observable that the subscriber subscribes to may be returned by another stream, and this process is also lazy. Therefore, if you set takeUntil at the top, you are likely to leak the content.

In other scenarios, takeUntil can also cause problems. You can ensure that takeUntil is placed correctly by configuring the rxjs-no-unsafe- takeUntil rule in rxjs-tslint-rules. In this case, it is sufficient that we set takeUntil at the top.

Thank you for reading ~