how to do responsive components in Angular2
Asked Answered
I

2

22

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.

Insurance answered 18/7, 2016 at 16:49 Comment(0)
D
4

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

Detonation answered 18/7, 2016 at 16:52 Comment(12)
Tom, this design will make desktop components and mobile components live in the same build which means a big bundle that has unused code. You won't be able to tree-shake it as it used, but not really used. For example, the <home-mobile> will never run on desktop, right?Denmark
Thanks for the reply. I could not find anything specific regarding media aware structural directives in the URL you provided. I am using ngIf structural directive for loading the child component and using MatchMediaService for the media aspect providing the decision point for ngIf. What am I missing?Insurance
Do you have a better idea? It seems the Angular team wants to provide something out-of-the-box but couldn't come up with some good approach. @View() was supposed to help here but was dropped because it didn't work out.Impermanent
@TomSchreck I meant you can build a custom *ngIf or *ngSwitch like *meadiaSwitch that adds the part that matches the current width (or whatever). The link is only supposed to show how to create structural directives.Impermanent
@ShlomiAssaf lazy loading should make this less of a burden (not a full solution though)Impermanent
@Shlomi - the home-mobile could be displayed in the desktop should the dimensions of the desktop browser change. The user could resize the browser manually. The app would respond accordingly and load the mobile view to meet the viewport's dimensions.Insurance
@Günter Zöchbauer - thanks. I see now what you mean.Insurance
I'm not sure having 2 versions for the same component based on media queries is the right approach, maybe if you share the class (extend) and just change the metadata. Anyway, in angular 1 we solved this quite easily with webpack by building a plugins that "diverges" the code into defined async chunks. so you would do require.diverge({mobile: './[mobile].my-comp.template.html', desktop: './[desktop].my-comp.template.html'). This also works for CSS and JS files. However, in NG2 the runtime compiler might not be there so this approach needs inspectionDenmark
Of course, the end result is 2 async chunk bundles with all code snippets, when the app loads you download the bundle suited for the environment (mobile/desktop) and bootstrapDenmark
But as Tom wrote this is not static. When the browser is resized below a threshold then even on the desktop mobile components would be shown, therefore statically bundling won't work.Impermanent
i've refactored the media checks into a base component. now every component extends the base component so IsMobile and IsDesktop are public variables on base component. I'm no longer duplicating code across multiple components. This seems to be the best approach that I can think of for loading components dynamically based on media queries. Thanks for your input.Insurance
Hi @TomSchreck, would it be possible for you to share a sample of how you solved this problem? Thanks.Treulich
M
0

Couldn't you avoid all that logic by routing to lazily loaded modules, ie. mobile, desktop, whatever, by making app.component navigate to the corresponding module's route based on navigator.userAgent? From https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent

we recommend looking for the string “Mobi” anywhere in the User Agent to detect a mobile device

https://embed.plnkr.co/NLbyBEbNoWd9SUW7TJFS/

Mcclish answered 5/10, 2017 at 18:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.