JEE Klassen > TypeScript

Ein JEE Backend ist mit wenig Aufwand über eine JSON basierte REST-Schnittstelle zur Verfügung gestellt. An diesem Punkt kann sich der Java Entwickler zurücklehnen und die Angular Entwickler machen lassen. Als Fullstackentwickler ( oder freundlicher Java Entwickler ) können wir uns ( oder unseren Kollegen ) das Leben leicht leichter machen.

In aller Regel werden wir unsere Angular Anwendung mittels TypeScript schreiben und hier immer wo es geht auf ein typsicheres Vorgehen setzen. Das erfordert es allerdings eigene TypeScript Definitionsdateien zu schreiben welche unsere Datenmodelle ( die wir ja bereits in Java geschrieben haben ) in die TypeScript-Welt bringt. Ein Maven-Plugin welches hier Hilft:

cz.habarta.typescript-generator # typescript-generator-maven-plugin

Dieses registrieren wir in unserer pom.xml, versehen mit einigen zusätzlichen Konfigurationen und wir erhalten eine *.d.ts Datei welche wir in die Angular Entwicklung mit einfließen lassen können:

    <build>
        <plugins>
            <plugin>
                <groupId>cz.habarta.typescript-generator</groupId>
                <artifactId>typescript-generator-maven-plugin</artifactId>
                <version>2.12.476</version>
                <executions>
                    <execution>
                        <id>generate</id>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                        <phase>process-classes</phase>
                    </execution>
                </executions>
                <configuration>
                    <jsonLibrary>jackson2</jsonLibrary>
                
                    <classesFromAutomaticJaxrsApplication>true</classesFromAutomaticJaxrsApplication>
                    <outputKind>module</outputKind>
                </configuration>
            </plugin>
        </plugins>
    </build>

Das funktioniert relativ gut, scheitert natürlich wenn wir mittels JSON-Mapper in die Struktur der generierten JSON-Nachricht eingreifen. Zum Beispiel übermitteln wir bei Relationen (User hat eine Liste von Projekten) sehr häufig lediglich die IDs der Projekte. In unseren TypeScript – Modelklassen werden wir aber gemäß Java-Klassen ein Objekt vom Typ „Projekt“ vorfinden. Hier müssen wir entweder manuell eingreifen, die generierten Klassen erweitern / überschreiben. Oder im Programm durch entsprechende Casts gegensteuern.

Werbeanzeigen

Angular – eigene ReactiveForm Komponenten

In größeren Projekten werden wir relativ schnell den Punkt erreichen in dem sich der ein oder andere Entwickler denkt: „Moment das hatten wir doch schon mal“. Wenn noch nicht geschehen ist das der Zeitpunkt um für den so identifizierten Bereich / Funktion eine eigene Komponente aus zu bilden. Wenn es sich dabei um ein Form-Element handelt mag es zuerst einmal nicht so offensichtlich sein wie man das am besten bewerkstelligt…

Nehmen wir ein einfaches konkretes Beispiel: in unserer Anwendung haben wir eine Auswahlbox von Benutzern durch die z.B. eine Aufgabe, ein Projekt etc. zugeordnet werden soll. Diese Auswahl von Benutzern wird nun an verschiedenen Stellen der Anwendung benötigt. Also entwickeln wir eine eigenen Komponente, was dank Angular-CLI ja auch ein Kinderspiel ist:

ng generate component UserSelector

So einfach ist es in diesem Beispiel jedoch erst einmal nicht. Das Ziel soll es ja sein unsere Komponente als Teil eines Formulars zu verwenden und eine Deklaration analog zu den Standard-HTML Elementen zu erreichen:

<form [formGroup]="form">
  <label for="user">Benutzer: </label>
  <app-user-selector formControlName="userId" id="user"></app-user-selector>
  <label for="comment">Kommentar: </label>
  <input type="text" id="comment" formControlName="comment"/>
</form>

app.component.html > Verwendung unserer Komponente

wir wollen hier einen ReactiveForm-Ansatz verfolgen bei dem die Registrierung von Datenmodel und Validatoren in der Komponentenklasse geschieht, dazu wird hier der Name des Controls mittels ‚formControlName‘ eingesetzt. Die Alternative wäre der Einsatz von „ngModel“

In unserem Projekt setzen wir PrimeNG als Komponentenbibliothek ein, unsere UserSelector Komponente wird also lediglich eine Art Wrapper, hier das Template:

<span [formGroup]="form">
  <p-dropdown
    [options]="users"
    optionLabel="name"
    class="form-control"
    [autoDisplayFirst]="false"
    dataKey="id"
    formControlName="value"
  ></p-dropdown>
</span>

user-selector-component.html > Template unserer Komponente

wir verwenden hier eine PrimeNG Komponente ‚p-dropdown‘. Für unsere eigene Komponente spannend ist hier die Erzeugung einer eigenen FormGroup innerhalb unserer Komponente, das ermöglicht uns die PrimeNG Komponente innerhalb „ganz normal“ mit unserer Komponenten-Klasse zu verbinden

import { Component, OnInit, forwardRef } from '@angular/core';
import { FormGroup, ControlValueAccessor, NG_VALUE_ACCESSOR, FormBuilder } from '@angular/forms';
import { DemoService } from '../demo.service';

// 1
@Component({
  selector: 'app-user-selector',
  templateUrl: './user-selector.component.html',
  styleUrls: ['./user-selector.component.css'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => UserSelectorComponent),
      multi: true
    }
  ]
})
//2
export class UserSelectorComponent implements ControlValueAccessor {
  users: any[];

  form: FormGroup;

  //3
  constructor(private service: DemoService, builder: FormBuilder) {
    service.getAll().subscribe(r => (this.users = r));

    this.form = builder.group({
      value: ['']
    });
    //4
    this.form.controls.value.valueChanges.subscribe(c => {
      this.onChange(c.id);
    });
  }

  //5
  writeValue(obj: number): void {
    if (obj) {
      const value = this.users.find(e => e.id == obj);
      if (value) this.form.patchValue({ value });
    }
  }

  //6
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  onChange: any = () => {};
  onTouched: any = () => {};
}

user-selector-component.ts > Komponentenklasse

1. die Metadaten für unsere Komponente. Neu an dieser Stelle ist die Registrierung unserer Komponente als Value-Accessor über das Provider Array, sehr einfach ausgedrückt ist dieses Deklaration bei jeder dieser Komponenten technisch bedingt genau so zu deklarieren ( sehr spannende Angular-Hintergrund-Themen zu dieser Deklaration: ‚Multi Providers‘ und ‚forwardRef‘ )

2. wir Implementieren das Interface ControlValueAccessor

3. Konstruktor. Neben einem Demo-Service ( der uns die Daten für unsere Liste ermittelt), lassen wir uns auch den FormBuilder von Anuglar injizieren um die FormGroup auf zu bauen

4. hier wird es spannender: wir registrieren einen Change-Listener an unserer internen FormControl ( die PrimeNG Dropdown-Liste ). Wenn der User hier eine Änderungen vornimmt wollen wir dieses Event „weiter geben“ und den Verwender unserer Komponente informieren, dazu wird die Methode onChange ( Interface ) verwendet. ( In unserem Beispiel nehmen wir hier auch eine Umwandlung vor: unserer DropDown-Liste liefert ein komplexes User-Objekt, in unserer Anwendung, also in den anderen fachlichen Objekten verwenden wir lediglich die Id.

5. writeValue ( Interface ) ist die Methode welche aufgerufen wir um ein Objekt welches aus dem Model kommt in der UI darzustellen, also beim Rendern oder bei Änderungen der Form. Wir bekommen an dieser Stelle die Id des bereits selektierten Benutzers, anhand dessen wir das komplexe User-Objekt aus unserem Array ermitteln und unser internes Formular damit aktualisieren.

6. Interface Methoden um die Registrierungen von EventListenern zu ermöglichen

Das war’s damit haben wir eine Komponente welche ganz einfach in unseren Formularen eingesetzt werden kann. Auf diese Weise lassen sich natürlich auch gänzlich eigene Komponenten ( ohne PrimeNG ) implementieren oder wesentlich komplexere wiederverwendbare Komponenten.

Noch mal gucken? > GITHUB

Angular strukturiert 2/2 – @NgModule

Umfangreiche Angular Projekte bestehen in aller Regel aus sehr vielen Komponenten und Service-Klassen die in eine sinnvolle Struktur gebracht werden müssen. Darüber hinaus macht es auch aus Performance Gründen Sinn die eigene Anwendung zu unterteilen. Im ersten Teil haben wir einen Blick auf die Organisation mittels Workspace geworfen. Heute schauen wir uns die Verwendung von Modulen an.

Jede Angular Anwendung besteht aus einem NgModule. Bei genauerer Betrachtung eher aus sehr vielen Modulen, ist doch auch die Core-Bibliothek von Angular in Modulen aufgeteilt ( BrowserModule. FormsModule … ) die wir bei Bedarf importieren. Aber auch unsere eigene Anwendung können wir mittels Module in einzelne Blöcke unterteilen. Neben der besseren Struktur unserer Anwendung hat das noch einen ganz entscheidenden Vorteil: Performance. Wir können komplexe Angular Anwendung in fachliche Teilbereiche gliedern und für jeden dieser Teile ein eigenes Modul implementieren. Ein User der unsere Anwendung aufruft wird nun nicht beim ersten Zugriff unsere gesamte Anwendung laden müssen sondern nur den Bereich ( Modul ) den er gerade anfragt. Erst wenn der User über entsprechende Navigation auf andere Bereiche zugreift werden diese Anwendungsteile geladen. Hier in Kürze wie das geht:

1. ng new [app-name]

Neue Anwendung erzeugen, die in unserem Beispiel nur die Startseite beinhaltet +  gemeinesame Komponente + gemeinsame Services

2. ng generate module [module-1]

Neues Modul generieren, z.B. „materials“ oder „customers“ oder „users“

3. Routing anpassen

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },
  { path: 'customer', 
   loadChildren: './customers/customers.module#CustomersModule' },
  { path: 'material', 
   loadChildren: './materials/materials.module#MaterialsModule' }
];

Der spannende Teil ist hier die Deklaration der Navigation auf die neuen Module, über die Eigenschaft „loadChildren“ wird hier der relative Pfad zur Module Datei angegeben + der Klassenname des Moduls (getrennt mittels „#“). Der Build Prozess resultiert dann in separaten JavaScript Dateien für die jedes Modul welche nur beim auslösen der entsprechenden Navigation abgerufen wird.

Zusammen mit Teil 1 hier auf github

Angular strukturiert 1/2 – workspace

Umfangreiche Angular Projekte bestehen in aller Regel aus sehr vielen Komponenten und Service-Klassen die in eine sinnvolle Struktur gebracht werden müssen. Darüber hinaus macht es auch aus Performance Gründen Sinn die eigene Anwendung zu unterteilen. Im ersten Teil werfen wir einen Blick auf die Organisation mittels Workspace.

In allen unseren Projekten werden wir den ein oder anderen Service Implementieren oder Komponenten ausprägen die nicht nur für das vorliegende Projekt interessant sind, sondern auch für anderen unternehmenseigene Projekte. Eine solche eigene Basis-Bibliothek lässt sich natürlich als eigenständiges NPM-Packet entwickeln und über ein internes Node-Repository ausliefern. Insbesondere zur frühen Entwicklungszeit, wenn sich noch viel in den Bibliotheken tut verkompliziert dieses Vorgehen aber die Bereitstellung und Verwendung. Eine Alternative (die sich aber später ebenfalls problemlos als NPM-Paket veröffentlichen lässt) ist seit der Version 6 in Angular-CLI vorhanden: „Libraries“ .

Die Grundidee ist einfach: ein einzelnes Angular-Projekt  (workspace)  dient als Basis für die Entwicklung unserer Anwendungen und Bibliotheken. Für jede Bibliothek ( library) oder Anwendung( application ) werden über entsprechende CLI-Befehle eigenständige Bestandteile generiert. Hier das grundsätzliche Vorgehen mittels Angular-CLI Befehle:

1.  ng new [workspacename]

Optional kann hier die Option: –create-application=false gewählt werden um die Demo-Anwendung nicht initialisieren zu lassen

2. ng generate library [name-1] –prefix=[prefix]

Eine erste Bibliothek, die wir später in unseren Projekten verwenden können. Hier entwickeln wir die gemeinsamen Komponenten und Services.

3. ng build [name-1]

Bibliothek bauen, optional kann hier auch das Flag –watch aktiviert werden um bei Änderungen den Build-Prozess zu triggern

4. ng generate application [name-2]

Eine Anwendung generieren. Hier arbeiten wir wie gewohnt, können aber auf die Services/Komponenten unserer Library zugreifen (nicht vergessen das Utils-Module zu importieren)

5. ng serve [name-2]

Entwicklungs-Server starten für definierte Anwendung…

Sowohl die Bibliotheken, als auch die einzelnen Anwendungen können später ganz autonom verteilt oder deployt werden. Im zweiten Teil werfen wir einen Blick auf die strukturierung mittels NgModule.

Bis dahin:

https://github.com/GEDOPLAN/anuglar-modulesD

Angular, i18n mit ngx-translate

Internationalisierung. Eine typische Aufgaben bei der Implementierung von Web-Anwendungen. Diese Anforderung macht auch vor Angular nicht halt. Hier bieten sich dem Entwickler gleich zwei Möglichkeiten: im ersten Teil haben wir die Möglichkeiten der Core-Bibliothek betrachtet, heute werfen wir einen Blick auf eine zusätzliche Bibliothek: ngx-translate.

i18n-2

@ngx-translate/core kann wie gewohnt per npm-Install im eigenen Projekt integriert werden. Ab hier ist die Verwendung (vielleicht im vergleich zur Core-Variante) relativ gut nach zu vollziehen. Anstatt den Build-Prozess für unterschiedliche Sprachen zu manipulieren werden die Texte hier zur Laufzeit umgewandelt. Damit das funktioniert muss das entsprechende Modul registiert und konfiguirert werden:

import {TranslateModule} from '@ngx-translate/core';


@NgModule({
  imports: [
      TranslateModule.forRoot()
  ],
   ...

Grundsätzlich wäre damit der Service schon einsatzbereit, müsste aber „manuell“ mit Übersetzungen versehen werden. In aller Regel wollen wir hier aber die Übersetzungen in einer separaten Datei vorhalten. Im Fall von ngx-translate geschieht das in einer JSON-Struktur.

{
  TITLE: 'Willkommen',
  HOME: {
    TEXT: 'Hallo Welt',
    FOOTER: 'verwendete Sprache: {{loc}}'
  }

Wie diese zugreifbar gemacht wird, wird über so genannte „TranslateLoader“ entschieden. Dabei handelt es sich um ein simples Interfaces welches entweder selber implementiert werden kann oder man greift z.B. auf den HTTPLoader (@ngx-translate/http-loader) zurück. Als Beispiel einer Implementierung wäre auch eine lokale Variante denkbar:

export class I18nLoader implements TranslateLoader {
  getTranslation(lang: string): Observable {
    if (lang === 'de') {
      return of(de);
    } else {
      return of(en);
    }
  }
}

---- Registrierung ----    
TranslateModule.forRoot({
  loader: {
    provide: TranslateLoader,
    useClass: I18nLoader
  }
})

(nicht für Produktion gedacht, alle Übersetzungen der Anwendungen würden immer zum Browser übertragen werden!)

Damit sind wir schon in der Lage unsere Übersetzungen ein zu setzen. Dies geschieht entweder:

im Template mittels Pipes

{{ 'HOME.TEXT' | translate }}
{{ 'HOME.FOOTER' | translate: { loc: 'de' } }}

oder im Code mittels Service

constructor(private translateService: TranslateService) {
  this.locale = translateService.getBrowserLang();
  translateService.use(this.locale);
  translateService.get('TITLE').subscribe(t => console.log('ngx: ' + t));
}

Das Vorgehen mittels ngx-translate ist sicherlich etwas weniger komplex in der Verwendung und Konfiguration und bietet genug Flexibilität um auch im Projektalltag gut einsetzbar zu sein. Ein Nachteil müssen wir uns allerdings bewusst machen: die Übersetzungen zur Laufzeit durchführen zu lassen kostet (zumindest ein wenig) Zeit, sodass mit der (komplizierteren?) Core-Variante ein leichter Performance-Vorteil vorliegt.

Live! In Farbe! auf github

Angular, i18n

Internationalisierung. Eine typische Aufgaben bei der Implementierung von Web-Anwendungen. Diese Anforderung macht auch vor Angular nicht halt. Hier bieten sich dem Entwickler gleich zwei Möglichkeiten: zum einen die Verwendung der Core-Bibliothek oder die Verwendung der Bibliothek: „ngx-translate“.

i18n-1

Angular i18n

Die Core Bibliothek bietet bereits die Funktion Texte innerhalb der Anwendung zu internationalisieren dazu ist in folgenden Schritten vorzugehen:

  1. Texte auszeichnen
  2. Übersetzungsdatei erzeugen
  3. Übersetzungen durchführen
  4. Anwendung bauen

 

Texte auszeichnen

Angular bedient sich für die Internationalisierung der Templates der Pipe „i18n“. Diese kann innerhalb des Templates verwendet werden um die Inhalte von beliebigen Komponenten zu markieren:

 
<h1>placeholder-text</h1> 

Neben der reinen Markierung existieren außerdem noch Möglichkeiten zum setzen
einer Beschreibung oder die Vergabe einer eindeutigen ID die später hilfreich ist wenn Texte der Anwendung hinzugefügt werden sollen: beschreibung@@id

 
<h1>placeholder-text</h1> 

 

Übersetzungsdatei erzeugen

Dank Angular-CLI ist die Erzeugung einer entsprechenden Übersetzungs-Datei dann ein Kinderspiel

 ng xi18n 

Führt zur Generierung einer „xlf-Datei“ welche alle markierten Textstellen als so genannte „trans-unit“ beinhaltet:

 
 
  placeholder 
   
    app/app.component.html 
    12 
  
 

 

Übersetzungen durchführen

Die Default-Sprache ist Englisch. Für weitere Übersetzungen wird die so erzeugte Datei nun kopiert und in aller Regel um einen Sprach-Post Fix erweitert (z.B. messages.de.xlf) Die eigentliche Übersetzung muss dann natürlich noch in den Dateien durchgeführt werden: dazu wird neben den bereits vorhandenen „source“ Attributen ein „target“ Attribut ergänzt, welches den übersetzten Wert erhält:

 

  (welcome) 
  Willkommen 
  ... 
 

 

(Alltagstauglich mit ngx-i18nsupport)

Bis hierher erscheint das Vorgehen noch nachvollziehbar. Allerdings zeigt sich die Core Bibliothek bei einem genaueren Blick etwas sperrig:

ein Update bestehender Texte ist direkt nicht möglich.

So würde bei einem erneuten Aufruf der Generierung die bestehende Datei einfach überschrieben werden und alle bisher gepflegten Übersetzungen wären dahin. Ein npm-Tool was hier Abhilfe schafft ist: xliffmerge welches über npm installiert werden kann (npm install –save-dev ngx-i18nsupport). Dieses Tool erlaubt uns ein intelligentes mergen von bestehenden Sprachdateien. Ein Typischer Ablauf lässt sich vermutlich am einfachsten anhand des projekt-tauglichen npm-Script erkären:

 
---- messages.config.json ----- 
{ 
 "xliffmergeOptions": { 
      "srcDir": "src/i18n", 
      "genDir": "src/i18n" 
    } 
} 

---- npm script ----- 
"i18n": 
   "ng xi18n --output-path i18n --i18n-locale en 
     && xliffmerge --profile src/messages.config.json en de" 

Zuerst lassen wir Angular-CLI ein Standard Message File generieren, welches wir in einem speziellen Ordner ablegen (ng xi18n –output-path i18n). Dann kommt xliffmerge ins Spiel: mittels einer Konfigurationsdatei (in der wir im einfachsten Fall wie zu sehen nur den Ordner angeben in dem unsere messages.xlf abgelegt wurde) und den Sprachen die wir unterstützen wollen (xliffmerge –profile src/messages.config.json en de) werden nun entsprechende Dateien erzeugt (messages.de.xlf, message.en.xlf). Der entscheidende Punkt:
sollten diese Dateien bereits vorhanden und mit Übersetzungen versorgt sein werden nur die neuen Elemente in diese Dateien übernommen.

 

Anwendung bauen

Ein zweiter Punkt dürfte den ein oder anderen überraschen. Wenn wir den AOT Compiler verwenden (was wir in aller Regel für unsere Produktions-Builds tun wollen und in der aktuellen Version auch der Standard Fall bei einem Prod-Build über
Angular CLI ist) ist ein dynamisches lesen / ermitteln der übersetzten Werte technisch nicht möglich. Der Compiler läuft ja im Fall von AOT nicht im Browser sondern bereits beim Build-Prozess.

Das heißt: ein separater Build pro unterstützte Sprache

und eine eigenständige Version unserer Anwendung die deployt werden muss. Das klingt nun etwas aufwendiger / dramatischer als es ist. Also schauen wir uns das ganze mal praktisch an:

mit AOT

Ein separater Build für jede unterstützte Sprache. Das lässt sich relativ einfach über ein npm Skript lösen das z.B. für Deutsch und Englisch auf Basis der oben erstellen Message-Dateien so aussehen könnte:

 "build": 
"npm run i18n 
&& ng build --prod --i18n-file src/i18n/messages.en.xlf --i18n-format xlf --i18n-locale en --output-path dist/en
--base-href . 

&& ng build --prod --i18n-file src/i18n/messages.de.xlf --i18n-format xlf --i18n-locale de --output-path
dist/de --base-href .", 

für jede Sprache haben wir nun im „dist“ Ordner eine lokalisierte Version unserer
Anwendung. Ein entsprechend konfigurierter Proxy oder eine simple Einstiegsseite mit ein wenig JavaScript könnte nun
automatisch auf Basis der Browser-Locale auf die korrekte Version umleiten.

mit JIT

Während der Entwicklung werden die meisten ( aus Gründen der Geschwindigkeit) auf den JIT Compiler setzen ( Standard bei ng serve). Zwar muss auch hier die Sprache vor Start der Anwendung feststehen ( und damit muss die Anwendung bei immer neu geladen werden wenn die Sprache geändert werden sollte) aber anders als bei AOT kann die Anwendung je nach Bedarf dynamisch initialisiert werden. Dazu konfigurieren wir die entsprechende Sprache und Message-Datei beim bootstrap unserer Anwendung in der main.ts, hier zum Beispiel über die Browser-Locale:

 
declare const require;
export function translationsFactory() {
  let locale = (window.clientInformation && window.clientInformation.language) || window.navigator.language;
  let language = locale.split('-')[0];

  return require(`raw-loader!./i18n/messages.${language}.xlf`);
}

platformBrowserDynamic()
  .bootstrapModule(AppModule, {
    providers: [
      { provide: TRANSLATIONS, useFactory: translationsFactory, deps: [] },
      { provide: TRANSLATIONS_FORMAT, useValue: 'xlf' }
    ]
  })
  .catch(err => console.error(err));

 

(Alltagstauglich mit i18n-polyfill)

Leider muss auch hier wieder ein „aber“ da gelassen werden. Eine typische Anforderung ist sicherlich auch Benachrichtigungen zu internationalisieren, also auch programmatisch auf dies Texte zugreifen zu können. Bisher hat es dieses Feature leider noch nicht direkt in den Standard geschafft, bis dahin existiert aber ein Polyfil der hoffentlich in ähnlicher Form auch in den Standard übergehen wird:
i18n-polyfill, https://github.com/ngx-translate/i18n-polyfill

Viele Ecken müssen bis hierher umschifft werden um die vermeidlich einfache Anforderung der Internationalisierung um zu setzen. Offiziell heißt es das dieser Prozess / das Vorgehen noch verbessert werden soll um die Verwendung von i18n zu vereinfachen,seien wir also gespannt.
Im zweiten Teil werfen wir trotzdem einen kurzen Blick auf eine Alternative Lösung die in Sachen Performance zwar der Core-Bibliothek nicht das Wasser reichen kann, aber ein etwas pragmatischeres Vorgehen wählt.

Live! In Farbe! auf github

Frohe Weihnachten, Angular Dynamic Components

demo

auf github