brainbits Blog

Wartbares Frontend durch Komponenten-Design mit AngularJS und SASS

Steven Weingärtner22.03.2016Code • Anwendungsentwicklung • Frontend

Single-page-Applikationen sind oft große und komplexe Anwendungen. Bei der Entwicklung sollte deshalb besonders auf die Strukturierung der CSS-Styles und des JavaScript-Codes geachtet werden, damit die Anwendungen langfristig wartbar bleiben. Das “Komponenten-Design” hat sich hierfür in den letzten Jahren als brauchbares Werkzeug erwiesen.

Was sind Komponenten?

Komponenten sind spezifische Teile einer Anwendung. Jede Komponente verfügt über ein Layout (HTML und CSS) und gegebenenfalls dazugehörige JavaScript-Logik. Dabei können Komponenten auch ineinander verschachtelt werden. Ein Beispiel dafür wäre eine einfache Übersichtsliste von Artikeln, die dem Benutzer eine Bewertung der Artikel als “gut” (Pfeil nach oben) oder “schlecht” (Pfeil nach unten) erlaubt. Dabei wird ein Punktestand (count) erhöht oder verringert.

Verschachtelte Komponenten mit AngularJS

Beispiel zweier verschachtelter Komponenten (Quelle: rscss.io)

Dieses Beispiel lässt sich einfach in zwei Komponenten aufteilen:

  1. Die Vote-Box ist eine Komponente, die lediglich die beiden Pfeil-Buttons und einen Zähler definiert.
  2. Der Article-Header enthält wiederum eine Vote-Box, die Überschrift des Artikels und eine kurze Zusammenfassung.

Eine solche Aufteilung bietet gleich mehrere Vorteile:

  1. Beide Komponenten können leicht wiederverwendet werden. So könnte der Article-Header einfach mehrmals untereinander in einer Liste ausgegeben werden oder auch in einer Sidebar als “Artikel des Monats”. Die Vote-Box hingegen könnte auch der Bewertung von Produkten oder andere Dingen dienen.
  2. Die Komponenten sind voneinander weitestgehend abgeschottet. Änderungen in einer Komponente sollten keinen unvorhergesehenen Einfluss auf andere Komponenten ausüben.
  3. Jede Komponente kann losgelöst von anderen Komponenten getestet werden.

Dieses Konzept ist so beliebt, dass selbst das w3c bereits an einer Spezifikation für “Webcomponents” arbeitet, damit Browser dieses Konzept eines Tages einheitlich und flächendeckend direkt unterstützen. Bis dahin kann man sich verschiedener JavaScript-Libraries bedienen, welche die Benutzung von Komponenten schon heute ermöglichen, zum Beispiel der webcomponents polyfill oder Facebooks react.

Wir bei brainbits setzen für diesen Zweck das Framework AngularJS ein. 

Komponenten in AngularJS

Der einfachste Weg, um in AngularJS (1.3+) mit Komponenten zu arbeiten, sind Direktiven.

// voteBox.js
angular.module('myApp').directive('voteBox', function () {
    return {
        restrict: 'e',
        scope: {},
        controllerAs: '$ctrl',
        controller: function Controller() { … },
        templateUrl: './voteBox.html',
        bindToController: {
            count: '=', // two-way binding, bitte Änderungen in AngularJS 1.5 beachten
            onUpVote: '&',
            onDownVote: '&',
        },
    };
});

Das Template für unsere Direktive enthält den Inhalt unserer Komponente. Das umliegende Element wird bereits durch den Aufruf gerendert, da wir auf die replace-Eigenschaft verzichten.

// voteBox.html
&lt;button class="up" ng-click="$ctrl.onUpVote()"></button>
&lt;span class="count">{{ $ctrl.count }}</span>
&ltbutton class="down" ng-click="$ctrl.onDownVote()"></button>

Eine AngularJS-Direktive, die als Komponente verwendet wird, sollte also folgende Merkmale enthalten:

  • Sie sollte eine Element-Restriktion erhalten, damit jede Komponente im HTML als eigener “HTML-Tag” beschrieben werden kann.
  • Sie benötigt ein eigenes Template (HTML), das den Aufbau bzw. Inhalt der Komponente enthält.
  • Sie benötigt einen Controller, also eine “Klasse”, welche die JavaScript-Logik der Komponente enthält.
  • Sie sollte die “controllerAs” Eigenschaft setzen, um aus dem Template auf die Controller-Instanz zugreifen zu können (wir empfehlen “$ctrl” als Wert für die Eigenschaft).
  • Sie sollte einen isolierten Scope erhalten, um sicherzustellen, dass keine Logiken oder Daten ungewollt nach außen in andere Komponenten oder von außen in die aktuelle Komponente gelangen.
  • Sie sollte über Attribut-Bindings und “bindToController” konkrete Schnittstellen nach außen anbieten (mehr dazu weiter unten).

Die Benutzung im Article-Header könnte dann wie folgt aussehen:

&ltvote-box count="$ctrl.score" on-up-vote="$ctrl.score += 1" on-down-vote="$ctrl.score -= 1">
&lt/vote-box>

In dem Beispiel sieht man bereits einen wichtigen Aspekt: Inputs und Outputs. Jede Komponente sollte konkrete Schnittstellen nach außen anbieten, über die Elternkomponenten mit dieser Komponente kommunizieren können. In diesem Beispiel bietet die Vote-Box einen “input”, um die aktuelle Bewertung hineinzureichen, und zwei outputs, die aufgerufen werden, wenn der “up”- oder “down”-Button geklickt wurde.

Dadurch lässt sich ein weiterer wichtiger Punkt verdeutlichen: Nicht jede Komponente sollte über das nötige “Wissen” verfügen, um Daten zu manipulieren. In diesem Fall wird die Vote-Box niemals ihren eigenen “count”-Wert manipulieren. Die Logik wird hier an anderer Stelle (z.B. dem Article-Header) implementiert. So kann die Vote-Box in verschiedensten Szenarien flexibel eingesetzt werden, auch wenn es zum Beispiel ein Limit für den count gibt oder wenn die Stimme mancher Benutzer möglicherweise doppelt zählen soll. Das Konzept sieht zwei grundlegende Arten von Komponenten vor: “schlaue” und “dumme” Komponenten. Es gibt viele Bezeichnungen für diese beiden Kategorien, wir verwenden meist “Container”- und “Presentation”-Komponenten. Während Presentation-Komponenten ausschließlich der Darstellung von Informationen dienen (wie die Vote-Box), verfügen Container-Komponenten über die eigentliche Applikations-Logik, um Daten zu laden und zu manipulieren (weitere Informationen zum Thema hier).

Zuletzt noch ein Wort zur Strukturierung der Dateien: Für uns hat es sich bewährt, für jede Komponente ein eigenes Verzeichnis anzulegen, in dem wir jeweils eine JavaScript-, HTML- sowie SCSS-Datei ablegen. Diese Komponenten-Verzeichnisse können dann nach Bedarf wiederum in “Feature”-Verzeichnisse gruppiert werden, um mehr Übersicht zu schaffen.

src
└┬─ components
 ├─┬─ voteBox
 │ ├─── voteBox.js
 │ ├─── voteBox.html
 │ └─── voteBox.scss
 └─┬─ articleHeader
   ├─── articleHeader.js
   ├─── articleHeader.html
   └─── articleHeader.scss

Änderungen seit AngularJS 1.5

Mit AngularJS 1.5 wurde die Unterstützung für Komponenten noch einmal maßgeblich verbessert:

  • Mit angular.module(‘myModule’).component(...) lassen sich Komponenten-Direktiven noch einfacher als bisher erzeugen.
  • Durch das neue “One-Way-Binding” für Attribute können Komponenten selber keine Änderungen an Input-Daten vornehmen, die möglicherweise Auswirkungen nach außen haben.

Die Vote-Box könnte demnach vereinfacht werden:

// voteBox.js
angular.module('myApp').component('voteBox', {
    controller: function Controller() { … },
    templateUrl: './voteBox.html',
    bindings: {
        count: '<', // one-way binding
        onUpVote: '&',
        onDownVote: '&',
    },
});

Der Hintergrund dieser besseren Integration von Komponenten in AngularJS ist die Vorbereitung auf ein mögliches Upgrade auf Angular2, denn das basiert vollständig auf dem Komponentendesign.

Styles für Komponenten

Auch für komponentenorientierte Styles gibt es bereits etliche Methodiken – die wohl bekannteste ist BEM. In einem aktuellen Projekt haben wir nun auf RSCSS gesetzt, das – bis zum Zeitpunkt dieses Artikels – auch eine sehr gute Figur macht.

Mithilfe dieser Methodiken kann man Styles in Komponenten (in BEM „Blöcke“ genannt) definieren, die aus sogenannten Elementen bestehen. Mit Variationen (in BEM „Modifier“ genannt) kann man einer Komponente oder einem Element ein abweichendes Aussehen verleihen. Das lässt sich sehr gut mit AngularJS-Komponenten kombinieren. So hat jede Komponente ihren eigenen Style-Block. Alle Bestandteile des Komponenten-Templates werden als Element behandelt. Soll aus dem JavaScript heraus das Erscheinungsbild eines Elements verändert werden, bekommt es einfach eine Variation per Klasse gesetzt.

Verschachtelte Komponenten aus Stylesicht

Zwei verschachtelte Komponenten aus Stylesicht (Quelle: rscss.io)

Zum Beispiel würden die Styles für den Article-Header in RSCSS-Schreibweise wie folgt aussehen:

.article-header {
    > .title {
        /* … */
    }
    > .meta {
        /* … */
    }
}

Wir setzen hierbei auf SASS, das macht die Stylesheets unserer Komponenten sehr übersichtlich.

Wichtig ist es, sich bei den Styles nur auf die eine Komponente zu konzentrieren. So sind etwa alle Styles zu vermeiden, die sich auf die Umgebung oder Position dieser Komponente beziehen. Dazu zählen auch die Eigenschaften margin oder position. Definiert eine Komponente ein Margin, kann man sie schwer unabhängig von ihrem Kontext betrachten, da das Margin zum Beispiel mit dem einer vorherigen Komponente zusammenfällt. Beim Definieren einer absoluten Position ist kaum zu sagen, welches das nächste Position-Parent ist, an dem sich die Komponente positionieren würde. Kurz gesagt: Man sollte darauf achten, immer nach innen zu stylen.

Die Positionierung von Komponenten und Aufteilung einer App sollte über separate Layout-Styles erfolgen. Hier bewähren sich natürlich Flexbox und Grid-Systeme.

Oft ist es praktisch, Elemente einer beinhalteten Komponente zu stylen. Ein Beispiel: Der Article-Header beinhaltet die Vote-Box. Allerdings sollen die Pfeile in der Vote-Box grau und nur rot sein, wenn sich der Mauszeiger über dem Header befindet. Die Styles könnten dann wie folgt aussehen:

// voteBox.scss
.vote-box {
    /* … */
    > .up,
    > .down {
        color: #707070;
    }
}

// articleHeader.scss
.article-header {
    /* … */
    &:hover > .vote-box > .up,
    &:hover > .vote-box > .down {
        color: #f66;
    }
}

In der Article-Header-Komponente ist davon auszugehen, dass die Struktur der Vote-Box bekannt ist. In einer großen Applikation kann das sehr kompliziert werden. Zudem können Probleme bei Strukturanpassungen der Vote-Box entstehen: Danach wäre eine flächendeckende Prüfung aller Komponenten nötig.

Eine Entkopplung dieses Zusammenhangs gelingt mit Hilfe von Variationen und @extend oder der Benutzung von Mixins. Die Vote-Box enthält eine Variation, die die Buttons rot färbt. Diese Variation wird dann auch im Article-Header für den :hover-State genutzt:

// voteBox.scss
.vote-box {
    /* … */
    > .up,
    > .down {
        color: #707070;
    }

    &.is-highlighted > .up,
    &.is-highlighted > .down {
        color: #f66;
    }
}

// articleHeader.scss
.article-header {
    /* … */
    &:hover > .vote-box {
        @extend .vote-box.is-highlighted;
    }
}

Fazit

Komponentendesign führt zu klaren Schnittstellen und Zuständigkeiten. Außerdem erhöht es die Wartbarkeit und Testbarkeit von Projekten. Das gilt nicht nur für AngularJS und SASS, sondern lässt sich auch mit anderen Frameworks oder gar statischem HTML und CSS umsetzen.

Zurück zur Übersicht