Angular Material und Mehrsprachigkeit - ein umfassendes How-to
Wie bekomme ich ein Projekt mit Angular und Material Design aufgesetzt und welche Herausforderungen stellen sich, wenn ich dort Mehrsprachigkeit implementieren möchte? In folgendem How-to habe ich meine Erfahrungen zusammengefasst. Ganz am Ende des Artikels findet ihr einen Download aller aufgeführten Code-Schnippsel. Viel Spaß beim Nachvollziehen!
Angular Materials sind moderne, schlichte Designkomponenten, mit denen unter relativ geringem Aufwand optisch ansprechende Anwendungen gestaltet werden können, welche sich auf allen Anzeigegeräten mit geringem Zusatzaufwand umsetzen lassen.
Das Materialdesign zielt darauf ab, Objekte in drei-dimensionalem Raum zu simulieren und somit eine natürliche, intuitive Darstellung und Interaktion zu ermöglichen, was durch verschiedene Prinzipien, die bei der Entwicklung beachtet werden müssen, ermöglicht werden soll.
- Materialien können in x- und y-Dimension variieren, besitzen jedoch eine einheitliche Höhe von 1dp (dp = density independent Pixel. Behält die gleiche Dimension bei unterschiedlichen dpi und ist nur für die Android-Entwicklung relevant. Kann für css schlichtweg mit px ersetzt werden).
- Objekte im Raum werfen Schatten, die je nach Höhenlage variieren, ergo auch Materialien.
- Materialien sind de facto Medien zur Darstellung ihres Inhalts und sind unabhängig davon, sprich Inhalte sind dynamisch veränderbar und können nur einen Teil des Materials ausfüllen, sind jedoch durch dessen Dimensionen beschränkt (kein Overflow erlaubt).
- Materialien sind Festkörper und blockieren demnach Event-Bubbling, können keine Positionen im Raum einnehmen, die bereits belegt sind und beschränken ihre Transformationen auf freien Platz im Raum, zum Beispiel keine Höhenveränderung durch ein darunterliegendes Material hindurch.
- Materialien dürfen jederzeit ihre Form und Größe innerhalb der zuvor genannten Beschränkungen verändern, sind jedoch als fest anzusehen - sprich kein falten oder verbiegen.
- Materialien können sich verbinden oder trennen.
- Weitere Informationen: https://material.io/guidelines/material-design/introduction.html
Angular Material bietet viele Designideen bereits vorumgesetzt an, die oftmals lediglich als css-Klasse oder Direktive angefügt werden müssen.
- Theming: Mittels scss-Mixins können Farbschemata einfach geändert werden. Ein eigenes Mixin kann erstellt werden, um komplexere Änderungen vorzunehmen.
- Elevation helpers: Mittels css-Klassen oder Mixins können z-Positionen einfach dargestellt werden. Auch für Animationen bei Höhenänderung gibt es ein Mixin.
- Typography: css-Klassen geben font-size, -height und -weight vor. Mixins bieten die Möglichkeit zur Anpassung.
Eine Übersicht der verschiedenen Komponenten mit Beispielen und Erläuterung des gesamten Funktionsumfangs kann hier betrachtet werden: https://material.angular.io/components/categories
Vorbereitung
In dieser Beispielanwendung wird zur Entwicklung Visual Studio Code mit Angular CLI verwendet, durch das mit wenigen Zeilenbefehlen ein arbeitsfähiges Projekt erstellt werden kann, ohne sich um config und packages kümmern zu müssen. Zudem wird ein laufendes Projekt on-the-fly kompiliert und im Browser aktualisiert.
Visual Studio Code
Sofern nötig muss Visual Studio Code installiert werden. Der Download kann von hier aus gestartet werden: https://code.visualstudio.com/
Node.js
Um Angular CLI nutzen zu können, muss zunächst node.js installiert werden, mindestens Version 6.9.0, besser jedoch die aktuellste Version. Auch der Node.js Package Manager (NPM) wird mit mindestens Version 3 benötigt, da dieser jedoch in der Node.js Installation enthalten ist, sind hier keine zusätzlichen Schritte notwendig.
Download Node.js: https://nodejs.org/en/download/
Angular CLI
Angular CLI kann aufgrund des zuvor installierten NPM bereits per Konsolenbefehl installiert werden. Hierzu wird also Visual Studio Code gestartet und ein Projektordner ausgewählt (entweder über die Welcome-Page oder über Menüpunkt Datei -> Ordner öffnen…).
In meinem Fall verwende ich den bereits vorhandenen Ordner "My Web Sites" in den "Eigenen Dokumenten".
Nun kann folgender Befehl im Terminal eingegeben werden, um Angular CLI zu installieren (Das Terminal kann über Anzeigen -> Integriertes Terminal angezeigt werden):
npm install -g @angular/cli
Neues Angular2 Projekt
Nun kann mit einem weiteren Konsolenbefehl ein neues Angular2 Projekt angelegt werden (sollte sich der Prompt nicht im zuvor ausgewählten Projektordner befinden, sollte zunächst dorthin navigiert werden):
ng new {PROJEKTNAME}
In diesem Fall wurde das Projekt mit einer Beschreibung benannt: Angular2MultilingualMaterialistic, also:
ng new Angular2MultilingualMaterialistic
Im Explorer sollte nun folgende Struktur vorhanden sein:
Um das Projekt zu testen, muss im Terminal in den Projektordner navigiert werden:
cd .\Angular2MultilingualMaterialistic\
Dort wird schließlich mittels folgendem Befehl die Anwendung kompiliert und, sofern nicht anders angegeben, auf localhost Port 4200 zur Verfügung gestellt:
ng serve
Ein erster Test ergibt folgendes Resultat:
Angular Material
Mit folgendem Befehl wird nun Angular Material dem Projekt hinzugefügt:
npm install --save @angular/material @angular/cdk @angular/animations
- Material: Dies ist das Standardpaket, welches die Materialkomponenten enthält.
- CDK (Component Dev Kit): Dies ist ein Paket, welches komponentenunabhängige Werkzeuge zur Verfügung stellt, wie zum Beispiel Tabelle, Layout, Scrolling oder Overlay.
- Animations: Einige Komponenten benötigen dieses Paket für erweiterte Animationen. ACHTUNG: Das Animationspaket verwendet die WebAnimations API, die noch nicht von allen Browsern unterstützt wird. Darum muss eventuell ein Polyfill inkludiert werden: https://github.com/web-animations/web-animations-js
Abschließend kann das Animationsmodul in unser app.module.ts eingefügt werden. Alle übrigen Module sind komponentenbasierend, das passende Modul muss also nur hinzugefügt werden, wenn die Komponente tatsächlich benötigt wird.
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, BrowserAnimationsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Mehrsprachigkeit
Für eine möglichst schnelle und einfache Implementierung der Mehrsprachigkeit kann man das Paket ngx-translate verwenden. Mit folgendem Befehl wird es dem Projekt hinzugefügt:
npm install --save @ngx-translate/core @ngx-translate/http-loader
Es reicht im Grunde zwar das core-Paket zu installieren, dann muss jedoch jede Übersetzung im Code hinzugefügt werden. Einfacher ist es, einen Loader zu verwenden, mit dem die Übersetzungen aus einer Datei ausgelesen werden können. In diesem Fall wird der zur Verfügung stehende HttpLoader verwendet, für den das entsprechende Paket mitinstalliert werden muss.
Auch das Mehrsprachigkeitsmodul muss der Anwendung hinzugefügt werden:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClient, HttpClientModule } from '@angular/common/http'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { AppComponent } from './app.component'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './assets/translation/', '.json'); } @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, BrowserAnimationsModule, HttpClientModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, useFactory: (HttpLoaderFactory), deps: [HttpClient] } }) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Hier wird der von ngx-translate zur Verfügung gestellte TranslateHttpLoader verwendet, der Übersetzungen aus Dateien mittels HttpClient lädt. Über eine Funktion wird ein neuer Loader erstellt, dem ein Pfad und ein Dateityp mitgegeben wird.
Nun müssen also in dem angegebenen Pfad die Sprachdateien angelegt werden und mit den Übersetzungen befüllt werden. ngx-translate bietet diverse Funktionen wie Nesting, in Text Parameter, Html-Tags, Service, Pipe und Direktive (mehr hier: https://github.com/ngx-translate/core). Der Name der Sprachdateien ist zugleich auch der Sprachname, wenn diese im Code verwendet wird.
{ "WELCOME_MSG": "Willkommen auf deiner ersten mehrsprachigen Angular2-App.", "PAGE_TWO_CONTENT": "Sie navigierten zu Punkt 2.", "PAGE_THREE_CONTENT": "Jetzt navigierten Sie zu Punkt 3.", "NAVIGATION": { "ONE": "Punkt 1", "TWO": "Punkt 2", "THREE": "Punkt 3" } } { "WELCOME_MSG": "Welcome to your first multilingual Angular2 app.", "PAGE_TWO_CONTENT": "You clicked on menu item number two.", "PAGE_THREE_CONTENT": "Now you clicked menu item number three.", "NAVIGATION": { "ONE": "1st item", "TWO": "2nd item", "THREE": "3rd item" } }
Um die Sprachfunktionalität tatsächlich anzuwenden, muss im TranslateService eine Sprache definiert werden. Da sich dies anwendungsweit auswirkt, kann die Übersetzungsfunktionalität direkt in die app.component eingebunden werden. Mittels der „use“-Funktion wird eine Sprache festgelegt. Kann diese nicht gefunden werden wird, die Default-Sprache angewandt, sofern festgelegt.
import { Component } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'app'; constructor(translate: TranslateService) { translate.setDefaultLang('en-GB'); translate.use('en-GB'); } }
Theme
Um die optischen Vorteile von Angular Material zu genießen muss man ein Theme definieren. Dazu kann man eines der vorgefertigten verwenden oder sein eigenes definieren. Es existieren:
- deeppurple-amber.css
- indigo-pink.css
- pink-bluegrey.css
- purple-green.css
Da später ein eigenes Theme definiert wird, ist es zunächst unerheblich, welches Theme gewählt wird. In der bereits vorhandenen styles.css wird also das Theme folgendermaßen hinzugefügt:
@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
Navigationsmenü erstellen
Als klassischer Aufbau einer App wird eine Kopfzeile mit Navigationsmenü hinzugefügt. Um die Navigation auch über die URL steuern zu können, bietet sich der in Angular integrierte Routingdienst an. Zunächst müssen also die Module für den Router und die Materialmodule für eine Toolbar sowie Tabs integriert werden. Außerdem können dem Routermodul direkt die Pfade hinzugefügt werden (Bei vielen Pfaden ist es ratsam, dies in einer Datei ausgelagert zu tun, welche sodann in das app.module importiert werden kann).
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClient, HttpClientModule } from '@angular/common/http'; import { RouterModule } from '@angular/router'; import { MatTabsModule, MatToolbarModule } from '@angular/material'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { AppComponent } from './app.component'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './assets/translation/', '.json'); } @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, BrowserAnimationsModule, HttpClientModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, useFactory: (HttpLoaderFactory), deps: [HttpClient] } }), RouterModule.forRoot([{ path: '', redirectTo: 'first', pathMatch: 'full' },{ path: 'first', component: ContentOneComponent },{ path: 'second', component: ContentTwoComponent },{ path: 'third', component: ContentThreeComponent }]), MatTabsModule, MatToolbarModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Leider gibt die Dokumentation von Angular Material nicht an, welches Modul für welche Komponente importiert werden muss. Da sich die Anzahl der Module jedoch in Grenzen hält, kann man durchaus im Import „Module“ eingeben und die von Intellisense vorgeschlagenen Einträge durchsuchen, bis man auf eines gestoßen ist, das nach dem zu verwendenden Modul benannt ist.
Dem Routermodul wird eine Liste an Objekten mitgegeben, die aus dem Unterpfad und der entsprechenden Komponente besteht, die an einer bestimmten Stelle dargestellt werden soll.
Komponenten und Views
Als erstes werden also entsprechende Komponenten und Views erzeugt: Eine Komponente für den Header, bestehend aus der Sprachauswahl und der Navigation, eine für die Navigation selbst und je eine für den Inhalt des Navigationspunktes. Letztere sind abgekapselt in einem eigenen Unterordner, da diese für gewöhnlich ebenfalls aus mehreren Komponenten bestehen können und daher leicht die Übersicht verloren gehen kann.
Zunächst wird also die View der app.component bearbeitet, sodass die Komponenten auch integriert werden.
<app-header></app-header> <router-outlet></router-outlet>
App-header wird der Selektor unserer Headerkomponente, router-outlet ist ein Element, das der Routing-Service verwendet, um die im RouterModule angegebene Komponente darzustellen.
Header
Die header.component.ts wird zunächst, wie folgt, umgesetzt.
import { Component } from '@angular/core';
@Component({ selector: 'app-header', templateUrl: '../views/header.view.html' }) export class HeaderComponent { }
In der Header-View wird sodann die Navigationskomponente in einer Material-Toolbar eingefügt.
<mat-toolbar> <app-nav></app-nav> </mat-toolbar>
Navigation
Auch die navigation.component.ts benötigt zunächst nicht mehr als eine Standardimplementierung.
import { Component } from '@angular/core'; @Component({ selector: 'app-nav', templateUrl: '../views/navigation.view.html' }) export class NavigationComponent { }
In der entsprechenden View wird dann das Navigationsmenü hinzugefügt.
<nav mat-tab-nav-bar> <a mat-tab-linkrouterLink="first" routerLinkActive #rla1="routerLinkActive" [active]="rla1.isActive">{{ "NAVIGATION.ONE" | translate }}</a> <a mat-tab-linkrouterLink="second" routerLinkActive #rla2="routerLinkActive" [active]="rla2.isActive">{{ "NAVIGATION.TWO" | translate }}</a> <a mat-tab-linkrouterLink="third" routerLinkActive #rla3="routerLinkActive" [active]="rla3.isActive">{{ "NAVIGATION.THREE" | translate }}</a> </nav>
Das routerLink-Attribut besteht aus dem Unterpfad, der dem Routermodul zugewiesen wurde.
Über die routerLinkActive-Direktive kann der isActive-State abgefragt werden. Die übliche Notation sieht dabei wie folgt aus:
routerLinkActive="active"
Sofern dieser Link nun aktiv wird, wird dem <a>-Element die css-Klasse active hinzugefügt. In diesem Fall soll aber die Animation der TabNavBar verwendet werden. Es wird also die routerLinkActive-Direktive mittels # einer Template-Variable zugewiesen. In der Zeile darunter wird dessen Property isActive an die active-Property des TabLinks angebunden. Hierbei muss für jeden Link eine eigene Variable definiert werden, da sich diese anderenfalls gegenseitig überschreiben.
Innerhalb der Links sind die ersten Übersetzungstags, die schlichtweg in die translate-Pipe gegeben und damit vom TranslateService übersetzt werden.
Content
Mehr als einen Text werden die Contentelemente in diesem Fall nicht anzeigen, der Einfachheit halber kann also das Template für die Komponente direkt in diese integriert werden.
import { Component } from '@angular/core'; @Component({ selector: 'app-content-one', template: '{{ "WELCOME_MSG" | translate }}' }) export class ContentOneComponent { }
import { Component } from '@angular/core'; @Component({ selector: 'app-content-two', template: '{{ "PAGE_TWO_CONTENT" | translate }}' }) export class ContentTwoComponent { } import { Component } from '@angular/core'; @Component({ selector: 'app-content-three', template: '{{ "PAGE_THREE_CONTENT" | translate }}' }) export class ContentThreeComponent { }
All diese Komponenten müssen nun noch zum Modul hinzugefügt werden.
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClient, HttpClientModule } from '@angular/common/http'; import { RouterModule } from '@angular/router'; import { MatTabsModule, MatToolbarModule } from '@angular/material'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { AppComponent } from './app.component'; import { HeaderComponent } from './components/header.component'; import { NavigationComponent } from './components/navigation.component'; import { ContentOneComponent } from './components/content/contentone.component'; import { ContentTwoComponent } from './components/content/contenttwo.component'; import { ContentThreeComponent } from './components/content/contentthree.component'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './assets/translation/', '.json'); } @NgModule({ declarations: [ AppComponent, HeaderComponent, NavigationComponent, ContentOneComponent, ContentTwoComponent, ContentThreeComponent ], imports: [ BrowserModule, BrowserAnimationsModule, HttpClientModule, RouterModule.forRoot([{ path: '', redirectTo: 'first', pathMatch: 'full' },{ path: 'first', component: ContentOneComponent },{ path: 'second', component: ContentTwoComponent },{ path: 'third', component: ContentThreeComponent }]), TranslateModule.forRoot({ loader: { provide: TranslateLoader, useFactory: (HttpLoaderFactory), deps: [HttpClient] } }), MatTabsModule, MatToolbarModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Führt man nun im Visual Studio Code Terminal folgenden Befehl aus
ng serve
und wechselt dann im Browser auf http://localhost:4200 sollte folgendes Bild erscheinen:
Animation hinzufügen
Der erste Test hat leider gezeigt, dass die slide-Navigation im Content-Bereich fehlt. Diese ist bei einer normalen Material Tab-Group automatisch vorhanden, verwendet man jedoch Routing, um den Inhalt zu wechseln, fehlt diese. Da sich ein Wechsel mittels Navigation jedoch natürlicher anfühlt und sich die Anwendung dann nahtloser in die Erfahrung einer mobilen App einfügt, wird diese manuell hinzugefügt.
Zunächst wird also eine Animationsdatei erstellt.
Diese wird mit folgendem Code befüllt.
import {trigger, animate, style, group, query, transition} from '@angular/animations'; export const routerTransition = trigger('routerTransition', [ transition('* => *', [ query(':enter, :leave', style({ position: 'fixed', width:'100%' }), { optional: true }), group([ query(':enter', [ style({ transform: 'translateX(100%)' }), animate('0.5s ease-in-out', style({ transform: 'translateX(0%)' })) ], { optional: true }), query(':leave', [ style({ transform: 'translateX(0%)' }), animate('0.5s ease-in-out', style({ transform: 'translateX(-100%)' })) ], { optional: true }), ]) ]) ])
Für eine Animation wird zunächst ein Trigger definiert, dem dann mehrere Transitions mitgegeben werden können. Da nur eine Animation Anwendung findet, kann die Transition schlichtweg als * => * definiert werden, was bedeutet, dass diese Animation bei jeder Statusänderung stattfindet. Die erste Query definiert einen Style, der dem animierten Element sowohl bei der Eintritts- als auch Austrittsanimation angefügt wird, also vom vorherigen Status zum gewünschten Status und vom gewünschten Status zum nächsten. Es finden also stets zwei Animationen gleichzeitig statt. Innerhalb der folgenden Gruppe werden sodann die eigentlichen Animationen definiert, die sowohl einen initialen Stil als auch die Animationsfunktion mit einem Übergang sowie einem resultierenden Stil enthalten. Dies sind alles css-Animationen.
Für jede Route wird ein State benötigt, dessen Wechsel sich abfragen lässt. Dieser wird der Route (die im Modul definiert wurde) hinzugefügt.
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClient, HttpClientModule } from '@angular/common/http'; import { RouterModule } from '@angular/router'; import { MatTabsModule, MatToolbarModule } from '@angular/material'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { AppComponent } from './app.component'; import { HeaderComponent } from './components/header.component'; import { NavigationComponent } from './components/navigation.component'; import { ContentOneComponent } from './components/content/contentone.component'; import { ContentTwoComponent } from './components/content/contenttwo.component'; import { ContentThreeComponent } from './components/content/contentthree.component'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './assets/translation/', '.json'); } @NgModule({ declarations: [ AppComponent, HeaderComponent, NavigationComponent, ContentOneComponent, ContentTwoComponent, ContentThreeComponent ], imports: [ BrowserModule, BrowserAnimationsModule, HttpClientModule, RouterModule.forRoot([{ path: '', redirectTo: 'first', pathMatch: 'full' },{ path: 'first', component: ContentOneComponent, data: { state: 'first' } },{ path: 'second', component: ContentTwoComponent, data: { state: 'second' } },{ path: 'third', component: ContentThreeComponent, data: { state: 'third' } }]), TranslateModule.forRoot({ loader: { provide: TranslateLoader, useFactory: (HttpLoaderFactory), deps: [HttpClient] } }), MatTabsModule, MatToolbarModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Nun muss die Animation der entsprechenden Komponente hinzugefügt werden. Da sich das router-outlet in der app.component befindet, wird dort die Animation hinzugefügt. Außerdem muss der Status der aktiven Route abgefragt werden können.
import { Component } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { routerTransition } from './animations/routertransition.animation'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], animations: [ routerTransition ] }) export class AppComponent { title = 'app'; constructor(translate: TranslateService) { translate.setDefaultLang('en-GB'); translate.use('en-GB'); } getState(outlet) { return outlet.activatedRouteData.state; } }
In der Html wird der Transition der aktuelle State hinzugefügt, indem die getState-Funktion aufgerufen wird.
<app-header></app-header> <main [@routerTransition]="getState(outlet)"> <router-outlet #outlet="outlet"></router-outlet> </main>
Ein erneuter Test zeigt nun eine Slide-Animation bei Navigationswechsel.
Sprachauswahl
Für die Sprachauswahl wird ein Icon-Button und ein Menü benötigt, somit müssen folgende Module hinzugefügt werden.
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClient, HttpClientModule } from '@angular/common/http'; import { RouterModule } from '@angular/router'; import { MatTabsModule, MatToolbarModule, MatIconModule, MatMenuModule, MatButtonModule } from '@angular/material'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { AppComponent } from './app.component'; import { HeaderComponent } from './components/header.component'; import { NavigationComponent } from './components/navigation.component'; import { ContentOneComponent } from './components/content/contentone.component'; import { ContentTwoComponent } from './components/content/contenttwo.component'; import { ContentThreeComponent } from './components/content/contentthree.component'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './assets/translation/', '.json'); } @NgModule({ declarations: [ AppComponent, HeaderComponent, NavigationComponent, ContentOneComponent, ContentTwoComponent, ContentThreeComponent ], imports: [ BrowserModule, BrowserAnimationsModule, HttpClientModule, RouterModule.forRoot([{ path: '', redirectTo: 'first', pathMatch: 'full' },{ path: 'first', component: ContentOneComponent, data: { state: 'first' } },{ path: 'second', component: ContentTwoComponent, data: { state: 'second' } },{ path: 'third', component: ContentThreeComponent, data: { state: 'third' } }]), TranslateModule.forRoot({ loader: { provide: TranslateLoader, useFactory: (HttpLoaderFactory), deps: [HttpClient] } }), MatTabsModule, MatToolbarModule, MatIconModule, MatMenuModule, MatButtonModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Auch wenn letztlich custom-Icons verwendet werden, muss der für Icons benötigte Icon-Font in der index.html hinzugefügt werden.
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Angular2MultilingualMaterialistic2</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> </head> <body> <app-root></app-root> </body> </html>
Danach wird das Menü neben der Navigation in die header.view.html eingefügt.
<mat-toolbar> <app-nav></app-nav> <mat-menu #languageMenu="matMenu"> <button mat-menu-item (click)="setLanguage('de-DE')">de-DE</button> <button mat-menu-item (click)="setLanguage('en-GB')">en-GB</button> </mat-menu> <button mat-icon-button [matMenuTriggerFor]="languageMenu"> <mat-icon>more_vert</mat-icon> </button> </mat-toolbar>
Das Menü an sich wird nirgends angezeigt und findet nur in Zusammenhang mit einem Button, der einen Menü-Trigger enthält, Verwendung. Das Menü selbst besteht aus einer Reihe von Buttons, deren Klick-Event an eine Funktion angebunden wird; in diesem Fall eine, die die entsprechende Sprache setzt.
Diese Funktion wird also demnach in der header-Komponente benötigt. Dafür braucht man den TranslateService, der im Konstruktor hinzugefügt und in der setLanguage-Methode verwendet wird.
import { Component } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'app-header', templateUrl: '../views/header.view.html' }) export class HeaderComponent { private translateService: TranslateService; constructor(translateService: TranslateService) { this.translateService = translateService; } setLanguage(languageName: string): void { this.translateService.use(languageName); } }
Ein weiterer Test zeigt folgendes Bild:
Custom Icons
Wie bei einer Sprachauswahl üblich sollte die derzeit ausgewählte Sprache im Menü erkennbar sein. Hierfür wird die Icon-Registry benötigt, die dem Modul als Provider hinzugefügt werden muss.
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClient, HttpClientModule } from '@angular/common/http'; import { RouterModule } from '@angular/router'; import { MatTabsModule, MatToolbarModule, MatIconModule, MatIconRegistry, MatMenuModule, MatButtonModule } from '@angular/material'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { AppComponent } from './app.component'; import { HeaderComponent } from './components/header.component'; import { NavigationComponent } from './components/navigation.component'; import { ContentOneComponent } from './components/content/contentone.component'; import { ContentTwoComponent } from './components/content/contenttwo.component'; import { ContentThreeComponent } from './components/content/contentthree.component'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './assets/translation/', '.json'); } @NgModule({ declarations: [ AppComponent, HeaderComponent, NavigationComponent, ContentOneComponent, ContentTwoComponent, ContentThreeComponent ], imports: [ BrowserModule, BrowserAnimationsModule, HttpClientModule, RouterModule.forRoot([{ path: '', redirectTo: 'first', pathMatch: 'full' },{ path: 'first', component: ContentOneComponent, data: { state: 'first' } },{ path: 'second', component: ContentTwoComponent, data: { state: 'second' } },{ path: 'third', component: ContentThreeComponent, data: { state: 'third' } }]), TranslateModule.forRoot({ loader: { provide: TranslateLoader, useFactory: (HttpLoaderFactory), deps: [HttpClient] } }), MatTabsModule, MatToolbarModule, MatIconModule, MatMenuModule, MatButtonModule ], providers: [MatIconRegistry], bootstrap: [AppComponent] }) export class AppModule { }
Sodann müssen die neuen Icons hinzugefügt und registriert werden. Da auch diese anwendungsweit angewendet werden können, wird dies in der app.component durchgeführt.
Die svg-Icons habe ich selbst erstellt. Sie befinden sich im Projekt am Ende dieses Blogposts.
import { Component } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { DomSanitizer } from '@angular/platform-browser'; import { MatIcon, MatIconRegistry } from '@angular/material/icon'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] , animations: [ routerTransition ] }) export class AppComponent { title = 'app'; constructor(translate: TranslateService, matIconRegistry: MatIconRegistry, domSanitizer: DomSanitizer) { translate.setDefaultLang('en-GB'); translate.use('en-GB'); matIconRegistry.addSvgIcon('de-DE', domSanitizer.bypassSecurityTrustResourceUrl('./assets/flags/de-DE.svg')); matIconRegistry.addSvgIcon('en-GB', domSanitizer.bypassSecurityTrustResourceUrl('./assets/flags/en-GB.svg')); } }
Über die Icon-Registry können svg-Dateien als Icon registriert werden. DomSanitizer ist ein Werkzeug, das Cross-Site-Scripting (XSS) verhindert, indem es schadhaften Text entfernt oder das Laden der Ressource komplett verhindert. Um dies zu umgehen (!!!!????), kann der Sanitizer angewiesen werden, dieser Ressource zu vertrauen.
Da die derzeit ausgewählte Sprache im Menü angezeigt werden soll, muss für dieses ein Property erstellt werden, das gesetzt wird, wenn der Benutzer eine Sprache auswählt und an das der Menü-Button gebunden wird.
import { Component } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'app-header', templateUrl: '../views/header.view.html' }) export class HeaderComponent { private translateService: TranslateService; private currentLanguage: string; constructor(translateService: TranslateService) { this.translateService = translateService; this.currentLanguage = 'en-GB'; } setLanguage(languageName: string): void { this.translateService.use(languageName); this.currentLanguage = languageName; } }
Nun müssen die Custom-Icons nur noch der Menüauswahl hinzugefügt werden.
<mat-toolbar> <app-nav></app-nav> <mat-menu #languageMenu="matMenu"> <button mat-menu-item (click)="setLanguage('de-DE')"><mat-icon svgIcon="de-DE"></mat-icon><span>de-DE</span></button> <button mat-menu-item (click)="setLanguage('en-GB')"><mat-icon svgIcon="en-GB"></mat-icon><span>en-GB</span></button> </mat-menu> <button mat-icon-button [matMenuTriggerFor]="languageMenu"> <mat-icon [svgIcon]="currentLanguage"></mat-icon> </button> </mat-toolbar>
Custom Theme
Will man keines der vorgefertigten Themes verwenden, so kann man sich sein eigenes definieren. Im einfachsten Fall besteht dies lediglich aus veränderten Farben.
Dafür muss eine sogenannte Palette definiert werden. Dies ist eine Sammlung verschiedener Schattierungen einer einzelnen Farbe, wobei in Zahlen deren Helligkeit angegeben wird. Die Farben mit vorangestelltem „A“ stellen vier helle Farbtöne mit stark erhöhter Sättigung dar. Im „contrast“-Bereich befinden sich die jeweiligen Farben für etwaigen Text vor dem entsprechenden Hintergrund; in der Regel ist dies Schwarz oder Anthrazit für helle Hintergrundfarben und Weiß für dunkle. Auf dieser Website können Paletten mittels Auswahl eines Farbtons automatisch erstellt werden: http://mcg.mbitson.com/
Angular Material verwendet verschiedene Paletten:
- primary: Farben dieser Palette werden von den meisten Komponenten großflächig verwendet.
- accent: Diese Farben werden für floating-Buttons und interaktive Elemente verwendet.
- warn: Farben zur Darstellung von fehlerhaften Status.
- foreground: Farben für Text und Icons.
- background: Farben für Hintergründe.
Benötigt werden nur primary und accent, die anderen Paletten werden auf einen Default-Wert gesetzt.
Zunächst wird also im src-Ordner eine theme.scss erstellt.
In dieser muss sodann das Angular2 Theming importiert sowie mat-core() inkludiert werden. Mittels der mat-palette-Funktion wird aus einer Farbliste eine Palette erstellt. Zwei oder mehr Paletten können dann zur Erstellung einer mat-light-theme oder mat-dark-theme verwendet werden. Zu guter Letzt wird dieses erstellte Theme mittels angular-material-theme angewandt.
@import '~@angular/material/theming'; @include mat-core(); $grey: ( 50 : #fdfdfd, 100 : #f9f9f9, 200 : #f5f5f5, 300 : #f1f1f1, 400 : #eeeeee, 500 : #ebebeb, 600 : #e9e9e9, 700 : #e5e5e5, 800 : #e2e2e2, 900 : #dddddd, A100 : #ffffff, A200 : #ffffff, A400 : #ffffff, A700 : #ffffff, contrast: ( 50 : #000000, 100 : #000000, 200 : #000000, 300 : #000000, 400 : #000000, 500 : #000000, 600 : #000000, 700 : #000000, 800 : #000000, 900 : #000000, A100 : #000000, A200 : #000000, A400 : #000000, A700 : #000000, ) ); $green: ( 50 : #fafde0, 100 : #f3fab3, 200 : #ebf780, 300 : #e2f34d, 400 : #dcf126, 500 : #d6ee00, 600 : #d1ec00, 700 : #cce900, 800 : #c6e700, 900 : #bce200, A100 : #ffffff, A200 : #f7ffd6, A400 : #edffa3, A700 : #e8ff8a, contrast: ( 50 : #000000, 100 : #000000, 200 : #000000, 300 : #000000, 400 : #000000, 500 : #000000, 600 : #000000, 700 : #000000, 800 : #000000, 900 : #000000, A100 : #000000, A200 : #000000, A400 : #000000, A700 : #000000, ) ); $red: ( 50 : #f9e0e0, 100 : #f0b3b3, 200 : #e68080, 300 : #db4d4d, 400 : #d42626, 500 : #cc0000, 600 : #c70000, 700 : #c00000, 800 : #b90000, 900 : #ad0000, A100 : #ffd7d7, A200 : #ffa4a4, A400 : #ff7171, A700 : #ff5858, contrast: ( 50 : #000000, 100 : #000000, 200 : #000000, 300 : #000000, 400 : #000000, 500 : #000000, 600 : #000000, 700 : #000000, 800 : #000000, 900 : #000000, A100 : #000000, A200 : #000000, A400 : #000000, A700 : #000000, ) ); $palette-primary: mat-palette($green); $palette-accent: mat-palette($grey); $palette-warn: mat-palette($red); $custom-theme: mat-light-theme($palette-primary, $palette-accent, $palette-warn); @include angular-material-theme($custom-theme);
Damit dieses Theme tatsächlich Anwendung findet, muss es in die Datei angular-cli.json eingefügt werden.
{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "project": { "name": "angular2-multilingual-materialistic" }, "apps": [ { "root": "src", "outDir": "dist", "assets": [ "assets", "favicon.ico" ], "index": "index.html", "main": "main.ts", "polyfills": "polyfills.ts", "test": "test.ts", "tsconfig": "tsconfig.app.json", "testTsconfig": "tsconfig.spec.json", "prefix": "app", "styles": [ "styles.css", "theme.scss" ], "scripts": [], "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" } } ], "e2e": { "protractor": { "config": "./protractor.conf.js" } }, "lint": [ { "project": "src/tsconfig.app.json", "exclude": "**/node_modules/**" }, { "project": "src/tsconfig.spec.json", "exclude": "**/node_modules/**" }, { "project": "e2e/tsconfig.e2e.json", "exclude": "**/node_modules/**" } ], "test": { "karma": { "config": "./karma.conf.js" } }, "defaults": { "styleExt": "css", "component": {} } }
Falls das neue Theme nicht übernommen wird, muss der ng serve Vorgang abgebrochen (CTRL + C im Visual Studio Code Terminal) und neu gestartet werden.
Die finale Anwendung sieht folgendermaßen aus:
Im letztlichen Projekt befinden sich noch einige css-Klassen, die der Anwendung einen saubereren initialen Start verpassen, wie etwa Fußzeile, Sprachauswahl rechts, Contentbereichgröße und -text und die zu Anfang angesprochene Höhenplatzierung der Toolbar-Materialien.
Wer möchte, kann sich das Projekt gerne downloaden! Viel Spaß und Erfolg damit!
Angular2MultilangualMaterialistic.zip
Du bist Softwareentwickler:in? Unser agiles Team sucht nach dir? Hier geht's zu unserer Jobbörse.
Ja, ich möchte zur lise Family gehören!