how to do responsive components in Angular2

Tom Schreck picture Tom Schreck · Jul 18, 2016 · Viewed 16k times · Source

I'm wading my way into Angular2. My objective is to create a responsive app that loads different components in response to different media-queries for device widths. My working example has a MatchMediaService:

import { Injectable } from '@angular/core';

@Injectable()
export class MatchMediaService 
{
    constructor()
    {

    }

    rules =
    {
        print: "print",
        screen: "screen",
        phone: '(max-width: 767px)',
        tablet: '(min-width: 768px) and (max-width: 1024px)',
        desktop: '(min-width: 1025px)',
        portrait: '(orientation: portrait)',
        landscape: '(orientation: landscape)',
        retina: '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)'
    };

    Check = function (mq)
    {
        if (!mq)
        {
            return;
        }

        return window.matchMedia(mq).matches;
    };

/**********************************************
    METHODS FOR CHECKING TYPE   
 **********************************************/
    IsPhone()
    {
        return window.matchMedia(this.rules.phone).matches;
    };

    IsTablet = function ()
    {
        return window.matchMedia(this.rules.tablet).matches;
    };

    IsDesktop = function ()
    {
        return window.matchMedia(this.rules.desktop).matches;
    };

    IsPortrait = function ()
    {
        return window.matchMedia(this.rules.portrait).matches;
    };

    IsLandscape = function ()
    {
        return window.matchMedia(this.rules.landscape).matches;
    };

    IsRetina = function ()
    {
        return window.matchMedia(this.rules.retina).matches;
    };


/**********************************************
    EVENT LISTENERS BY TYPE
 **********************************************/    
    OnPhone(callBack)
    {
        if (typeof callBack === 'function')
        {
            var mql: MediaQueryList = window.matchMedia(this.rules.phone);

            mql.addListener((mql: MediaQueryList) =>
            {
                if (mql.matches)
                {
                    callBack(mql);
                }
            });
        }
    };

    OnTablet(callBack)
    {
        if (typeof callBack === 'function')
        {
            var mql: MediaQueryList = window.matchMedia(this.rules.tablet);

            mql.addListener((mql: MediaQueryList) =>
            {
                if (mql.matches)
                {
                    callBack(mql);
                }
            });
        }
    };

    OnDesktop(callBack)
    {
        if (typeof callBack === 'function')
        {
            var mql: MediaQueryList = window.matchMedia(this.rules.desktop);

            mql.addListener((mql: MediaQueryList) =>
            {
                if (mql.matches)
                {
                    callBack(mql);
                }
            });
        }
    };  

    OnPortrait(callBack)
    {
        if (typeof callBack === 'function')
        {
            var mql: MediaQueryList = window.matchMedia(this.rules.portrait);

            mql.addListener((mql: MediaQueryList) =>
            {
                if (mql.matches)
                {
                    callBack(mql);
                }
            });
        }
    };  

    OnLandscape(callBack)
    {
        if (typeof callBack === 'function')
        {
            var mql: MediaQueryList = window.matchMedia(this.rules.landscape);

            mql.addListener((mql: MediaQueryList) =>
            {
                if (mql.matches)
                {
                    callBack(mql);
                }
            });
        }
    };
}

Then inside a 'parent' component (HomeComponent), I use MatchMediaService to determine which child component (HomeMobileComponent or HomeDesktopComponent) to load depending on what MatchMediaService returns as well as listener events that fire when browser resizes through different dimensions:

import { Component, OnInit, NgZone } from '@angular/core';
import { MatchMediaService } from '../shared/services/match-media.service';
import { HomeMobileComponent } from './home-mobile.component';
import { HomeDesktopComponent } from './home-desktop.component';

@Component({
    moduleId: module.id,
    selector: 'home.component',
    templateUrl: 'home.component.html',
    providers: [ MatchMediaService ],
    directives: [ HomeMobileComponent, HomeDesktopComponent ]
})
export class HomeComponent implements OnInit 
{
    IsMobile: Boolean = false;
    IsDesktop: Boolean = false;

    constructor(
        private matchMediaService: MatchMediaService,
        private zone: NgZone        
    )
    {
        //GET INITIAL VALUE BASED ON DEVICE WIDTHS AT TIME THE APP RENDERS
        this.IsMobile = (this.matchMediaService.IsPhone() || this.matchMediaService.IsTablet());
        this.IsDesktop = (this.matchMediaService.IsDesktop());

        var that = this;


        /*---------------------------------------------------
        TAP INTO LISTENERS FOR WHEN DEVICE WIDTH CHANGES
        ---------------------------------------------------*/

        this.matchMediaService.OnPhone(
            function (mediaQueryList: MediaQueryList)
            {
                that.ShowMobile();
            }
        );

        this.matchMediaService.OnTablet(
            function (mediaQueryList: MediaQueryList)
            {
                that.ShowMobile();
            }
        );

        this.matchMediaService.OnDesktop(
            function (mediaQueryList: MediaQueryList)
            {
                that.ShowDesktop();
            }
        );
    }

    ngOnInit()
    {

    }

    ShowMobile()
    {
        this.zone.run(() =>
        { // Change the property within the zone, CD will run after
            this.IsMobile = true;
            this.IsDesktop = false;
        });
    }

    ShowDesktop()
    {
        this.zone.run(() =>
        { // Change the property within the zone, CD will run after
            this.IsMobile = false;
            this.IsDesktop = true;
        });
    }   
}
<home-mobile *ngIf="(IsMobile)"></home-mobile>
<home-desktop *ngIf="(IsDesktop)"></home-desktop>

This approach works. I can load the appropriate component in response to the device. It affords me the ability to customize a component (content, style, functionality, etc) to the device thus enabling the best user experience. This also affords me the ability to target different components for Mobile, Tablet, and Desktop (although I only am focusing on mobile and desktop in example).

Is there a better way to do this? The downside is that I'm forcing every component to be comprised of the parent component to determine via MatchMediaService which child component to load. Will this be scalable to work in a full blown production level app? I'm very interested in your feedback on a better approach, or if this approach is acceptable and scalable for a full blown production app. Thanks for you feedback.

Answer

G&#252;nter Z&#246;chbauer picture Günter Zöchbauer · Jul 18, 2016

You can create a custom media-aware *ngIf or *ngSwitch structural directive to make it less repetitive.

https://angular.io/docs/ts/latest/guide/structural-directives.html