Ausnahme gefangen: SSL certificate problem: certificate is not yet valid 📌 Create a drum kit using RxJS and Angular standalone components

🏠 Team IT Security News

TSecurity.de ist eine Online-Plattform, die sich auf die Bereitstellung von Informationen,alle 15 Minuten neuste Nachrichten, Bildungsressourcen und Dienstleistungen rund um das Thema IT-Sicherheit spezialisiert hat.
Ob es sich um aktuelle Nachrichten, Fachartikel, Blogbeiträge, Webinare, Tutorials, oder Tipps & Tricks handelt, TSecurity.de bietet seinen Nutzern einen umfassenden Überblick über die wichtigsten Aspekte der IT-Sicherheit in einer sich ständig verändernden digitalen Welt.

16.12.2023 - TIP: Wer den Cookie Consent Banner akzeptiert, kann z.B. von Englisch nach Deutsch übersetzen, erst Englisch auswählen dann wieder Deutsch!

Google Android Playstore Download Button für Team IT Security



📚 Create a drum kit using RxJS and Angular standalone components


💡 Newskategorie: Programmierung
🔗 Quelle: dev.to

Introduction

This is day 1 of Wes Bos's JavaScript 30 challenge where I create a drum kit to play sounds when keys are pressed. In the tutorial, I created the components using RxJS, Angular standalone components and removed the NgModules.

In this blog post, I describe how the drum component (parent) uses RxJS fromEvent to listen to keydown event, discard unwanted keys and play sound when "A", "S", "D", "F", "G", "H", "J", "K" or "L" is hit. When the correct key is pressed, the parent updates the subject that drum kit components (children) subscribe to. Then, the child with the matching key plays the corresponding sound to make a tune.

Create a new Angular project

ng generate application day1-javascript-drum-kit

Bootstrap AppComponent

First, I convert AppComponent into standalone component such that I can bootstrap AppComponent and inject providers in main.ts.

// app.component.ts

import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { DrumComponent } from './drum';

@Component({
  selector: 'app-root',
  imports: [
    DrumComponent,
  ],
  template: '<app-drum></app-drum>',
  styles: [`
    :host {
      display: block;
      height: 100vh;
    }
  `],
  standalone: true,
})
export class AppComponent {
  title = 'RxJS Drum Kit';

  constructor(private titleService: Title) {
    this.titleService.setTitle(this.title);
  }
}

In Component decorator, I put standalone: true to convert AppComponent into a standalone component.

Instead of importing DrumComponent in AppModule, I import DrumComponent (that is also a standalone component) in the imports array because the inline template references it.

// main.ts

import { enableProdMode, inject } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
import { AppComponent } from './app/app.component';
import { browserWindowProvider, windowProvider } from './app/core/services';
import { environment } from './environments/environment';

bootstrapApplication(AppComponent, {
  providers: [
    {
      provide: APP_BASE_HREF,
      useFactory: () => inject(PlatformLocation).getBaseHrefFromDOM(),
    },
    browserWindowProvider,
    windowProvider,
  ]
})
  .catch(err => console.error(err));

browserWindowProvider and windowProvider are providers from core folder and I will show the source codes later.

Second, I delete AppModule because it is not used anymore.

Add window service to listen to keydown event

In order to detect key down on native Window, I write a window service to inject to ScrollComponent to listen to keydown event. The sample code is from Brian Love's blog post here.

// core/services/window.service.ts

import { isPlatformBrowser } from "@angular/common";
import { ClassProvider, FactoryProvider, InjectionToken, PLATFORM_ID } from '@angular/core';

/* Create a new injection token for injecting the window into a component. */
export const WINDOW = new InjectionToken('WindowToken');

/* Define abstract class for obtaining reference to the global window object. */
export abstract class WindowRef {
  get nativeWindow(): Window | Object {
    throw new Error('Not implemented.');
  }
}

/* Define class that implements the abstract class and returns the native window object. */
export class BrowserWindowRef extends WindowRef {
  constructor() {
    super();
  }

  override get nativeWindow(): Object | Window {
    return window;    
  }
}

/* Create an factory function that returns the native window object. */
function windowFactory(browserWindowRef: BrowserWindowRef, platformId: Object): Window | Object {
  if (isPlatformBrowser(platformId)) {
    return browserWindowRef.nativeWindow;
  }
  return new Object();
}

/* Create a injectable provider for the WindowRef token that uses the BrowserWindowRef class. */
export const browserWindowProvider: ClassProvider = {
  provide: WindowRef,
  useClass: BrowserWindowRef
};

/* Create an injectable provider that uses the windowFactory function for returning the native window object. */
export const windowProvider: FactoryProvider = {
  provide: WINDOW,
  useFactory: windowFactory,
  deps: [ WindowRef, PLATFORM_ID ]
};

I export browserWindowProvider and windowProvider to inject both providers in main.ts.

Declare Drum and DrumKey components

I declare standalone components, DrumComponent and DrumKeyComponent, to create a drum kit. To verify they are standalone, standalone: true is specified in the Component decorator.

src/app
├── app.component.ts
├── core
│   └── services
│       ├── index.ts
│       └── window.service.ts
├── drum
│   ├── drum.component.ts
│   └── index.ts
├── drum-key
│   ├── drum-key.component.ts
│   └── index.ts
├── helpers
│   ├── get-asset-path.ts
│   ├── get-host-native-element.ts
│   └── index.ts
├── interfaces
│   ├── index.ts
│   └── key.interface.ts
└── services
    ├── drum.service.ts
    └── index.ts
// get-asset-path.ts

import { APP_BASE_HREF } from '@angular/common';
import { inject } from '@angular/core';

export const getFullAssetPath = () => {
    const baseHref = inject(APP_BASE_HREF);
    const isEndWithSlash = baseHref.endsWith('/');
    return `${baseHref}${isEndWithSlash ? '' : '/'}assets/`;
}
// get-host-native-element.ts

import { ElementRef, inject } from '@angular/core';

export const getHostNativeElement = 
() => inject<ElementRef<HTMLElement>>(ElementRef<HTMLElement>).nativeElement;

getFullAssetPath and getHostNativeElement are helper functions that inject application base href and host native element in the construction phase of the components.
// drum.component.ts

import { NgFor } from '@angular/common';
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, inject } from '@angular/core';
import { filter, fromEvent, map } from 'rxjs';
import { WINDOW } from '../core/services';
import { DrumKeyComponent } from '../drum-key/drum-key.component';
import { getFullAssetPath, getHostNativeElement } from '../helpers';
import { DrumService } from '../services';

const getImageUrl = () => { 
  const imageUrl = `${getFullAssetPath()}images/background.jpg`;
  return `url('${imageUrl}')`;
}

const getEntryStore = () => { 
  const getEntryStore = inject(DrumService); 
  return getEntryStore.getEntryStore();
};

const windowKeydownSubscription = () => {
  const drumService = inject(DrumService);
  const allowedKeys = getEntryStore().allowedKeys;
  return fromEvent(inject<Window>(WINDOW), 'keydown')
    .pipe(
      filter(evt => evt instanceof KeyboardEvent),
      map(evt => evt as KeyboardEvent),
      map(({ key }) => key.toUpperCase()),
      filter(key => allowedKeys.includes(key)),
    ).subscribe((key) => drumService.playSound(key));
}

@Component({
  imports: [
    NgFor,
    DrumKeyComponent,
  ],
  standalone: true,
  selector: 'app-drum',
  template: `
    <div class="keys">
      <app-drum-key *ngFor="let entry of entries" [entry]="entry" class="key"></app-drum-key>
    </div>
  `,
  styles: [`...omitted due to brevity...`],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DrumComponent implements OnInit, OnDestroy {
  entries = getEntryStore().entries;
  hostElement = getHostNativeElement();
  imageUrl = getImageUrl();
  subscription = windowKeydownSubscription();

  ngOnInit(): void {
    this.hostElement.style.backgroundImage = this.imageUrl;
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}

DrumComponent imports DrumKeyComponent and NgFor to render different drum keys. NgFor is required because inline template uses ng-for directive to create a drum kit. windowKeydownSubscription uses RxJS to create an Observable to observe keydown event and subscribe the Observable to return an instance of Subscription.

// drum-key.component.ts

import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild, inject } from '@angular/core';
import { filter, fromEvent, map } from 'rxjs';
import { getFullAssetPath, getHostNativeElement } from '../helpers';
import { Key } from '../interfaces';
import { DrumService } from '../services';

const getSoundFileFn = () => {
  const assetPath = getFullAssetPath();
  return (description: string) => `${assetPath}sounds/${description}.wav`;
}

const drumKeyTranstionEnd = () => 
  fromEvent(getHostNativeElement(), 'transitionend')
    .pipe(
      filter(evt => evt instanceof TransitionEvent),
      map(evt => evt as TransitionEvent),
      filter(evt => evt.propertyName === 'transform')
    );

@Component({
  standalone: true,
  selector: 'app-drum-key',
  template: `
    <ng-container>
      <kbd>{{ entry.key }}</kbd>
      <span class="sound">{{ entry.description }}</span>
      <audio [src]="soundFile" #audio></audio>
    </ng-container>
  `,
  styles: [`...omitted due to brevity...`],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DrumKeyComponent implements OnDestroy {
  @Input() 
  entry!: Key;

  @ViewChild('audio', { static: true })
  audio: ElementRef<HTMLAudioElement> | undefined;

  @HostBinding('class.playing') isPlaying = false;

  cdr = inject(ChangeDetectorRef);
  playSoundSubscription = inject(DrumService).playDrumKey$
    .pipe(filter(key => key === this.entry.key))
    .subscribe(() => this.playSound());
  transitionSubscription = drumKeyTranstionEnd()
    .subscribe(() => {
      this.isPlaying = false;
      this.cdr.markForCheck();
    });
  getSoundFile = getSoundFileFn();

  get soundFile() {
    return this.getSoundFile(this.entry.description);
  }

  playSound() {
    if (!this.audio) {
      return;
    }

    const nativeElement = this.audio.nativeElement;
    nativeElement.currentTime = 0;
    nativeElement.play();
    this.isPlaying = true;
    this.cdr.markForCheck();
  }

  ngOnDestroy(): void {
    this.playSoundSubscription.unsubscribe();
    this.transitionSubscription.unsubscribe();
  }
}

DrumKeyComponent constructs playSoundSubscription and transitionSubscription subscriptions to play the actual sound and display a yellow border until the sound ends. Using inject operator, I construct these subscriptions outside of constructor and ngOnInit.

Declare drum service to pass data from Drum to DrumKey component

When DrumComponent observes the correct key is pressed, the key must emit to DrumKeyComponent to perform CSS animation and play sound. The data is emit to Subject that is encapsulated in DrumService.

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class DrumService {
  private readonly playDrumKey = new Subject<string>();
  readonly playDrumKey$ = this.playDrumKey.asObservable();

  playSound(key: string) {
    this.playDrumKey.next(key);
  }

  getEntryStore() {
    const entries: Key[] = [
      {
        key: 'A',
        description: 'clap'
      },
      {
        key: 'S',
        description: 'hihat'
      },
      {
        key: 'D',
        description: 'kick'
      },
      {
        key: 'F',
        description: 'openhat'
      },
      {
        key: 'G',
        description: 'boom'
      },
      {
        key: 'H',
        description: 'ride'
      },
      {
        key: 'J',
        description: 'snare'
      },
      {
        key: 'K',
        description: 'tom'
      },
      {
        key: 'L',
        description: 'tink'
      }
    ];

    return {
      entries,
      allowedKeys: entries.map(entry => entry.key),
    }
  }
}

Use RxJS and Angular to implement key down observable

Declare subscription instance member, assign the result of windowKeydownSubscription to it and unsubscribe in ngDestroy()

subscription = windowKeydownSubscription();

ngOnDestroy(): void {
    this.subscription.unsubscribe();
}
// drum.component.ts

const windowKeydownSubscription = () => {
  const drumService = inject(DrumService);
  const allowedKeys = getEntryStore().allowedKeys;
  return fromEvent(inject<Window>(WINDOW), 'keydown')
    .pipe(
      filter(evt => evt instanceof KeyboardEvent),
      map(evt => evt as KeyboardEvent),
      map(({ key }) => key.toUpperCase()),
      filter(key => allowedKeys.includes(key)),
    ).subscribe((key) => drumService.playSound(key));
}
  • fromEvent(inject(WINDOW), 'keydown') - observe keydown event on native window
  • filter(evt => evt instanceof KeyboardEvent) - filter event is an instance of KeyboardEvent
  • map(evt => evt as KeyboardEvent) - cast event to KeyboardEvent
  • map(({ key }) => key.toUpperCase()) - convert key to uppercase
  • filter(key => allowedKeys.includes(key)) - validate key can play sound
  • subscribe((key) => drumService.playSound(key)) - subscribe the observable to play the wav file Use RxJS and Angular to implement play sound file
// drum-key.component.ts

const drumKeyTranstionEnd = () => 
  fromEvent(getHostNativeElement(), 'transitionend')
    .pipe(
      filter(evt => evt instanceof TransitionEvent),
      map(evt => evt as TransitionEvent),
      filter(evt => evt.propertyName === 'transform')
    );

playSoundSubscription = inject(DrumService).playDrumKey$
    .pipe(filter(key => key === this.entry.key))
    .subscribe(() => this.playSound());

transitionSubscription = drumKeyTranstionEnd()
    .subscribe(() => {
      this.isPlaying = false;
      this.cdr.markForCheck();
    });

Let's demystify playSoundSubscription

  • inject(DrumService).playDrumKey$ - observe playDrumKey$ observable of DrumService
  • filter(key => key === this.entry.key) - compare component's key and the key pressed, and they are the same
  • subscribe(() => this.playSound()) - play the wav file

Let's demystify drumKeyTranstionEnd and transitionSubscription

  • fromEvent(getHostNativeElement(), 'transitionend')- observe transition event of the host element
  • filter(evt => evt instanceof TransitionEvent) - filter event is an instance of TransitionEvent
  • map(evt => evt as TransitionEvent) - cast event to TransitionEvent
  • filter(evt => evt.propertyName === 'transform') - filter the event property is transform
  • subscribe(() => { this.isPlaying = false; this.cdr.markForCheck(); }) - subscribe the observable to update host class to display yellow border until the sound stops

This is it, we have created a drum kit that plays sound after pressing key.

Final Thoughts

In this post, I show how to use RxJS and Angular standalone components to create a drum kit. The application has the following characteristics after using Angular 15's new features:

  • The application does not have NgModules and constructor boilerplate codes.
  • Apply inject operator to inject services in const functions outside of component classes. The component classes are shorter and become easy to comprehend.
  • In DrumKeyComponent, I assign subscriptions to instance members directly and don't have to implement OnInit lifecycle hook.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

Resources:

...



📌 Create a drum kit using RxJS and Angular standalone components


📈 113.05 Punkte

📌 Create an analog clock using RxJS and Angular standalone components


📈 78.47 Punkte

📌 Text to speech tutorial using RxJS and Angular


📈 44.68 Punkte

📌 Exploring the Pros and Cons of Standalone Components in Angular


📈 41.47 Punkte

📌 CVE-2023-26116 | angular angular.copy redos (SNYK-JS-ANGULAR-3373044)


📈 40.1 Punkte

📌 Implementing Standalone Components in Angular 15


📈 39.69 Punkte

📌 Standalone components in Angular


📈 39.69 Punkte

📌 How to install Bootstrap 5 in Angular 17... Standalone components Including css,js & icons.


📈 39.69 Punkte

📌 Infinite Scrolling the Angular 6 and RxJS Way!


📈 39.56 Punkte

📌 Sub-RFC 4 for Angular Signals sparks interesting discussion started by RxJS author — Ben Lesh


📈 37.77 Punkte

📌 Episode 24/07: Angular 17.2, optional RxJs


📈 37.77 Punkte

📌 Accelerate Angular App Development with create-angular-auth-nav


📈 34.19 Punkte

📌 How to Run Angular Apps Using Angular CLI and PM2


📈 33.64 Punkte

📌 Using Angular’s EventEmitter to Share Data Between Child and Parent Components


📈 31.96 Punkte

📌 Angular, React, Vue and Co: Peacefully United Thanks To Web Components and Micro Apps


📈 28.62 Punkte

📌 Angular, React, Vue and Co: Web Components and Mini Apps


📈 28.62 Punkte

📌 Updates from the Angular Team and new Angular 17 features!


📈 28.52 Punkte

📌 Angular Addicts #24: Angular 17.3, Signals and unit testing best practices, Storybook 8 & more


📈 28.52 Punkte

📌 My Coding Adventure: Balancing a full-time job, evening classes, and a Drum & Bass events and DJing side hustle


📈 28.44 Punkte

📌 How do I test and mock Standalone Components?


📈 28.11 Punkte

📌 Angular 14.2 bringt Best Practices im Umgang mit Bildern und Standalone-Routing


📈 28.01 Punkte

📌 Angular Standalone in SSR: update


📈 28.01 Punkte

📌 Rendering NativeScript Angular Templates and Components into images


📈 26.83 Punkte

📌 Testing Angular routing components with RouterTestingHarness, provideLocationMocks, and provideRouter


📈 26.83 Punkte

📌 Angular 5.1 zusammen mit Angular Material 5.0.0 erschienen


📈 26.73 Punkte

📌 ANGULAR 6 (FORMERLY ANGULAR 2) – THE COMPLETE GUIDE


📈 26.73 Punkte

📌 7 steps to Angular Material Dialog instead of Angular Component


📈 26.73 Punkte

📌 Building a Dynamic Serverless Photo Blog with Angular & Google Sheets - 1: Solving Angular Router Navigation Issues


📈 26.73 Punkte

📌 CVE-2023-26117 | angular redos (SNYK-JS-ANGULAR-3373045)


📈 26.73 Punkte

📌 Google's New Angular 2.0 Isn't Compatible With Angular 1


📈 26.73 Punkte

📌 Google's New Angular 2.0 Isn't Compatible With Angular 1


📈 26.73 Punkte

📌 CVE-2023-26118 | angular redos (SNYK-JS-ANGULAR-3373046)


📈 26.73 Punkte

📌 Unveiling Angular 17 - The Angular Renaissance


📈 26.73 Punkte

📌 Episode 24/13: Native Signals, Details on Angular/Wiz, Alan Agius on the Angular CLI


📈 26.73 Punkte











matomo