Angular Libraries mit Nx – So veröffentlichst Du Deine Komponente

Foto des Autors
Robert Weyres

npm ist vermutlich das bekannteste Portal für Abhängigkeiten und Bibliotheken für JavaScript/TypeScript und Node.js Applikationen. Die hier veröffentlichten Pakete können über npm install paketname einem Projekt hinzugefügt werden. In diesem Tutorial zeige ich dir anhand eines simplen Kontaktformulars, wie Du auch Deine Bibliothek dort veröffentlichen kannst.

Einleitung

Hast Du eine UI-Komponente für Angular Libraries mit Nx gebaut, die die Welt verändern kann? Auch wenn das ein eher unwahrscheinliches Szenario ist, so können auch alltäglich benötigte Komponenten für andere Projekte oder die Öffentlichkeit interessant sein. Ich zeige dir, wie du eine Komponentenbibliothek schreibst, die auf npm veröffentlicht wird und dann für die ganze Welt verfügbar ist!

Als Grundlage dient ein Nx-Workspace, in dem die neue Bibliothek angelegt und aus dem heraus sie veröffentlicht wird. Ein Showcase-Projekt wird die neue Funktionalität präsentieren.

Für dieses Szenario werden wir ein einfaches Kontaktformular erstellen, das über einen öffentliche (oder eigene) API eine E-Mail versenden kann. Diese alleinstehende Komponente wird als Library in anderen Angular- oder Nx-Projekten verwendbar sein, damit Du sie in Zukunft nicht erneut schreiben musst.

GitHub Repository
Das fertige Git Repository zu diesem Artikel sind im Conciso GitHub zum Download verfügbar.

Erstellung der Bibliothek

Die Bibliothek soll in einem Nx Workspace installiert werden, wofür ich das Kommandozeilentool von Nx benutze. Dieses kann mit npm install -g nx installiert und benutzt werden.

Anschließend habe ich mit dem Befehl npx create-nx-workspace@latest einen Nx Workspace mit einer Angular Anwendung erstellt:

$ npx create-nx-workspace@latest contact-form
? What to create in the new workspace         angular 
? Application Name                                              contact-form-showcase 
? Default stylesheet format                                SASS(.scss) 
? Use Nx Cloud?                                                    No
Code-Sprache: Bash (bash)

Die Angular Anwendung dient als Showcase um die Library einzubinden und darzustellen.

Um die Library zu generieren, wechsele ich zunächst in den neuen Ordner “contact-form” (cd contact-form).

Mit dem Befehl nx g library contact-form --publishable --importPath @namespace/contact-form wird eine neue Bibliothek mit dem Namen “contact-form” erstellt. Das Flag “publishable” bewirkt, dass zusätzliche Dateien innerhalb der Bibliothek angelegt werden, die für das Veröffentlichen auf npm benötigt werden. Der “importPath” ist der Name der letztendlich veröffentlichten Bibliothek, worüber sie mittels npm install zu einem Projekt hinzugefügt werden kann.

Das Kontaktformular

Ein simples Kontaktformular benötigt lediglich ein Form und eine Möglichkeit, die eingegebenen Daten abzuschicken. Ich habe daher eine UI-Komponente für das Formular selbst, sowie einen Service für die Kommunikation mit der E-Mail API erstellt:

nx g c contact-form --project=contact-form

nx g s email-api --project=contact-form

In der Moduldatei der Bibliothek habe ich für die API-Interaktion das HttpClientModule und das ReactiveFormsModule für das Formular selbst importiert.

Inhalt von libs/contact-form/src/lib/contact-form/contact-form.module.ts:

import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';

import { ContactFormComponent } from './contact-form/contact-form.component';
import { EmailApiService } from './email-api.service';

@NgModule({
  imports: [CommonModule, HttpClientModule, ReactiveFormsModule],
  declarations: [ContactFormComponent],
  exports: [ContactFormComponent],
  providers: [EmailApiService],
})

export class ContactFormModule {}
Code-Sprache: TypeScript (typescript)

Die UI-Komponente besteht aus einem Formular mit 3 Textfeldern für Name, E-Mail Adresse und Nachricht, sowie einem Button für das Absenden. Unter dem Button wird nach dem Versenden eine Statusmeldung angezeigt, ob der Versand erfolgreich war oder nicht.

Inhalt von libs/contact-form/src/lib/contact-form/contact-form-component.html:

<form
  class="contact-form"
  [formGroup]="formGroup"
  (ngSubmit)="submitFormData(formGroup)"
>
  <div class="form-group">
    <label for="name">Ihr Name</label>
    <input
      class="form-control"
      type="text"
      name="name"
      placeholder="Name"
      formControlName="name"
    />
  </div>
  <div class="form-group">
    <label for="email">Ihre E-Mail Adresse</label>
    <input
      class="form-control"
      type="email"
      name="email"
      placeholder="ihre@mail.de"
      formControlName="email"
    />
  </div>
  <div class="form-group">
    <label for="message">Ihre Nachricht</label>
    <textarea class="form-control" formControlName="message" name="message">
    </textarea>
  </div>
  <button class="form-button" type="submit" [disabled]="!formGroup.valid">
    Senden
  </button>
  <div class="form-status" *ngIf="(emailSent$ | async) === true">
    E-Mail erfolgreich versendet!
  </div>
  <div class="form-status" *ngIf="(emailSent$ | async) === false">
    Fehler: E-Mail konnte nicht versendet werden
  </div>
</form>
Code-Sprache: HTML, XML (xml)

Anschließend bekommt das Formular etwas Styling in der zugehörigen SCSS-Datei, um es auch optisch ansprechend zu gestalten.

Inhalt von libs/contact-form/src/lib/contact-form/contact-form-component.scss

.contact-form {
  background-color: white;
  max-width: 500px;
  margin: auto;
  padding: 40px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.075);
  box-sizing: border-box;
}

.form-button {
  display: block;
  border: 1px solid #ced4da;
  border-radius: 0.25rem;
  width: 96%;
  padding: 0.375rem 0.75rem;
}

.form-control {
  display: block;
  width: 90%;
  padding: 0.375rem 0.75rem;
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.5;
  color: #282b2e;
  background-color: #fff;
  background-clip: padding-box;
  border: 1px solid #ced4da;
  border-radius: 0.25rem;
  margin-top: 0.5rem;
  transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}

.form-group {
  margin-bottom: 1rem;
}

.form-status {
  margin-top: 2rem;
}

textarea {
  overflow: auto;
  resize: vertical;
}
Code-Sprache: SCSS (scss)

In der Komponente selbst wird das Form zusammengebaut und abgeschickt. Außerdem habe ich ein Subject definiert, das den Status des API-Aufrufs enthält, um die Statusmeldung unter dem Button anzeigen zu können.

Der Input-Parameter apiURL kann von außen gesetzt werden und bestimmt, wo die Formulardaten hingeschickt werden.

Inhalt von libs/contact-form/src/lib/contact-form/contact-form-component.ts

import { Component, Input, OnDestroy } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil, tap } from 'rxjs/operators';

import { EmailApiService } from '../email-api.service';

@Component({
  // eslint-disable-next-line @angular-eslint/component-selector
  selector: 'contact-form',
  templateUrl: './contact-form.component.html',
  styleUrls: ['./contact-form.component.scss']
})

export class ContactFormComponent implements OnDestroy {
  @Input() apiURL = 'https://forwardmethis.com/meine@mail.de';

  formGroup: FormGroup;
  emailSent: Subject<boolean | null> = new Subject();
  emailSent$ = this.emailSent.asObservable();

  private unsubscribe$: Subject<void> = new Subject();

  constructor(
    private builder: FormBuilder,
    private emailService: EmailApiService
  ) {
    this.emailSent.next(null);
    this.formGroup = this.builder.group({
      name: new FormControl('', [Validators.required]),
      email: new FormControl('', [Validators.required, Validators.email]),
      message: new FormControl('', [Validators.required])
    });
  }

  ngOnDestroy(): void {
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }

  submitFormData(formData: FormGroup) {
    this.emailService
      .sendEmail(this.apiURL, formData.value)
      .pipe(
        tap((response: string) => {
          if (response === 'OK') {
            this.emailSent.next(true);
          } else {
            this.emailSent.next(false);
          }
        }),
        takeUntil(this.unsubscribe$)
      )
      .subscribe();
  }
}
Code-Sprache: TypeScript (typescript)

Der Email-API Service sendet die Daten des Forms als JSON an die definierte API.

Inhalt von libs/contact-form/src/lib/email-api.service.ts

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class EmailApiService {
  httpOptions = {
    headers: new HttpHeaders({
      'Content-Type': 'application/json'
    })
  };
  constructor(private http: HttpClient) {}

  sendEmail(apiURL: string, data: any): Observable<any> {
    return this.http.post(apiURL, JSON.stringify(data), this.httpOptions).pipe(
      map((response: any) => {
        return response;
      }),
      catchError((error: any) => {
        return of(error.statusText);
      })
    );
  }
}
Code-Sprache: TypeScript (typescript)

Mit diesen Elementen habe ich bereits alles, was für das Kontaktformular benötigt wird und kann es zur Präsentation im Showcase-Projekt einbinden.

Präsentation im Showcase

Um das neue Formular in der Showcase-Applikation anzuzeigen, muss das zugehörige Modul in der app.module.ts des Showcase-Projekts importiert werden.

Inhalt von apps/contact-form-showcase/src/app/app.module.ts:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ContactFormModule } from '@tehw0lf/contact-form';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, ContactFormModule],
  providers: [],
  bootstrap: [AppComponent],
})

export class AppModule {}
Code-Sprache: TypeScript (typescript)

Nun muss nur noch der entsprechende Selektor aus der UI-Komponente in app.component.html eingebunden werden.

Inhalt von apps/contact-form-showcase/src/app/app.component.html:

<contact-form [apiURL]="emailAPIURL"></contact-form>
Code-Sprache: HTML, XML (xml)

Die API-URL wird in der zugehörigen Komponente gesetzt.

Inhalt von apps/contact-form-showcase/src/app/app.component.ts:

import { Component } from '@angular/core';

@Component({
  selector: 'contact-form-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})

export class AppComponent {
  emailAPIURL = 'https://forwardmethis.com/meine@mail.de';
}
Code-Sprache: TypeScript (typescript)

Das war schon alles! Zum Test kann mittels nx serve -o die Anwendung gestartet und im Browser geöffnet werden. Das Ergebnis sollte ein einfaches Form mit einem Button sein:

Wenn ich nun einen Namen, eine E-Mail Adresse und eine Nachricht eingebe, wird der Button aktiv:

Anschließend wird nach dem Klick auf Senden angezeigt, dass die E-Mail erfolgreich versendet wurde:

Die Oberfläche von ContactFormShowcase zeigt ein ausgefülltes Kontaktformular mit Name, E-Mail-Adresse und Nachricht und den Hinweis für die erfolgreiche Versendung der E-Mail

Die resultierende E-Mail ist sehr minimalistisch, wenn mit dem Standard-Service gearbeitet wird. ForwardMeThis sendet lediglich den Inhalt des Forms als JSON-Objekt an die E-Mail Adresse, die beim HTTP-Aufruf übergeben wird. Beim ersten Aufruf wird zunächst eine Bestätigungsmail gesendet, wo der Erhalt von Emails von ForwardMeThis erlaubt werden muss.

Inhalt der Email:

Oberfläche von Forward Me This zeigt den Inhalt der gesendeten E-Mail für den Wissensbeitrag Angular Libraries mit Nx – So veröffentlichst Du Deine Komponente
Resultierende E-Mail von ForwardMeThis

Nun erfüllt das Kontaktformular seinen Zweck und ist bereit, als Bibliothek veröffentlicht zu werden.

Veröffentlichen auf npm

npm ist vermutlich das bekannteste Portal für Abhängigkeiten und Bibliotheken für JavaScript/TypeScript und Node.js Applikationen. Die hier veröffentlichten Pakete können über npm install paketname einem Projekt hinzugefügt werden.

Mit Nx haben wir bereits eine Bibliothek erstellt, die veröffentlicht werden kann. In der Datei package.json in der Bibliothek wird der Paketname und die Version gesetzt, sowie nötige Abhängigkeiten, um die Bibliothek zu verwenden.

Inhalt von libs/contact-form/package.json:

{
  "name": "@namespace/contact-form",
  "version": "0.0.1",
  "peerDependencies": {
    "@angular/common": "^13.0.0",
    "@angular/core": "^13.0.0",
    "@angular/forms": "^13.0.0"
  },
  "dependencies": {
    "tslib": "^2.3.0"
  }
}
Code-Sprache: JSON / JSON mit Kommentaren (json)

Wichtig hierbei ist, dass die Version fortlaufend hochgezählt wird.

Um die Bibliothek zu veröffentlichen, wird ein Account auf npm | Sign Up benötigt. Nun kann man sich über die Kommandozeile in seinen Account einloggen mit npm login. Hierbei werden Name, Password und E-Mail Adresse abgefragt. Die E-Mail Adresse ist auch in den Metadaten der veröffentlichten Pakete enthalten.

Zur Vorbereitung der Bibliothek wird das Projekt über npm run build gebaut. Hierbei wird die Bibliothek und die Showcase-Applikation gebaut und in den Ordner dist/ kopiert.

Um die Bibliothek nun zu veröffentlichen, führe ich npm publish dist/libs/contact-form --access public --dry-run aus. Das --dry-run Flag besagt, dass zunächst noch nicht veröffentlicht wird, sondern es sich um einen Probelauf handelt.

Wenn beim Probelauf keine Fehler aufgetreten sind, veröffentliche ich die Bibliothek. Hierzu lasse ich das --dry-run Flag einfach weg: npm publish dist/libs/contact-form --access public.

Anschließend ist die Bibliothek auf npm öffentlich gelistet unter dem gewählten Paketnamen.

Ein sehr ähnliches Kontaktformular habe ich zum Beispiel auf diesem Wege unter npm: @tehw0lf/contact-form veröffentlicht.

Schreibe einen Kommentar

Das könnte Dich auch noch interessieren

Performante Authentifizierung mit Keycloak

Performante Authentifizierung mit Keycloak

Ich werde anhand eines konkreten Testszenarios zeigen, wie man die Performance einer verteilten Anwendung durch föderative Authentifizierung mit Keycloak massiv ...
Die Anatomie von CQRS in Java

Die Anatomie von CQRS in Java

Aufgrund meiner festen Überzeugung, dass man Patterns am besten lernt, wenn man sie zunächst einmal selbst implementiert hat, erläutere ich ...
Keycloak lokal installieren in nur 3 Schritten

Keycloak lokal installieren in nur 3 Schritten

Keycloak kann mit SPIs fast beliebig erweitert werden. Dafür ist es sinnvoll eine lokale Installation von Keycloak bereitzustellen. Wie das ...