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

Werbeanzeigen

[deprecated]Angular 2 – Konverter ($parsers and $formatters)

Der folgende Abschnitt ist veraltet!In der aktuellen Version ist folgendes Vorgehen zu verwenden:

https://javaeeblog.wordpress.com/?p=4409

Angular 2 steht in den Startlöchern bereit, wenn auch erst in einer frühen Beta-Phase. Zeit genug also sich mit der neuen Welt von Anuglar JS vertraut zu machen. Bei den ersten Schritten stolpert man sehr schnell über die Frage: Wie findet eine Konvertierung von Daten zwischen View und Modell statt? In Angular1 standen uns hierfür $formatters und $parsers zur Verfügung, in Angular2 gibt es dazu bisher kein offizielles Konzept (siehe GitHub ). Aber werfen wir einen Blick auf die Möglichkeiten die sich uns heute schon bieten…

 

Das typische Binding einer Eingabe Komponente in Angular 2 sieht bekanntlich so aus:

<input id="datefield" [(ngModel)]="recordService.record.startTime" />

Durch die doppelte Klammerung („[(….)]“, alternativ: „onbind-ngModel“) zusammen mit der „ngModel“-Direktive führen wir an dieser Stelle ein Two-Way-Binding ein mit dessen Hilfe die Werte unseres Models in der View angezeigt und Änderungen zurück gespielt werden. Nutzen wir dieses Vorgehen auch für ein Datenfeld vom Typ „Date“ führt dies dazu, dass wir in der Anzeige die unansehnliche String Repräsentation eines Date-Objektes finden, das bei Änderungen zu allem Überfluss auch noch als String zurück ins Model geschrieben wird. Ein Converter / Parser muss also her. $parsers und $formatters aus Angular 1 haben (bisher?) ihren Weg noch nicht in den Version 2. geschafft, dennoch gibt es mit den bestehenden Mittel die Möglichkeit eine solche Funktion zu implementieren: eine eigene Direktive:

(hier einmal die gesamte Implementierung in TypeScript, im Folgenden werde wir auf die einzelnen Punkte eingehen)

import {Directive, Renderer, ElementRef} from 'angular2/core'
import {NgModel, DefaultValueAccessor} from 'angular2/common'

@Directive({
selector: '[tsdate]',
host: {
    '(change)': 'onChanges($event.target.value)'
}
})
export class DateDirective extends DefaultValueAccessor {

constructor(private model: NgModel, private el: ElementRef, private renderer: Renderer) {
    super(renderer, el);
    model.valueAccessor = this;
}

writeValue(obj: Date): void {
    if (obj) {
        super.writeValue(this.formatDate(obj));
    }
}

onChanges(value: string) {
    this.onChange(this.parseDate(value));
}

formatDate(inputDate: Date): string {
    // Datum formatieren
    return ...
}

parseDate(inputString: string): Date {
 // Datum parsen
 return ...Date...
}
}

Mittels des TypeScript Decorators „Directive“ erzeugen wir hier eine Direktive die später an HTML Elemente angehängt werden kann. Über welche Property dies geschieht legen wir durch das selector Attribut fest: selector: ‚[tsdate]‘, Verwendung: . Als weiteren Schritt wollen wir geänderte Eingaben des Textfeldes auf dem die Direktive verwendet werden soll mit einer entsprechenden Methode versehen um darauf zu reagieren. Mit Hilfe des „host“ Attributes registrieren wir uns auf das entsprechende „change“-Event des übergeordneten HTML Elements (in unserem Fall: „input“) um zusammen mit dem Wert des Textfeldes als Parameter eine unserer Methoden auf zu rufen.

Die eigentliche Implementierung unserer Logik findet in der anschließend deklarierten Klasse statt:

export class DateDirective extends DefaultValueAccessor

Dabei erben wir von der Angular zur Verfügung gestellten Klasse: DefaultValueAccessor (implements ControlValueAccessor). Diese Klasse wird von der NgModel-Direktive verwendet um Änderungen ins Model zurück zu schreiben und auf Eingaben des Benutzers zu reagieren.

constructor(private model: NgModel, private el: ElementRef, private renderer: Renderer) {
    super(renderer, el);
    model.valueAccessor = this;
}

Innerhalb des Konstruktors lassen wir uns unter anderem das NgModel injizieren mit dessen Hilfe wir das Ein- und Ausgabeverhalten der Direktive anpassen werden. Dazu ändern wir den valueAccessor auf die aktuelle Klasse um die von uns benötigten Methoden zu überschreiben und nach Bedarf an zu passen.

Die beiden entscheidenen Methoden die wir  dazu nutzen sind:

writeValue(obj: Date): void

Die „writeValue“ Methode wird aufgerufen um einen Wert aus dem Model in die View zu übertragen. An dieser Stelle greifen wir ein und nehmen das Date-Objekt aus dem Model um es in eine für uns angenehme String-Repräsentation zu bringen. Das eigentliche setzen des value-Attributes des Textfeldes überlassen wir dann der Standard Implementierung.

onChanges(value: string)

Wie in der Konfiguration angegeben wird diese Methode bei Änderungen des Textfeldes aufgerufen und mit dem geänderten Wert versorgt. An dieser Stelle führen wir die Umwandlung des Strings in ein echtes JavaScript Datum durch, um zum Abschluss erneut die Standard Implementierung dafür zu bemühen diesen Wert ans Model zu übertragen

Wie wir sehen konnten besteht auch mit den in der Beta-Version zur Verfügung gestellten Mitteln eine recht übersichtliche Möglichkeit einen Konverter/Parser zu implementieren. Ob dieses Vorgehen in dieser Form auch in Zukunft Verwendung findet oder es eine ergänzendes Konzept in Angular 2 geben wird muss sich zeigen.

Das Beispiel findet sich noch einmal hier in seiner gesamten Form: github

AngularJS i18n – Teil 2 – globalize

Im ersten Teil des Beitrags wurde ein Blick auf „angular-translate“ geworfen, das wohl meist genutzte Modul um AngularJS Anwendungen multilingual zu entwickeln. In diesem Beitrag dreht sich alles um die Formatierung und das Parsen von länderspezifischen Formaten. Eine weit verbreitete Bibliothek für diese Aufgabe ist:  

Globalize

Globalize stammt aus dem jQuery-Universum und kümmert sich in den verschiedensten Bereichen um die Internationalisierung von Ein- und Ausgaben. Aktuell liegt die Version 1.0.0 vor die sich maßgeblich von den Vorgängerversionen unterscheidet. Globalize in der Version 0.x nutzte noch ein eigenes JSON Format um seine Formatierungs-Regeln zu definieren, wohingegen in der aktuellsten Version CLRD (Unicode Common Locale Data Repository, http://cldr.unicode.org/)  als Basis genutzt wird. Damit besteht die vollständige Einbindung von Globalize aus zwei Bereichen: Globalize selbst (modulare JavaScript-Dateien) und CLRD Format-Definitionen (JSON Dateien)
Basis für das folgende Beispiel ist ein simples AngularJS Projekt das mit Hilfe von grunt konfiguriert und gebaut wird. Zu Beginn werden wir jedoch verstärkt auf die konkreten Schritte eingehen die nötig sind um Globalize zu nutzen. Ein vollständiges Beispiel mit grunt folgt am Ende.

Installation, „Bower und los?“

Bower als Packet Manager macht es jedem Entwickler einfach neue Abhängigkeiten zu installieren. In diesem Fall reicht ein einfaches „install“ aber leider nicht aus. Wie anfangs bereits erwähnt greift Globalize auf ein zentrales Repository zu welches die benötigten Formatierungsregeln anbietet. Diese, im JSON-Format vorliegenden, Informationen können jedoch nicht direkt über Bower bezogen werden sondern müssen separat installiert werden. Das muss zum Glück niemand von Hand erledigen. Um das automatische Herunterladen der Informationen zu aktivieren, registrieren wir zwei entsprechende Scripte in unserer Bower-Konfiguration („.bowerrc“) :

  "scripts": {
      "preinstall": "npm install cldr-data-downloader@0.2.x",
      "postinstall": "node ./node_modules/cldr-data-downloader/bin/download.js -i public_html/bower_components/cldr-data/index.json -o public_html/bower_components/cldr-data/"
  }

Es folgt die eigentliche Installation der Pakete mit:

bower install globalize --save

(Wichtig: die oben registrierten Skripte werden wirklich nur ausgeführt wenn eine Installation über Bower stattgefunden hat. Sollten die Abhängigkeiten zu Globalize bereits vorhanden gewesen sein werden die Skripte nicht durchlaufen. Die beiden Schritte können dann natürlich auch manuell durchgeführt werden. Nach einer erfolgreichen Installation sollten die „*.json“ Dateien im Angegebenen Export Ordner vorliegen: public_html/bower_components/cldr-data/)

Als nächstes folgt die Einbindung in unsere HTML Seite:

 <script src="bower_components/cldrjs/dist/cldr.js">
 <script src="bower_components/cldrjs/dist/cldr/event.js">
  
 <script src="bower_components/globalize/dist/globalize.js">
 <script src="bower_components/globalize/dist/globalize/number.js">
 <script src="bower_components/globalize/dist/globalize/date.js">

Zuerst werden die benötigten CLDR-Bibliotheken referenziert (um später die Definitionen zu laden). Dann folgen die Globalize-Module die genutzt werden sollen. Folgende Module stehen derzeit zur Verfügung (Referenz: https://github.com/jquery/globalize#pick-the-modules-you-need )

  • globalize, Core Bibliothek
  • globalize/currency, Formatieren von Währungen
  • globalize/date, Formatieren von Kalenderdaten
  • globalize/message, Übersetzen von Texten
  • globalize/number, Formatieren von Zahlen
  • globalize/plural, Übersetzung von Zahlen
  • globalize/relative-time, Übersetzungen für relative Zeitangaben
  • globalize/units, Formatieren von Einheiten

Damit haben wir nun die Funktionen um Globalize zu nutzen. Was nun noch fehlt ist das Laden der benötigen Daten. Je nachdem welche Funktionen verwendet werden sollen, müssen die entsprechenden CLRD-Daten geladen werden: https://github.com/jquery/globalize/blob/master/README.md#2-cldr-content

Zu diesem Zweck existiert die Funktion:

Globalize.load(...);

die als Parameter die JSON-Informationen entgegen nimmt.
Das Laden der Definitionen könnte in AgularJS so aussehen:

var language=‘de‘;  
var clrdRef=[
    'bower_components/cldr-data/supplemental/likelySubtags.json',
    'bower_components/cldr-data/supplemental/numberingSystems.json',
    'bower_components/cldr-data/supplemental/timeData.json',
    'bower_components/cldr-data/supplemental/weekData.json',
    'bower_components/cldr-data/main/'+language+'/numbers.json',
    'bower_components/cldr-data/main/'+language+'/ca-gregorian.json',
    'bower_components/cldr-data/main/'+language+'/timeZoneNames.json'                ];

clrdRef.forEach(function(url){
   $http.get(url).then(function(resp) {
       Globalize.load(resp.data);
   });
});

Das Laden dieser Konfigurationen für Globalize sollte natürlich vor der Verwendung stattfinden. Da es sich bei den Zugriffen um asynchrone http-Requests handelt macht es uns AngularJS leider nicht leicht. Würden wir diese Initialisierung in der run-Methode unserer Anwendung ausführen wird es vorkommen das die Requests noch nicht beendet sind, Globalize noch nicht initialisiert ist und Formatierungsaufgaben durch Globalize mit einem Fehler quittiert werden. Um dieses Problem zu umgehen gibt es zwei Möglichkeiten:

  • manuelles Boostraping unserer Anwendung
  • Einsatz von UI-Router und die Verwendung des „resolve“ Attributes

Das Module „ui-router“ welches sich um die Navigation innerhalb unserer Anwendung kümmert bietet eine Möglichkeit die in der View verwendeten Controller mit Daten zu versorgen. Das hat den Vorteil das wir unsere Controller nicht mit einer Vielzahl von asyncronen Requests füllen müssen. Ein typischer Anwendungsfall ist z.B. für eine Seite die Daten in tabelarischer Form anzeigt, diese bereits vor dem Aufruf unserer Controllers zu laden und per Injection zur Verfügung zu stellen. Der Vorteil besteht darin das die Navigation erst nach vollständigem Laden der  asyncronen Aufgaben (promise) ausgeführt wird. Wir müssen nun nur sicher stellen das ein solches resolve bei jedem unserer Naviagationen ausgeführt wird. Zu diesem zweck nutzen wir die Möglichkeit von „ui-route“ mehrstufige Templates zu deklarieren. (weitere Details über ui-route gibt es hier: https://github.com/angular-ui/ui-router )

  
  .config(function ($stateProvider, $urlRouterProvider) {  
    $stateProvider
            .state('app', {
                abstract: true,
                template: '< div ui-view></div>',
                resolve: {
                    _init_: function ($window, GlobalizeLanguageLoader) {
                        var lang = $window.navigator.language || $window.navigator.userLanguage;   
                        return GlobalizeLanguageLoader.loadLanguage(lang);
                    }
                }
            })
            .state('app.home', {
                url: '/home',
                templateUrl: 'modules/home/home.html'
            })

Einige Dinge fallen auf:

  • ein als „abstrakt“ gekennzeichneter State
    • damit stellen wir lediglich sicher das dieser State nicht direkt aufgerufen werden kann
  • eine Template Definition:    template: < div ui-view></div>
    • streng genommen bilden wir ein mehrstufiges Template. Unsere index.html enthält eine Direktive „ui-view“ dessen Inhalt  mit den konkreten Seiten gefüllt wird. In dem Fall des States „app“ füllen wir diesen Bereich lediglich mit einer weiteren ui-view-Direktive die dann durch die konkreten Navigationen den eigentlichen Inhalt anzeigt.
  • „resolve“ innerhalb unseres „app-state“
    • dieser Resolve wird für jeden abhängigen State ausgeführt
    • ! der deklarierte Service wird bei jeder Navigation ausgeführt. Aus dem Grund sollten wir sicher stellen das wir die Initialisierung von Globalize nur initial oder nach einer Änderung der Sprache durchgeführen.
  • Alle weiteren Seiten werden als Abhängige Zustände deklariert „app.[state-name]“
  • die eigentliche Konfiguration von Globalize haben wir in diesem Beispiel in einen Service ausgelagert (Implementierung ähnlich wie oben zu sehen)

Mit diesem Vorgehen stellen wir sicher das Globalize, unabhängig von der aufgerufenen „Seite“, initialisiert ist bevor die Anwendung selbst ausgeführt wird.

Kann es jetzt endlich los gehen?

Die eigentlichen Formatierungen sind, wer hätte es gedacht, dann sehr einfach. Globalize bietet diverse Funktionen an um Daten zu formatieren und zu parsen, hier nur einige Beispiele:

  var g = new Globalize(language);
  vm.number = g.numberFormatter()(10000.88);
  vm.numberRounded = g.numberFormatter({round: "ceil", maximumFractionDigits: 0})(10000.88);
  vm.date = g.dateFormatter()(new Date());
  vm.dateLong = g.dateFormatter({datetime: "long"})(new Date());

Es liegt nahe eigene Direktiven zu entwickeln welche diese Aufgaben übernehmen. Für ein Datum könnte eine solche Direkte so aussehen:

  .directive("glDate", function (GlobalizeLanguageLoader) {
      return {
          restrict: 'A',
          require: 'ngModel',
          link: function (scope, element, attr, ngModel)
          {
              ngModel.$formatters.push(function (value) {
                  if (value) {
                      return GlobalizeLanguageLoader.get().dateFormatter({date: "short"})(value);
                  }
              });  
              ngModel.$parsers.push(function (value) {
                  return GlobalizeLanguageLoader.get().dateParser({date: "short"})(value);
              });  
          }
      };
  });

(in diesem Beispiel wird erneut unsere Factory eingesetzt. Diese enthält wie zu sehen, neben der Initialisierung von Globalize, auch eine Instanz von Globalie die bereits mit einer Sprache instanziiert wurde (new Globalize(‚de‘)).

Zugegebenermaßen ist die Konfiguration von globalize in der Version 1.X nicht ganz unkompliziert. Wenn die anfänglichen Hürden aber erst einmal gemeistert sind stützen wir unsere Anwendung auf eine tausendfach bewerte Internationalisierungs-Datenbank. Bei der eigentlichen Integration in AngularJS ist lediglich die strikte asyncrone Verarbeitung noch eine kleine Hürde bevor es dank Parser und Formatter an die eigenen AngularJS Direktiven geht.

Hier gibt es das Projekt noch mal komplett…

 

AngularJS i18n – Teil 1 – angular-translate

Ein häufiges Thema in unseren Anwendungen ist die Internationalisierung der Oberfläche . Dabei geht es nicht ausschließlich um die Übersetzung von Labels und Texten sondern auch um das länderspezifische Anzeigen und Entgegennehmen von Zahlen und Daten. Für AngularJS bietet sich das Modul „angular-translate“ für Übersetzungen und die jQuery Bibliothek „globalize“ für das Formatieren von Eingaben an. In diesem Betrag werfen wir einen genaueren Blick auf:

angular-translate

Das Modul bietet für die Übersetzung von Texten entsprechende Filter, Direktiven und einen Service. Diese Übersetzungen basieren auf einem JSON Format welches auch verschachtelte Strukturen erlaubt (s. unten).
„angular-translate“ gliedert sich in mehrere einzelne Pakete, die je nach benötigten Funktionen installiert und verwendet werden können

  • angular-translate
    • Core Packet
  • angular-translate-loader-static-files
    • ermöglich das Laden der Übersetzung auf Basis von Dateien
  • angular-translate-loader-url
    • ermöglicht das Laden der Übersetzungen auf Basis einer URL
  • angular-translate-loader-partial
    • ermöglich das Laden von Teil-Übersetzungen

Nachdem die gewünschten Pakete installiert und die entsprechenden JavaScript Referenzen gesetzt sind besteht der zweite Schritt darin die Übersetzungen zu konfigurieren. Dies geschieht in aller Regel in der Config-Methode unserer Anwendung.

1.    Abhängigkeiten definieren:

angular.module("AngularJSi18n", ['pascalprecht.translate'])

2.    Config Methode erweitern (mit Zugriff auf den TranslateProvider)

.config(function ($translateProvider) {

3.    Registrierung der Übersetzungen auf Basis der gewünschten Methode:

–    Direkt in unserer Anwendung:

Für größere Anwendung wird man diesen Fall vermeiden

  $translateProvider.translations('de', {
      TITLE: "i18n mit AngularJS",
      MENU: {
          HOME: 'Startseite'
      }
  });

–    Über einzelne Dateien (angular-translate-loader-static-files wird benötigt):

Werden die Übersetzungen ausschließlich in der AngularJS Anwendung verwendet und wird keinerlei dynamische Erstellung benötigt.

  $translateProvider.useStaticFilesLoader({
      prefix: 'resources/i18n/',
      suffix: '.json'
  });

= für jede Lokalisierung (z.B. „de_DE“) wird eine entsprechende Datei im angegebenen Pfad gesucht. In unserem Beispiel für Deutsch: resources/i18n/de_DE.json
Diese Datei sollte entsprechende Werte im JSON-Format bereit halten:

{
"TITLE": "i18n mit AngularJS",
     „MENU“  {
            "HOME": "Startseite"
        }
}
–    über einen Backend-Service (angular-translate-loader-url wird benötigt)

Flexibelste Variante. Denkbar wäre hier eine Java EE Rest Schnittstelle die Java .properties Dateien in das entsprechende JSON Format umwandelt. Damit können dieselben Übersetzungen für das Backend oder weitere Anwendungen verwendet werden wie für unsere AngularJS Applikation.

$translateProvider.useUrlLoader("/webresources/i18nRestService");

= für jede Sprache wird die angegebene URL aufgerufen und um den Parameter „lang“ ergänzt. In unserem Beispiel für Deutsch: „/webresources/i18nRestService?lang=de_DE“ . Bei der Response muss sich wieder um ein JSON Format handeln.

–    Partielles Laden von Übersetzungen (angular-translate-loader-partial wird benötigt)

Kann verwendet werden wenn die Datei der Übersetzungen extrem groß sind.

  $translateProvider.useLoader('$translatePartialLoader', {
      urlTemplate: 'resources/i18n/partitial/{part}/{lang}.json'
  });

= wir registrieren einen Templatepfad in dem uns die beiden Variablen „lang“ für die Sprache und „part“ als Key zur Verfügung stehen. Wir können dann in einzelnen Controllern konkrete Teile der Übersetzungen Abrufen:

  .controller('detail', function ($translate, $translatePartialLoader) {
      $translatePartialLoader.addPart('details');
      $translate.refresh();
  }

= wird bei eingestellter Kultur „de_DE“ folgenden Pfad abrufen:
‚resources/i18n/partitial/details/de_DE.json‘ und die Übersetzungen aktualisieren.

Welche Sprache darf es sein?

Bevor wir nun unsere Übersetzungen zum Einsatz bringen müssen wir der Anwendung noch mitteilen welche Sprache denn zu nutzen ist. Dies geschieht ebenfalls in der Config-Methode indem wir das Modul anweisen die Browsersprache zu ermitteln. Zusätzlich definieren wir eine Sprache die verwendet werden soll wenn die Browser-Sprache nicht verfügbar ist

$translateProvider.determinePreferredLanguage();
$translateProvider.fallbackLanguage('de');

Wichtig dabei ist das die Browser-Sprache inklusive Region ermittelt wird. Demzufolge wird ein auf Deutsch eingestellter Browser in aller Regel die Datei „de_DE“ suchen. Diese Unterscheidung der Regionen ist in unseren Projekten eher unüblich aus dem Grund bietet angular-translate die Möglichkeit die zur Verfügung stehenden Sprachen zu deklarieren und auch ein Mapping an zu geben:

  $translateProvider.registerAvailableLanguageKeys(['de', 'en'], {
      'en_*': 'en',
      'de_*': 'de'
  });

= alle Kulturen die mit „en“ oder „de“ beginnen, also unabhängig der Region, werden auf unseren Sprachschlüssel „en“ bzw. „de“ gemappt.

Übersetzen bitte!

Die eigentliche Übersetzungen ein zu setzen ist nun der kleinste der Schritte. „angular-translate“ bietet drei unterschiedliche Varianten an um Texte zu übersetzen:
per Filter (‚translate‘)

  {{'TITLE' | translate}}

per Direktive (‚translate‘)

  <span translate='TITLE'></span>

per Service (‚$translate‘)

  $translate('TITLE').then(function (text) {
      vm.title = text;
  });

Bei der Verwendung des Services ist es wichtig zu beachten das es sich bei dem Rückgabewert des translate-Services nicht um die Übersetzung handelt sondern um ein „promise“, aus dem Grund verwendet das Beispiel die Methode „then“ um die Übersetzung nach erfolgreicher Ermittlung entgegeben zu nehmen und zu verarbeiten.
Egal welche Methode zum Einsatz kommt, die Übersetzungen werden auf Basis des Keys ermittelt (im Beispiel ‚TITLE‘). Innerhalb der Übersetzungsdateien kann wie oben bereits gesehen auch mit einer Hierarchischen Struktur gearbeitet werden. Die entsprechenden Keys werden dann mit einem Punkt zusammengesetzt: „MENU.HOME“

Übersetzung mit Parametern

In den Übersetzungen lassen sich auch dynamische Werte verwenden. Diese sind mittels „{{}}“ im Nachrichtentext gekennzeichnet:

 "DEMOTITLE": "Demo: {{title}}"

Bei der Verwendung kann dieser Parameter dann  übergeben werden:

{{'DEMOTITLE' | translate:'{title: "i18n"}'}}

<span translate='DEMOTITLE' translate-values='{title: "i18n"}'></span>
  
$translate('DEMOTITLE', {title: "i18n"}).then(function (text) {
      vm.title = text;
});

$translateSanitization: No sanitization strategy has been configured

Eine Warnung die bei der Verwendung von „angular-translate“ im Log des Browsers auftaucht. Diese Warnung weißt daraufhin das keinerlei Escaping der Übersetzungen stattfindet. Mit dieser Konstellation ist es von Angreifern leicht möglich schädliche Scripte aus zu führen da diese über die Übersetzungs-Dateien ungefiltert von der Anwendung ausgeführt werden.

"DEMOTITLE": "alert('Böses Script)"

Wird zu einer entsprechenden Anzeige des alerts-führen. Im Standard stehen folgende Strategien zur Verfügung:

  • null : (Default)
  • sanitize: bereinigt die gesamte Übersetzung von HTML/Script Tags
  • escape: escaped die gesamte Übersetzung
  • sanitizeParameters: bereinigt nur die Parameter von HTML/Script
  • escapeParameters: escaped nur die Parameter

Verwendung (.config-Methode):

$translateProvider.useSanitizeValueStrategy('sanitize')

(’sanitize‘ wird ab Version 3.0 auch der Standardfall sein)

Mit ‚angular-translate‘ ist das Internationalisieren von Anwendungen wie gerade gesehen kein Hexenwerk. Die verschiedenen Quellen die für das Laden der Übersetzungen herangezogen werden können sind flexibel Einsetzbar und sollten in jeder Projekt-Konstellation eine passende Möglichkeit bieten.

Hier gibt es das Projekt noch einmal komplett…

Den zweiten Teil dieser Reihe werden wir der Verarbeitung von lokalisierten Daten mittels ‚globalize‘ widmen…

AngularJS und Java EE: Bean Validation

Nicht nur AngularJS, auch JSF bietet die Möglichkeit Eingaben mit einer Validierung zu versehen. In unseren Projekten verzichten wir allerdings seit geraumer Zeit darauf die JSF-eigenen Validatoren zu verwenden und setzen als Mittel der Wahl BeanValidation ein um GUI unabhängig Regeln in unserem Datenmodell zu verankern. Dies geschieht im Fall von JSF und mit der Hilfe von Komponenten Bibliotheken (Primefaces, Richfaces) nahtlos ohne dass wir uns explizit um die Registrierung dieser Regeln kümmern müssten. Aber wie sieht es mit AngularJS aus? Wie können wir die Validierungsregeln, die an unserem Datenmodel deklariert sind, in eine JavaScript Anwendung nutzen? Ein Modul welches uns hier zur Seite steht ist:

valdr

Valdr verwendet bei der Deklaration seiner Regeln einen Model-zentrierten Ansatz. Das heißt anstatt einem konkreten Eingabefeld eine Regel zuzuweisen findet diese Deklaration auf Basis von Objekten und dessen Attributen statt. Hier ein sehr einfaches Beispiel wie eine solche Deklaration aussehen könnte:

  valdrProvider.addConstraints({
      'Demo': {               //Um welches Objekt handelt es sich?
          'messageAttribute': { // welches Attribut?
              'size': {      // welche Regel soll angewendet werden? 
                  'min': 1,
                  'max': 10,
                  'message': 'Zeichen: 1-10'
              },
              //weitere Regeln für 'messageAttribute'
          },
          //weitere Attribute + Regeln für "Demo"
      },
      // weitere Objekte
  });

Verwendet wir hier eine JSON-Struktur die festlegt welche Objekte für die Validierung zur Verfügung stehen und welche Attribute eine Validierungsregel beinhalten. Neben der hier gezeigten „size“ Regel existieren natürlich noch weitere:

  • size – Längen-Prüfung mit min und max
  • minLength,maxLength – analog zu ‚size‘ (jedoch nur für Strings)
  • min,max – Prüfung von Zahlen
  • required – Pflichtfeld
  • pattern – Prüfung mittels regex
  • email – Prüfung auf gültige Email
  • digits – Prüfung auf Zahlen mit Vor- und Nachkommastellen (integer, fraction)
  • url – Prüfung auf gültige URL
  • future,past – zeitliche Prüfung (zusätzlich wird das Modul: Moment.js benötigt)

Bei der Verwendung innerhalb unserer Formulare ist es nun lediglich nötig das verwendete Objekt über die valdr-type Direktive zu deklarieren und darauf zu achten das unsere Eingabefelder mittels „name“ Attribut mit dem Namen in unseren Validierungsregeln übereinstimmen. Die Ausgabe der Nachrichten erfolgt automatisch wenn zusätzlich zu valdr auch das Module valdr-messages installiert ist und das Eingabefeld in einem Block deklariert ist der die Direktive: valdr-form-group beinhaltet:

<head>
    < script src="bower_components/angular/angular.js" type="text/javascript"></script>
    < script src="bower_components/valdr/valdr.js" type="text/javascript"></script>
    < script src="bower_components/valdr/valdr-message.js" type="text/javascript"></script>
    < script src="app.js" type="text/javascript"></script>
</head>
<body ng-controller="bv as vm">
    <form valdr-type="Demo" name="vm.demoForm">
        < div valdr-form-group>
            < label for="message">Message</label>
            < input id="message" name="messageAttribute" ng-model="vm.messageInput">
        </div>
    </form>
</body>

Alternativ zur automatischen Fehlerausgabe werden auch die AngularJS – Flags $valid/$invalid auf dem Formular gesetzt + entsprechende valdr-Fehlerobjekte im Formular $error-Attribut abgelegt. Über letztgenanntes lässt sich so auch eine eigene Fehlerausgabe implementieren die z.B. alle Meldungen zu einer Liste zusammenfasst (idealerweise würde man dies in eine Direktive auslagern)

(JavaScript 6 Syntax)

  // aktualsiert unserer Liste von Fehlermeldungen
  $scope.$watch(function() { return vm.demoForm.$valid }, function() {
      vm.errorMessages = vm.getErrors();
  });
  
  // liefert eine flache Liste von Fehlermeldungen im Format: "Feld: Meldung"
  // die mittels ng-repeat angezeigt werden kann
 // der erste Fehler des ersten fehlerhaften Attributes liegt z.B. hier:
 // demoForm.$error.valdr[0].valdrViolations[0]
  vm.getErrors = function() {
      if (this.demoForm.$error && this.demoForm.$error.valdr)
          return this.demoForm.$error.valdr
              .map(field => field.valdrViolations
                  .map(error => error.field + " : " + error.message))
              .reduce((prev, current) => prev.concat(current));
  }

Bean Validation

Die Einbeziehung von BeanValidation erfolgt schließlich durch ein zusätzliches Modul: valdr-bean-validation auf AngularJS-Seite und die Einbindung einer Java Bibliothek auf Java-Seite. Die Grundidee ist einfach: aus unserem Java-Projekt werden die Validierungsregeln im oben gezeigten Format auf Basis der BeanValidation Annotationen generiert. Die unterstützen BeanValidation Annotationen sind: NotNull, Min, Max, Size, Digits, Pattern, Future, Past, Email (Hibernate) und URL (Hibernate).

Für die Einbindung stellt das Modul zwei Möglichkeiten zur Verfügung:

  • generierung zur Build-Zeit mittels Maven (s. valdr-bean-validation#cli-client
  • über ein Servlet, welches zur Laufzeit die entsprechenden Informationen generiert.

Um eine möglichst lose Kopplung unseres Java Projektes und der AngularJS Anwendung zu erreichen hier ein Beispiel über die genannte Servlet Variante.  Folgende Maven-Dependency fügen wir hinzu:

    <dependency>
      <groupId>com.github.valdr</groupId>
      <artifactId>valdr-bean-validation</artifactId>
      <version>1.1.2</version>
  </dependency>

Neben der Registrierung des Servlets wird schließlich noch eine Konfiguration benötigt bevor die JSON Formate über den valdrProvider geladen werden können. Ohne weitere Konfiguration wird diese Datei im Classpath unter dem Namen: valdr-bean-validation.json gesucht (kann über den Servlet-Init-Parameter „configFile“ angepasst werden)

Java

valdr_java

AngualrJS

valdr_angular

 

Das Modul valdr bietet uns also die Möglichkeit unsere bewährte Deklaration von Validierungsregeln mittels BeanValidation auch in die JavaSccript Welt zu tragen. Neben den hier gezeigten Möglichkeiten ist unter anderem auch die Internationalisierung der Fehlernachrichten und die Entwicklung eigener Validatoren möglich, weitere Details siehe: valdr @ github

AngularJS – manuelles Bootstraping für asynchrone Initialisierungen

AngularJS kommt mit vielen „magischen“ Funktionen daher welche die Entwicklung und das Zusammenspiel vereinfachen, allem voran die Depedency Injection. Unsere Aufgaben als Entwickler um eine AngurJS Anwendung zu initialisieren beschränken sich auf die Einbindung der Bibliothek und die Deklaration einer ng-app Direktive. Aber was ist wenn wir unsere Anwendung mit Daten eines http-Services konfigurieren wollen bevor die Anwendung dem Benutzer zur Verfügung steht? An dieser Stelle steht AngularJS uns mit seinem strikten asynchronen Verhalten im Weg, bietet aber eine Ausweg: manuelles Bootstraping

Die normale Initialisierung einer AngularJS Anwendung findet ganz automatisch statt. Nachdem die HTML Seite geladen wurde (inklusive AngularJS Referenz) ermittelt Angular über die angular-app Direktive  die zu ladende Anwendung, erzeugt den Injektions-Mechanimus und erzeugt/lädt alle benötigten Objekte und Module. Innerhalb dieses Prozesses werden auch die .config und .app Funktionen der Referenzierten Module zur Initialisierung geladen. Aber selbst in diesen Methoden ist es uns  nicht möglich auf die Abarbeitung von asynchronen Anfragen zu warten (um zum Beispiel eine Konfiguration über http zu laden). Um ein solches Szenario dennoch zu realsieren bestehen mehrer Möglichkeiten

  • einen synchron xhr-Request stellen ohne http-Service  (xmlhttp.open(method, url, false))
    • syncrone Anfragen im main Thread sind deprecated
  • ui-router mit einem Mehrstufigem Template dessen Root-State mittels „resolve“ die benötigten Daten lädt
  • manuelle Bootstraping

Um ein manuelles Boostraping unserer AngularJS Anwendung zu realisieren müssen wir im ersten Schritt dafür Sorge tragen das AngularJS mit seinem Automatismus uns nicht zuvorkommt. Aus diesem Grund entfernen wir die ng-app Direktive aus unserer HTML-Seite:

Vorher: 
< html ng-app="AngularBootstraping">
Nachher:
< html>

Damit führt AngularJS keinerlei Initialisierung durch und es liegt an uns diese, nach Fertigstellung unserer Konfiguration, aus zu führen.  Ausgehend davon das unser Angular Modul den Namen: „AngularBootstraping“ trägt könnte eine solche Methode so aussehen:

(function () {
    "use strict";
    // Erzeugen einer Injector Instanz
    // dazu übergeben wir die von uns benötigten Module
    // inklusive 'ng' welches uns diverse Angular Objekte zur Verfügung stellt
    var initInjector = angular.injector(["ng", "AngularJSi18n.globalize.services", "AngularBootstraping.initialize"]);

    // den Injektor nutzen um manuelle Dependency Injection durch zu führen
    // und asyconre Initialisierungen durchführen, z.B:
    
    // 1. Initialisierung von Globalize über eine Factory
    var $window = initInjector.get("$window");
    var lang = $window.navigator.language || $window.navigator.userLanguage;
    var GlobalizeLanguageLoader = initInjector.get("GlobalizeLanguageLoader");

    // 2. KonfigurationsObjekt über Rest Schnittstelle laden
    var configPromise = initInjector.get("InitFactory").getConfig();
    var globalizePromise = GlobalizeLanguageLoader.loadLanguage(lang);
    
    // auf alle asychronen Requests warten:
    var $q = initInjector.get("$q");

    $q.all([configPromise, globalizePromise]).then(function(result){
        // die Ergebnisse unserer Initialisierung legen wir als Konstanten ab:
        angular.module("AngularBootstraping").constant("$config", result[0]);
        angular.module("AngularBootstraping").constant("$globalize", result[1]);
        
        // erst jetzt lassen wir AngularJS seine Initialisierung durchführen
        angular.bootstrap(document, ["AngularBootstraping"]); 
    });
    
})();

Damit stellen wir sicher dass alle unsere Aufgaben zur Initialisierung beim eigentlichen Programmstart bereits abgeschlossen sind. Wichtig: in einer Produktiven Umgebung sollte eine stabile Fehlerbehandlung implementiert werden, wenn einer der Schritte innerhalb der Initialisierung fehlschlägt (z.B. Rest-Services stehen nicht zur Verfügung)  findet keine Anzeige der Anwendung statt.

Asynchrone Requests sind ein wichtiger Bestandteil von AngularJS Anwendungen die ein anwenderfreundliches Handling sicherstellt. Wir sollten nur in unausweichlichen Situationen auf diese Initialisierung zurückgreifen da sie den Start der Anwendung entsprechend verzögert ohne das der Anwender über die aktiven Lade-Vorgänge informiert wird. Dennoch gibt es durchaus solche Situationen für die AngularJS, wie zu sehen, eine passende Lösung parat hält.

AngularJS und Java EE: Build Prozess

Der Schritt von der Entwicklung von Webanwendungen mit JSF hin zu JavaScript Frameworks wie „AngularJS“ ist kein Kleiner. Umfangreiche Anwendungen mit JavaScript zu schreiben ist da nur ein kleiner Baustein denn allzu schnell wird klar, dass die „JavaScript Welt“ ganz eigene Tools, Frameworks, Do’s and Don’ts hatAngularJS + Yeoman_s

Wenn die ersten AngularJS – Tutorials erst einmal absolviert sind und man anfängt sich ernsthaft Gedanken um eine saubere Projektstruktur zu machen wird es Zeit zu automatisieren. Ein häufig eingesetztes Tool für diesen Zweck ist „grunt“. Grunt allein reicht hier allerdings bei weitem nicht denn für jede Aufgabe gibt es eigenes Modul das ganz individuell konfiguriert werden will.

Yeoman erscheint da vielen Entwicklern der Retter in der Not zu sein und bietet die Flucht aus dem Dschungel der Module, Plugins und Konfigurationen, aber müssen wir überhaupt flüchten? Es steht außer Frage: Yeoman nimmt dem Entwickler viel Arbeit ab. Auf Basis von so genannten Generatoren werden (nicht nur für AngularJS) ganze Projekte mit nur wenigen Angaben generiert, inklusive entsprechender Prozesse fürs Testing, Development und Release-Builds. Alleine für AngularJS gibt gleich eine Vielzahl von solchen Generatoren, allen voran der offizielle „angular“ Generator vom Yeoman Team selbst.

Auf den ersten Blick ähnelt es demnach den Maven Archetypes mit dessen Hilfe wir ja auch im Java Umfeld Template basierte Projekte erstellen können. Yeoman geht allerdings noch einen Schritt weiter. Nach dem initialen Projekt-Setup kann Yeoman auch während der Entwicklung zur Generierung von Sourcen verwendet werden. Wir benötigen eine Angular-Route? Nichts leichter als das „yo angular:route myRoute“ generiert einen entsprechenden Eintrag in der Konfiguration, legt View, Controller und passende Test-Cases an.

„Traue keinem Generator den du nicht verstehst“

Yeoman erleichtert also das Einrichten eines Projektes und Unterstützt bei der täglichen Entwicklung. Aber nur weil wir etwas machen lassen heißt das nicht, dass wir davon entbunden sind es zu verstehen, außerdem legt jeder Yeoman-Generator seine eigene Struktur und Abläufe fest die in Gänze oftmals nicht den eigenen Anforderungen entsprechen. Also widmen wir uns heute dem ganz eigenen Setup eines AngularJS Projektes. Genutzt wird in diesem Beispiel NetBeans als Werkzeug, welches die eine oder andere Aufgabe ebenfalls übernehmen kann.

Als Grundlage dient folgende Projektstruktur:

AngularJS + Yeoman_folderfiles

Der eigentliche Build-Prozess gliedert sich in diverse einzelne Schritte die wir mittels „grunt“ automatisieren. Werfen wir also einen Blick auf die Kern-Aufgaben die in aller Regel zur Bereitstellung einer JavaScript Anwendung nötig ist und wie wir dies mittels „grunt“ bewerkstelligen können. ( wer noch nie mit grunt gearbeitet hat, finde hier  eine kleine Einführung )


 

Was: Code Quality prüfen

Womit: grunt-contrib-jshint

Wie:

        jshint: {
            options: {
                jshintrc: '.jshintrc'
            },
            all: {
                src: "public_html/{,modules/**/}*.js"
            }
        },

Ergebnis:

jshint überprüft unseren Quelltext (alle *.js Dateien in unserem Webfolder und unterhalb von ‚modules‘) auf Basis der Regeln die in der Datei: „.jshintrc“ Konfiguriert sind. Eine Verletzung der Regeln führt zu einem Abbruch des Vorgangs. Die Prüfungen die jshint zur Verfügung stehen und die in der jshintrc-Datei aktiviert werden können sind hier alle aufgeführt: http://jshint.com/docs/options/


Was: CSS Source optimieren

Womit: grunt-postcss + autoprefixer

Wie:

        postcss: {
            options: {
                processors: [
                    require('autoprefixer')({
                        browsers: ['last 2 versions']
                    })
                ]
            },
            release: {
                files: [{
                        expand: true,
                        cwd: 'public_html/resources/styles/',
                        src: '*.css',
                        dest: '.tmp/styles/'
                    }]
            }
        },

Ergebnis:

Wir verwenden im Beispiel nur den autoprefixer der dafür sorgt dass unsere CSS Regeln mit den Browser-spezifischen Erweiterungen ergänzt werden. (-ms… , -webkit…). Das Ergebnis ist eine neue CSS-Datei die wir (erst einmal) in einem temporären Ordner ablegen. Der „autoprefixer“ ist nur ein Anwendungsfall, es existieren diverse Plugins für postcss, siehe: https://github.com/postcss/postcss/blob/master/docs/plugins.md


Was: automatisch JavaScript- und CSS-Links in index.html setzen

Womit: grunt-injector

Wie:

injector: {
            options: {
                bowerPrefix: 'bower:',
                template: 'public_html/index.html',
                ignorePath: 'public_html',
                addRootSlash: false
            },
            development: {
                files: {
                    'public_html/index.html': ['bower.json', 'public_html/{,modules/**/}*.js', 'public_html/resources/styles/*.css']
                }
            },
            prepare: {
                options: {
                    min: true,
                    relative: true,
                    template: 'public_html/index.html'
                },
                files: {
                    'public_html/index.tmp': ['.tmp/scripts/**/*.js', '.tmp/styles/*.css', 'bower.json']
                }
            }
        },

Ergebnis:

Sowohl unsere Bower-Abhängigkeiten als auch unsere eigenen Module/CSS Dateien werden automatisch in der index.html Datei eingetragen. Dazu verwendet „injector“ einen bestimmten Bereich innerhalb der HTML Datei den wir selber festlegen müssen. Wir verwenden im Beispiel den Standard Fall:

„<!– injector:js –> bzw. <!– injector:css –> .

Es kommen zwei unterschiedliche targets zum Einsatz:

„development“: fügt die nicht komprimierten Abhängigkeiten direkt in die index.xhtml ein

„prepare“: wird für ein Release verwendet, nutz die .min-Versionen der JavaScript-Dateien und generiert eine eigene temporäre Index-Datei.


Was: Unsere Angular Module für die Komprimierung Vorbereiten

Warum:

AngularJS arbeitet mit Dependency Injection die über Namensgleichheit realisiert ist. Eine Komprimierung der Sourcen wird in aller Regel dazu führen das durch die Änderung der Variablennamen dies nicht mehr möglich ist. Angular bietet dazu eine entsprechende sichere Schreibweise an die wir uns hier generieren lassen (anstatt sie selber zu verwenden)

Womit: grunt-ng-annotate

Wie:

        ngAnnotate: {
            build: {
                files: [{
                        expand: true,
                        src: "public_html/{,modules/**/}*.js",
                        ext: '.annotated.js',
                        dest: '.tmp/scripts'
                    }]
            }
        },

Ergebnis:

In unserem temporären Verzeichnis wird für jedes unserer Angular-Module eine entsprechende .annoted.js-Datei generiert die später komprimiert werden kann. Dazu ersetz das Modul lediglich die kritischen Deklarationen, zum Beispiel:

.controller("home", function ($scope) {…
wird zu:
.controller("home", ["$scope", function ($scope) {….

Was: Komprimieren von CSS und JavaScript-Dateien

Womit: grunt-usemin

Wie:

        useminPrepare: {
            html: 'public_html/index.tmp',
            options: {
                dest: 'dist'
            }
        },
        
        usemin: {
            html: ['dist/{,*/}*.html'],
            css: ['dist/styles/{,*/}*.css'],
            js: ['dist/scripts/{,*/}*.js']
        },

Ergebnis:

Das Modul bietet zwei Tasks: useminPrepare und usemin. ‚useminPrepare‘ führt keine eigenen Änderungen an den Dateien durch stattdessen generiert dieser Task dynamisch zusätzliche Tasks: cssmin,uglify,concat,filerev jeweils mit dem target: ‚generated‘. Genau wie der „grunt-injector“ wird hier mit einem HTML-Kommentar Block in der index.xhtml gearbeitet. Das Ausführen der Tasks sorgt dafür das:

  • alle Referenzen (CSS und JavaScript) die in einem solchen Block definiert sind: <!– build:[type css/js] [Ziel z.B. dist/scripts.js] –>
    • komprimiert werden (uglify für .js und cssmin für .css),
    • zu der entsprechende Datei in unserem ‚dist‘-Order zusammen gefasst werden (concat)

abschließend sorgt ‚usemin‘ dafür das die Referenzen in der index.html auf die minimierten Dateien verweisen.


Was: Komprimieren von HTML Dateien

Womit: grunt-contrib-htmlmin

Wie:

        htmlmin: {
            release: {
                options: {
                    collapseWhitespace: true,
                    conservativeCollapse: true,
                    collapseBooleanAttributes: true,
                    removeCommentsFromCDATA: true
                },
                files: [{
                        expand: true,
                        cwd: 'dist',
                        src: ['{,**/}*.html'],
                        dest: 'dist'
                    }]
            }
        },

Ergebnis:

Alle HTML-Dateien in unserem Release-Verzeichnis (dist) werden nach den konfigurieren Regeln komprimiert um eine geringere Dateigröße zu erreichen. Die zu Verfügung stehenden Optionen sind hier aufgeführt.


 

Zusätzlich haben wir noch einige Tasks um Dateien zu kopieren, die temporären Dateien und Ordner zu löschen und ein Maven-Artefakt (Beitrag) zu erzeugen. Ein entsprechender Prozess könnte dann so aussehen:

    grunt.registerTask("development", [
        // Sourcen für die Entwicklung setzen
        'injector:development',
        // Änderungen an Bower/index.html = neu generieren
        'watch:index'
    ]);

    grunt.registerTask('release', [
        // alte Dateien Löschen
        'clean',
        // Code Quality Check
        'jshint:all',
        // unsere Module für das uglyfy Vorbereiten
        'ngAnnotate:build',
        // CSS Regeln ergänzen
        'postcss:release',
        // Dateien kopieren (html, Bilder, Fonts)
        'copy:release',
        // min Versionen (und unsere ngAnnotatet-Vorbereiteten Sourcen) referenzieren
        'injector:prepare',
        // usemin Konfigurieren
        'useminPrepare',
        // Zusammenfassen
        'concat:generated',
        // CSS minimieren
        'cssmin:generated',
        // JS minimieren
        'uglify:generated',
        // Referenzen ersetzen
        'usemin',
        // HTML minimieren
        'htmlmin:release',
        // Maven Artefakt erzeugen
        'maven:release',
        // Sourcen für die Entwicklung wieder setzen
        'injector:development',
        // temporäre Dateien löschen
        'clean:tmp'
    ]);

Zwei Dinge fallen auf:

  • in vielen Grunt-Prozessen wird man die Registrierung eines lokalen Servers finden (connect + livereload) der wärend der Entwicklung automatisch die Änderungen der Webseite lädt und anzeigt. Netbeans bietet hier einen „embedded Webserver“ der über die Properties des Projektes (>“Run“) aktiviert werden kann. Ein Ausführen des Projektes bietet dank Chrome-Plugin („NetBeans Connector“) eine nahtlose Einbindun in NetBeans:
    • automatischer Refresh der Seite bei Änderungen
    • Netzwerk-Monitor
    • Browser-Logs direkt in NetBeans
    • HTML-Selektor über NetBeans (Anzeige/Änderung Styles)
    • JavaScript Debugging
  • TEST. Bisher führt unserer Prozess noch keinerlei Tests aus. Diesem Thema widmen wir in nächster Zeit einen ganz eigenen Beitrag.

Ergebnis (vorher,nachher)

angular_uglify_vorher_nachher

Es wird deutlich das ein automatisierter Prozess mit Grunt nicht nur für mehr Stabilität und Komfort bei der Entwicklung sorgt sondern vor allem, selbst in diesem extrem kleinen Szenario, in einer massiven Reduzierung der Netzwerklast in der finalen Anwendung.

Das gesamte Beispiel gibt es noch einmal hier.