Ich musste in einer Angular Anwendung mit Twitter Bootstrap unterschiedliche Navigationstypen verwenden, und so zwischen nav-tabs und nav-sidebar umschalten.

Die ng-class directive könnte man dazu verwenden wenn man im Controller die notwendigen Überprüfungen machen würde. Jedoch fand ich dies ziemlich hacky. Da man dann auch bei jedem digest die Überprüfung macht.

Moderne Browser können über window.matchMedia() prüfen ob eine Media Query, für ältere Browser <= IE 9 gibt es entsprechende Polyfills. Auf die gehe ich hier jedoch nicht ein.

var mql = window.matchMedia('(min-width: 768px)');  
if (mql.matches) {  
  // media query passt
}

Dies muss dann nur noch ausgeführt werden, an geeigneter stelle. Nun könnte man sich auf das resize-Event vom window hängen, dies führt jedoch dann zum gehäuften abfragen und ein wenig sollte man ja schon auf die Performance achten. Zum Glück kann man sich mit addListener() an die Media Query anmelden wird benachrichtigt wenn sich eine Media Query geändert hat.

In diesem Beispiel packe ich es eine AngularJS Directive so dass sie sehr einfach überall angewendet werden kann.

Die Verwendung soll analog zu ng-class sein, so dass ich ein einen Ausdruck angeben kann im welchem fall welche CSS Klassen gesetzt werden soll. Die Direktive soll hier da-media-class heißen.

Beispiel:

<ul role="navigation"  
    class="nav" 
    da-media-class="{'nav-sidebar':'(min-width: 768px)','nav-tabs':'(max-width: 767px)'}">
    <li>...</li>
</ul>  

Wenn also die query passt soll die gewünschte Klasse gesetzt werden.
Des Weiteren soll die direktive schon definierte queries kennen so das man dort übliche Größeangaben reinsetzen kann. Üblich hier im sinne von Twitter Bootstrap.

<ul role="navigation"  
    class="nav" 
    da-media-class="{'nav-sidebar':'sm-min','nav-tabs':'xs'}">
    <li>...</li>
</ul>  

Dies fördet die Wart- und Lesbarkeit.

Die definition der Direktive.

function mediaClassDirective($window) {  
    //
    return {
        restrict: 'A',
        link: link,
        scope: {
            mediaClass: '&daMediaClass'
        }
    }
}

angular.module('da.directives.responsive', [])  
    .directive('daMediaClass', mediaClassDirective);        

Dies könnte auch beim Anwendungs-Modul definiert werden, aber es ist best practice sich direktiven die man wieder verwenden möchte das man diese in eigenen Angular Module packt und dann in die Anwendungen die Abhängigkeit zum Modul angibt.

Die function mediaClassDirective() erzeugt ein Directive Definition Object (DDO) welches die Direktive beschreibt.

Diese Direktive soll nur als Attribute eingesetzt werden können und sich das übergebene CSS/MediaQuery-Object in den eigenen Scope ziehen.

In der link() Funktion wird nun das Query Object auseinander gepflückt und die mit matchMedia erstellte Abfrage zwischengespeichert damit man diese bei einer Änderung alle überprüfen kann.

function link(scope, element) {  
    var mediaClass = scope.mediaClass();
    var queries = {};
    for (var cssClass in mediaClass) {
        var query = mediaClass[cssClass];
        var mql = $window.matchMedia(query);
        var queries[cssClass] = query;
        // ...
    }
    // ...
}

Das Prüfen der mediaQueries muss dann entsprechende ausgeführt werden und die jeweilige CSS Klassen hinzugefügt oder entfernt werden. Dies muss beim aufrufen der Seite und bei einer Änderung passieren. Auch müssen alle Queries überprüft werden weil man nicht unbedingt für jede einzelne definierte Query benachrichtigt wird. Sondern jeweils nur von einer, wollte die erste auf die eine Änderung zutrifft.

function queryMedia(queries, element) {  
    for (var cssClass in queries) {
        var mql = queries[cssClass];
        if (mql.matches) {
            element.addClass(cssClass);
        } else {
            element.removeClass(cssClass);
        }
    }
}    

Die gesammte Directive, mit der Möglichkeit der bekannten Größen sowie leichten Validierungen ist hier zu sehen. Geschrieben in ES6/Typescript.

module directives {  
    var mediaQueries = {
        'xs-min': '(min-width: 480px)',
        'xs': '(max-width: 767px)',
        'sm-min': '(min-width: 768px)',
        'sm': '(min-width: 768px) and (max-width: 991px)',
        'md-min': '(min-width: 992px)',
        'md': '(min-width: 992px) and (max-width: 1199px)',
        'lg-min': '(min-width: 1200px)',
        'lg': '(min-width: 1200px)'
    };

    function mediaClassDirective($window) {

        function queryMedia(queries, element) {
            for (var cssClass in queries) {
                var mql = queries[cssClass];
                if (mql.matches) {
                    element.addClass(cssClass);
                } else {
                    element.removeClass(cssClass);
                }
            }
        }

        function link(scope, element) {
            var mediaClass = scope.mediaClass();
            if (!angular.isObject(mediaClass)) {
                throw "media-class is not set to an object with css and media query definitions";
            }
            var queries = {};
            for (var cssClass in mediaClass) {
                var query = mediaClass[cssClass];
                query = mediaQueries[query] || query;
                var mql = $window.matchMedia(query);
                if (mql.media == "not all" && query != "not all") {
                    throw query + " is not a valid media query";
                }
                queries[cssClass] = mql;
                mql.addListener(()=>queryMedia(queries, element));
            }
            queryMedia(queries, element)
        }

        return {
            restrict: 'A',
            link: link,
            scope: {
                mediaClass: '&daMediaClass'
            }
        }
    }
    angular.module('da.directives.responsive', [])
        .directive('daMediaClass', mediaClassDirective);
}