DEV Community

Discussion on: Wrapping Angular Material button in custom Angular component (part 2)

 
dzhavat profile image
Dzhavat Ushev

Hi Benoît,
You're right! There's something strange going on. I'm testing it in Firefox and it works fine. No console log on disabled button. In Edge and Chrome however it doesn't work and can see the console log when the button is disabled but not when it's enabled. Which is even more strange :D
Will have to dig deeper :)

Thread Thread
 
benlune profile image
Benoît Plâtre • Edited

Hi Dzhavat,
I should have tell you with which browser I tested ;-) But yes, that's very strange you have a different behaviour with Firefox on such a basic case...
As we listen to the click on host element, it seems logic to me that the event is still emitted even if a child button is disabled. The workaround I found, not ideal, is to use pointer-events none or auto depending on disabled value. I tried to work with HostListener to preventDefault on click, which seems more clean to me, but it didn't work...

Thread Thread
 
benlune profile image
Benoît Plâtre

Hi Dzhavat,
I think a better way should to emit a custom event from the Button Host when user click on a real button. We won't have a kind of fake click event but a buttonClick event, emitted from the button. Getting this buttonClick could also remove confusion with the real click event.
I will follow this idea this week and let you know.
I'm also thinking about the way I will use a link tag, which can have a taste of button thanks to Angular Material, but is a link.

Thread Thread
 
dzhavat profile image
Dzhavat Ushev

I tried to recreate an isolated StackBlitz with the issue. Yes, Firefox doesn't emit the click event, whereas Chrome/Edge do (haven't tested in other browsers). I think your suggestion makes sense. Even though I'd have preferred to only use the click listener. Let me know about your findings when you try it :)

Thread Thread
 
benlune profile image
Benoît Plâtre

Hi Dzhavat,
I tested on Safari and the click event is emitted too, like Chrome/Edge.
Let's keep in touch about that !

Thread Thread
 
dzhavat profile image
Dzhavat Ushev

I got some suggestion for how to fix this on Twitter. Looks like using pointer-events: none when the button is disabled fixes the issue. Created a StackBlitz :)

Thread Thread
 
benlune profile image
Benoît Plâtre

Hi Dzhavat,
Yes that's what I did but I think it's not ideal. I think it's more safe to listen to the real button target click event.
I tried another approach, creating the Component manually and listening directly to the button instance. Then emitting a btnClick event with the event coming from the real button. My button wrapper is pointer-events:none, always, and only the child button is pointer-event:auto.
Here is the template :

<ng-template #buttonContainer></ng-template>
<div #contentWrapper>
    <ng-content></ng-content>
</div>
Enter fullscreen mode Exit fullscreen mode

Here is the buildComponent function :

#buildComponent() {
    if (this.buttonContainer && this.#variant && !this.componentRef) {
      this.componentRef = this.buttonContainer.createComponent<ButtonTemplate>(
        this.buttonComponentVariant,
        {
          injector: this.buttonComponentInjector,
          projectableNodes: [[this.contentWrapper.nativeElement]],
        }
      );
      this.componentRef.changeDetectorRef.detectChanges();
      if (this.componentRef.instance.target) {
        fromEvent(this.componentRef.instance.target.nativeElement, 'click')
          .pipe(untilDestroyed(this))
          .subscribe((event: any) => {
            this.btnClick.emit(event as PointerEvent);
          });
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

My buttons implements the ButtonTemplate interface

export interface ButtonTemplate {
  target: ElementRef<HTMLButtonElement>;
}
Enter fullscreen mode Exit fullscreen mode

I'm going on on with it, I'll let you know if it's fine. I plan also to manage not only <button> but <a> too, which may have the look of buttons.

Thread Thread
 
benlune profile image
Benoît Plâtre

Hi Dzhavat,
The way the component was created, with static providers, made the properties on the wrapper impossible to be bound on the final button component. Once the disabled property was set, it couldn't be updated.
I found a way to be able to update value from Wrapper to button instance.
I created a private validateDisabled function which update the button instance isDisables @Input (which is no more injected).

#validateDisabled(firstChange: boolean) {
    if (this.componentRef?.instance) {
      this.componentRef.instance.isDisabled = this.disabled;
      const changes = {
        isDisabled: new SimpleChange('', this.disabled, firstChange),
      };
      this.componentRef.instance.ngOnChanges(changes);
    }
  }
Enter fullscreen mode Exit fullscreen mode

Calling ngOnChange on the dynamically created component seems to be the only way to force the update of the component's properties.
The ButtonTemplate now has more properties

export interface ButtonTemplate {
  target: ElementRef<HTMLButtonElement>;
  isDisabled: boolean;
  ngOnChanges(changes: SimpleChanges): void;
}
Enter fullscreen mode Exit fullscreen mode

For example, here is the AccentButtonComponent

export class AccentButtonComponent implements ButtonTemplate, OnChanges {
  @Input()
  isDisabled = false;

  @ViewChild('target', { static: true, read: ElementRef })
  target!: ElementRef<HTMLButtonElement>;

  constructor(
    @Inject(typeAttributeToken) public type: BheButtonType,
    private cdr: ChangeDetectorRef
  ) {}

  ngOnChanges(changes: SimpleChanges): void {
    this.cdr.detectChanges();
  }
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
dzhavat profile image
Dzhavat Ushev

Hey Benoît,
Thanks for sharing your solution. Appreciate it!
I haven't looked at it in details yet but will do that in the coming days and share my thoughts :)