Search code examples
angularbuttonrxjsreactive

Custom Angular button component needs to fire event


TL;DR: I have created a button component in Angular and I want to utilise the best practice reactive behaviour in this button firing behaviour in whichever component decides to use it. I don't know what that best practice is.

I have created a component which renders a button and needs to invoke an action in the parent component. What I have currently is the parent component with this in the template:

<my-button (clicked)="doSomething()"></my-button>

The parent component code has this function being fired:

doSomething() {
    // ..stuff happens
}

The problem is that the myBtn component is currently listening to a click event on a button element like this:

<button type="button" (click)="doClick()"></button>

And this function is used to fire the event itself (so here is the full button component code):

import { Component, EventEmitter, Input, Output } from "@angular/core";

@Component({
  selector: "my-button",
  templateUrl: "./my-button.component.html",
  styleUrls: ["./my-button.component.css"]
})
export class MyButtonComponent {

  constructor() {}
  
  @Input() someCondition: boolean;

  @Output() clicked = new EventEmitter();

  doClick() {
    if (!this.someCondition) {
      this.clicked.emit(); // NOT REACTIVE???
    }
  }
}

I recognise that the doClick logic is not the best practice, but I'm not sure what the best practice is.


Solution

  • The best code I have come up with so far is this:

    import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from "@angular/core";
    import { combineLatest, fromEvent, Observable, Subject } from "rxjs";
    import { debounceTime, distinctUntilChanged, filter, map, takeUntil, withLatestFrom } from 'rxjs/operators';
    
    @Component({
      selector: "my-button",
      templateUrl: "./my-button.component.html",
      styleUrls: ["./my-button.component.css"]
    })
    export class MyButtonComponent {
      destroy$ = new Subject();
    
      constructor() {}
    
      @ViewChild("refreshBtn", { static: true }) clickerBtn: ElementRef;
      
      @Input() refreshState$: Observable<boolean>;
    
      @Input() label: string;
      @Input() refreshLabel: string;
    
      @Output() clicked = new EventEmitter();
    
      ngAfterViewInit() {
        fromEvent(this.clickerBtn.nativeElement, 'click')
          .pipe(takeUntil(this.destroy$), debounceTime(300), withLatestFrom(this.refreshState$))
          .subscribe(([_, refresh]) => !refresh && this.clicked.emit());
      }
    
      ngOnDestroy() {
        this.destroy$.next();
        this.destroy$.complete();
      }
    }
    

    This works and, so far, is the "most reactive" code I can come up with.