3

I want to configure my Angular 9 app to display a component differently depending on whether someone is using a mobile device to ivew it. When I used to build the template in Python, there was a user_agents package that would allow me to detect and serve different HTML based on a mobile path

{% load user_agents %}
...
{% if not request|is_mobile and not request|is_tablet %}
            <td>{{ item.category }}</td>
            <td><a href="{{ item.path }}">{{ item.title }}</a></td>
            <td align="center">{{ item.created_on }}</td>
{% else %}
            <td>
                <div>
                    {{ item.category }} · {{ item.created_on_fmted }}
                </div>
                <div>
                    <a href="{{ item.mobile_path }}">{{ item.title }}</a>
                </div>
            </td>
{% endif %}

Now that I'm building in Angular, I have the below mat-table for my regular devices

<mat-table #table [dataSource]="dataSource">

    <ng-container matColumnDef="category">
      <mat-header-cell *matHeaderCellDef> category </mat-header-cell>
      <mat-cell *matCellDef="let item">{{ item.category.path }}</mat-cell>
    </ng-container>

    <ng-container matColumnDef="title">
      <mat-header-cell *matHeaderCellDef> item </mat-header-cell>
      <mat-cell *matCellDef="let item"><a href='{{ item.path }}'>{{ item.title }}</a></mat-cell>
    </ng-container>

    <ng-container matColumnDef="date">
      <mat-header-cell *matHeaderCellDef> Date </mat-header-cell>
      <mat-cell *matCellDef="let item">{{ item.created_on }}</mat-cell>
    </ng-container>

  <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
  <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>

but I'm unclear what the Angular way is to detect if I'm on a mobile device and change the HTML appropriately. I would prefer not to detect screen sizes as that doesn't seem fool proof.

Will Taylor
  • 1,650
  • 9
  • 23
satish
  • 703
  • 5
  • 23
  • 52

2 Answers2

6

So you can detect whether the user is on desktop/tablet/mobile using the data available from the Navigator userAgent.

You could write your own Angular service to do this, but the simple solution is to use a library such as ngx-device-detector. It provides a service (which uses navigator.userAgent under the hood) which you can inject into your components to detect what device/platform the user is running.

You can use it in your component like:

constructor(private deviceService: DeviceDetectorService) {}

isMobile = this.deviceService.isMobile();

Then in your template:

<div *ngIf="isMobile">I only appear on mobile</div>

What might be even nicer is to wrap this in a directive so that you don't have to repeat this code inside many components.

import { Directive, OnInit, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
  selector: '[isMobile]'
})
export class IsMobileDirective implements OnInit {

  constructor(private deviceService: DeviceDetectorService,
              private templateRef: TemplateRef<any>,
              private viewContainer: ViewContainerRef) { }

  ngOnInit() {
    if (this.deviceService.isMobile()) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      this.viewContainer.clear();
    }
  }
}

Now you can easily render content based on the device by simply using this directive on any element inside of your component templates (note that the asterisk is required for this structural directive):

<div *isMobile>I only appear on mobile</div>

Similarly you could also create isDesktop, isTablet, isNotMobile directives as required.

Will Taylor
  • 1,650
  • 9
  • 23
  • Is "*isMobile" the right way to use the custom directive? I don't need an "ngIf" or anything? I ask because when I use "*isMobile" or "*!isMobile" nothing is happening (the table doesn't appear). (asterisk seems to be messing up formatting. Hope it's clear what I'm asking) – satish Jan 30 '21 at 20:48
  • You need to make sure that the directive is declared in (or imported into) any module which needs it. See this StackBlitz example: https://stackblitz.com/edit/angular-ivy-nhijlx?file=src/app/app.component.html – Will Taylor Feb 01 '21 at 08:59
  • @satish the "\*" is for structural directives it means it will update elements (https://angular.io/guide/structural-directives) "Structural directives are responsible for HTML layout. They shape or reshape the DOM's structure, typically by adding, removing, or manipulating elements. As with other directives, you apply a structural directive to a host element. The directive then does whatever it's supposed to do with that host element and its descendants. Structural directives are easy to recognize. An asterisk (*) precedes the directive attribute name as in this example." – Lautre Feb 02 '21 at 15:42
0

Angular is SPA, that's mean that all is loaded together (well exist lazy modules and so, but remember the idea). So you need know is is mobile at first of the application.

There are two aproach, using APP_Initialize or serve a different .html

Using APP_INITIALIZER it's only defined a funciton in your main.module

export function init_app(configService: ConfigService) {
    return () => configService.getIfisMobile();
}
@NgModule({
    declarations: [AppComponent],
    imports: [
       ...
    ],
    providers: [
        { provide: APP_INITIALIZER, useFactory: init_app, deps: [ConfigService], multi: true },
    ],
    bootstrap: [AppComponent]
})

Your ConfigService some like

@Injectable({
    providedIn: 'root',

})
export class ConfigService {
    isMobile:boolean=false;
    constructor(private httpClient: HttpClient) {}

    getIfisMobile() {
       return this.httpClient.get("your-url-get-isMobile")
            .toPromise()
            .then((response: any) => {
                this.isMobile = response;
            })
            .catch(err => {
            });
    }
 }

Then, any component that inject in constructor ConfigService can ask about this.configService.isMobile

The other aproach is if we can serve an .html. in in our .html we can in any way add a little javascript like

<script>var isMobile=true</script>
//or is is not Mobile
<script>var isMobile=false</script>

We can, in main.component.ts ask about it and store in a service(*)

  constructor(@Inject(WINDOW) public window: Window, private configService: ConfigService) {
    this.configService.isMobile = (window as any).isMobile;
  }

Update ::glups: I forget the window.service

/* Create a new injection token for injecting the window into a component. */
export const WINDOW = new InjectionToken('WindowToken');

/* Define abstract class for obtaining reference to the global window object. */
export abstract class WindowRef {

  get nativeWindow(): Window | Object {
    throw new Error('Not implemented.');
  }

}

/* Define class that implements the abstract class and returns the native window object. */
export class BrowserWindowRef extends WindowRef {

  constructor() {
    super();
  }

  get nativeWindow(): Window | Object {
    return window;
  }

}

/* Create an factory function that returns the native window object. */
export function windowFactory(browserWindowRef: BrowserWindowRef, platformId: Object): Window | Object {
  if (isPlatformBrowser(platformId)) {
    return browserWindowRef.nativeWindow;
  }
  return new Object();
}

/* Create a injectable provider for the WindowRef token that uses the BrowserWindowRef class. */
const browserWindowProvider: ClassProvider = {
  provide: WindowRef,
  useClass: BrowserWindowRef
};

/* Create an injectable provider that uses the windowFactory function for returning the native window object. */
export const windowProvider: FactoryProvider = {
  provide: WINDOW,
  useFactory: windowFactory,
  deps: [WindowRef, PLATFORM_ID]
};

/* Create an array of providers. */
export const WINDOW_PROVIDERS = [
  browserWindowProvider,
  windowProvider
];

(*)Store in a service is the more confortable way to get a "global variable"

Eliseo
  • 50,109
  • 4
  • 29
  • 67