angularkeyboard-shortcutsangular-directiveangular2-directives

Angular host listener property in component decorator with logic


I have a SidebarComponent:

import { Component } from '@angular/core';
import { MenuComponent } from '../shared/menu/menu.component';

@Component({
  selector: 'app-sidebar',
  standalone: true,
  imports: [MenuComponent],
  templateUrl: './sidebar.component.html',
  styleUrl: './sidebar.component.css',
  host: {
    '(document:keydown.n)': 'toggleMenu($event)',
  },
})
export class SidebarComponent {
  isMenuOpen = false;

  toggleMenu(event: Event) {
    this.isMenuOpen = !this.isMenuOpen;
    event.stopPropagation();
  }

  onOutsideClick() {
    this.isMenuOpen = false;
  }
}

I am listening for a "n" keypress to toggle the menu:

 host: {
    '(document:keydown.n)': 'toggleMenu($event)',
  },

In the new Angular documentation, it is recommended to use the host property on the @Component decorator instead of the @HostListener() decorator.

That said, how to add complex logic inside of the string in the host property object? For example, I am listening for the key "n" to toggle the menu. However, it is triggering when typing the letter "n" in an unrelated input field. How to stop this behavior?

Also, there is no syntax highlighting in the host property obviously because it is a string in an object. Not sure if this is intentional, but it makes for a bad developer experience when referring to fields and methods in the component class. But this is a side issue.


Solution

  • We can use e.stopPropagation() to ensure that the event is not propagated to the document level when it's coming from an input field, as per your requirements:

    import { Component } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import 'zone.js';
    import { MenuComponent } from './app/menu/menu.component';
    import { ClickOutsideDirective } from './app/click-outside.directive';
    
    @Component({
      selector: 'app-root',
      imports: [MenuComponent, ClickOutsideDirective],
      standalone: true,
      host: {
        '(document:keydown)': 'toggleMenuKeyboard($event)',
      },
      template: `
      <div class="container">
        <div class="menu-container">
          <button (click)="toggleMenu()" class="ignore-click">Toggle menu</button>
          @if(isMenuOpen) {
          <app-menu appClickOutside (clickedOutside)="isMenuOpen=false;"/>
        }
        </div>
        <input (keydown)="inputChange($event)"/>
    </div>
      `,
      styles: `
        .menu-container {
          position: relative
        }
    
        .container {
          display: flex;
          justify-content: center;
          align-items: center;
          margin-top: 100px;
        }
      `,
    })
    export class App {
      isMenuOpen = false;
    
      toggleMenu() {
        this.isMenuOpen = !this.isMenuOpen;
      }
    
      toggleMenuKeyboard(e: any) {
        if (e.key === 'n') {
          this.isMenuOpen = !this.isMenuOpen;
        }
      }
    
      inputChange(e: Event) {
        e.stopPropagation();
      }
    }
    
    bootstrapApplication(App);
    

    stackblitz


    It's very unconventional that we use a letter to perform a shortcut, it's always supported by another character like ctrl, shift or alt (to eliminate the problem you have described) so please do that check and trigger the method as shown below!

    import { Component } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import 'zone.js';
    import { MenuComponent } from './app/menu/menu.component';
    import { ClickOutsideDirective } from './app/click-outside.directive';
    
    @Component({
      selector: 'app-root',
      imports: [MenuComponent, ClickOutsideDirective],
      standalone: true,
      host: {
        '(document:keydown)': 'toggleMenuKeyboard($event)',
      },
      template: `
      <div class="container">
        <div class="menu-container">
          <button (click)="toggleMenu()" class="ignore-click">Toggle menu</button>
          @if(isMenuOpen) {
          <app-menu appClickOutside (clickedOutside)="isMenuOpen=false;"/>
        }
        </div>
    </div>
      `,
      styles: `
        .menu-container {
          position: relative
        }
    
        .container {
          display: flex;
          justify-content: center;
          align-items: center;
          margin-top: 100px;
        }
      `,
    })
    export class App {
      isMenuOpen = false;
    
      toggleMenu() {
        this.isMenuOpen = !this.isMenuOpen;
      }
    
      toggleMenuKeyboard(e: any) {
        if(e.key === 'n' && e.altKey) {
          this.isMenuOpen = !this.isMenuOpen;
        }
      }
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo