BEM in Angular (V2+) Projekten

Von Christian Otto |

Angular2 kam seinerzeit mit einer Neuerung, bei der es den Trend „Web Components“ aufgriff und uns von nun an ermöglichte, „Shadow DOM“ zu verwenden („nativ“ möglich, „emulated“ per default). Damit war es quasi ad hoc möglich, gekapselte Komponenten mit einem nur für sie relevanten und zugänglichen Stylesheet zu erstellen, was dem Anspruch, mit Angular z.B. auch eine modulare Komponenten-Bibliothek erschaffen zu können, gerecht wurde.

Welche Vorteile bringen das „Shadow DOM“ und das „Scoped Styling“ mit sich? Nun, es bedeutet vor allem, dass alle Styles (alle CSS rules), die wir global, also außerhalb unserer Komponenten, definieren, von den Komponenten genutzt werden können, wohingegen Styles, die innerhalb einer Komponente definiert werden, nicht nach außen reichen („locally scoped“).

Ein Entwickler kann sich also sicher sein, dass Styling-Regeln, die er im der Komponente zugehörigen Style-File anlegt, nirgendwo anders Einfluss haben, also das globale Layout der gesamten Anwendung auch „nicht kaputt machen können“. Und: er muss eigentlich keine besondere Namensgebung, z.B. bei den CSS-Classen an den verschiedenen Teilbereichen des Templates beachten. Wenn alles „scoped“ ist, dann kann es sowohl in Komponente ABC eine Klasse „.content“ geben, als auch in Komponente XYZ – ohne, dass sich die beiden mit ihren CSS-Regeln in die Quere kommen!

So weit. So gut. Oder auch nicht?

Nun, grundsätzlich ist das natürlich ein schönes und wünschenswertes Verhalten. Was aber passiert nun, wenn wir, zum Beispiel, eine eigene Button-Komponente (wir nennen sie mal „CustomButtonComponent“ mit dem Selektor „custom-button“), erstellt haben, und diese nun in einem speziellen Kontext einsetzen.

Ein beispielhaftes Template der Komponente (`custom-button.component.html`):

<!-- template of custom-button-component -->
<button class="custom-button" [disabled]="isDisabled" (click)="click($event)">
<!-- anything placed inside <custom-button-component></...> -->
<ng-content></ng-content>
</button>

Ebenfalls beispielhaft das dazugehörige Style-File (`custom-button.component.scss`):

// this is the component, <custom-button-component>
:host {
    display: inline-block;
    font-size: 1rem;
}

// the button inside the template
.custom-button {
    display: block;
    width: 100%;
    height: 100%;
    font-family: inherit;
    font-size: inherit;
    background-color: #fff; // weiß :)
    border-radius: .25em;
    border: .125em solid grey;
    color: grey;
    padding: .25em .5em;
}

Sagen wir mal, wir verwenden die Button-Komponente im Template einer „ApplicationHeaderComponent“, dort als „Logout“-Button.

<application-header>
    <span class="application-header__title">`s dashboard</span>
    <custom-button *ngIf="loggedIn" (clicked)="logOut()">Logout</custom-button>
</application-header>

Und jetzt kommt es! Wir möchten, dass die CustomButtonComponent dort dann nicht „weiß” als Hintergrundfarbe hat, sondern „grau”. Als Vordergrund- und Rahmenfarbe dafür dann „weiß”.

Unter dem Gesichtspunkt der Modularität darf die CustomButtonComponent selbst eigentlich nichts über ihren Verwendungs-Kontext wissen, d.h. wir dürfen jetzt nicht in der `custom-button.component.scss` einen Workaround für dieses Szenario vorsehen.

Don´t do this! (in `custom-button.component.scss`)

...
// When used inside an application-header.
// We need to use :host-context to reach out of this scope.
:host-context(app-application-header) {
    .custom-button {
        background-color: grey;
        border-color: white;
        color: white;
                    }
}

Hm, also muss die Modifikation stattdessen in das Style-File der ApplicationHeader Komponente? Bingo!

Ergo, die vorgesehenen Anpassungen finden sich hier wieder (`application-header.component.scss`):

application-header,
:host {
    display: block;
}

.application-header {
    display: flex;
    align-items: center;

// nested button
// Not reachable until components use `encapsulation: ViewEncapsulation.None`
    .custom-button {
        background-color: grey;
        border-color: white;
        color: white;
}
}

Done? Leider nein! Denn nun passiert bezüglich der visuellen Anpassung der CustomButtonComponent... rein gar nichts! Aber warum? Tja, jetzt haben wir die Situation, dass das Standardverhalten der Komponenten, „ViewEncapsulation.Emulated” (also emuliertes Shadow DOM / scoped styles), es verbietet, Einfluss auf Kind-Elemente einer anderen Komponente auszuüben. Unsere Styling-overrides greifen nicht!

Die beste Lösung daher: Die ViewEncapsulation in den Komponenten aufgeben (s.u.), in den Templates mit einer sinnvollen Namensgebung für die CSS-Klassen arbeiten (Empfehlung: „BEM”, für möglichst niedrige CSS-Spezifität), und dann die gewünschten Elemente etwa wie im letzten obigen Code-Beispiel adressieren! Works!

@Component({
    selector: 'custom-button',
    templateUrl: './custom-button.component.html',
    styleUrls: ['./custom-button.component.scss'],
    encapsulation: ViewEncapsulation.None // this makes the difference!
})

Senior Software Engineer

Christian Otto

Christian Otto startete 2007 als erster Mitarbeiter von MAXIMAGO. Seit Beginn seiner Ausbildung (noch vor der Jahrtausendwende!) hat der gelernte Mediengestalter einen Fokus auf die Onlinewelt und dabei einiges miterlebt: Von Microsoft Frontpage, Macromedia Dreamweaver bis zu Flash. Außerdem hat er Ausflüge in WPF und Silverlight unternommen. Heute liegen seine technische Vorlieben in allem rund um CSS und Control-Entwicklung.

Mehr zu Christian Otto