kompaniietst
kompaniietst

Reputation: 250

Angular directive for detect screen size

Help me, please, how can I do that?

I want to apply directive to the divs, it will be show or hide content depending of it's value, for example: *ifViewportSize="'mobile'"

<div *ifViewportSize="'large'">PC</div>
<div *ifViewportSize="'small'">Mobile</div>

Directive: (rezult in console.log) https://stackblitz.com/edit/angular-ivy-wdl8ee

@Directive({
  selector: '[ifViewportSize]'
})
export class IfViewportSizeDirective {

size: string;

  config = {
    large: 992,
    medium: 768,
    small: 576
  };

  constructor(
    private elemRef: ElementRef,
    private vcRef: ViewContainerRef,
    private templRef: TemplateRef<any>) {

    window.onresize = (event) => {
      this.showElem();
    };
  }

  @Input() set ifViewportSize(size: string) {
    this.size = size;
  }

  ngOnInit() {
    this.showElem();
  }

  showElem() {
    console.log('size: ',this.size);

    if (this.config[this.size] < window.innerWidth) {
      this.vcRef.clear();
      this.vcRef.createEmbeddedView(this.templRef);
    }
    else this.vcRef.clear();
  }

}

Directive works only in the last div. Please tell me why?

Also I tried to create (right here, on stackblitz) separately directives ifMobile и ifTablet. I implemented there a function window.onresize, but again this function works only on the last div.

How can I fix it? If this is a wrong way to detect the screen size, how can I do this right? Thanks a lot!

Upvotes: 4

Views: 3355

Answers (2)

yurzui
yurzui

Reputation: 214007

Update

The best solution would be not to reinvent the wheel but rather use @angular/cdk/layout functionality:

if-viewport-size.directive.ts

type Size = 'small' | 'medium' | 'large';

const config = {
  small: [Breakpoints.Small, Breakpoints.XSmall],
  medium: [Breakpoints.Medium],
  large: [Breakpoints.Large, Breakpoints.XLarge]
};

@Directive({
  selector: "[ifViewportSize]"
})
export class IfViewportSizeDirective implements OnDestroy {
  private subscription = new Subscription();

  @Input("ifViewportSize") set size(value: Size) {
    this.subscription.unsubscribe();
    this.subscription = this.observer
      .observe(config[value])
      .subscribe(this.updateView);
  }

  constructor(
    private observer: BreakpointObserver,
    private vcRef: ViewContainerRef,
    private templateRef: TemplateRef<any>
  ) {}

  updateView = ({ matches }: BreakpointState) => {
    if (matches && !this.vcRef.length) {
      this.vcRef.createEmbeddedView(this.templateRef);
    } else if (!matches && this.vcRef.length) {
      this.vcRef.clear();
    }
  };

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

Usage:

<div *ifViewportSize="'large'">PC</div>
<div *ifViewportSize="'medium'">Tablet</div>
<div *ifViewportSize="'small'">Mobile </div>

Stackblitz Example

The benefits of using this solution:

  1. It avoids memory leak

  2. Resize event is listed in a performant way(outside of Angular zone)

  3. It doesn't remove and recreate template infinitely on resize but rather only once it hits breakpoint

Previous answer

It works only for the last div because you're listening for resize event through onresize property which is overrided be each directive.

window.onresize = (event) => {
   this.showElem();
};

window.onresize = (event) => { <---- you completely replace onresize event
  this.showElem();
};
...
window.onresize = (event) => { <---- one more replacement
  this.showElem(); <-------------- only this handler will be executed 
};

You could use @HostListener instead but it doesn't work in structural directives. So try using window.addEventListener instead:

ngOnInit() {
 window.addEventListener('resize', this.showElem);
 ...
}

showElem = () => { // <-- note syntax here
   ...
}

ngOnDestroy() {
  window.removeEventListener('resize', this.showElem);
}

Note: I would also consider listening to resize even outside of NgZone via ngZone.runOutsideAngular()

Now, your resize handler should work.

Forked Stackblitz

But

  • you're constantly removing template

  • it's quite unclear how you're going to handle boundaries for all your sizes. Because mobile screen will be visible even for PC size since it meets condition mobileSize < window.innerWidth

Upvotes: 5

micronyks
micronyks

Reputation: 55443

You can take advantage of let directive with $implicit context in angular being used with structural directive normally as below,


<div *ifViewportSize="let view=$implicit"> .... </div>

import { Directive, ElementRef, ViewContainerRef, TemplateRef, OnInit, Input, HostListener } from '@angular/core';

export enum VIEW{
  MOBILE ,
  TABLET ,
  PC 
}

@Directive({
  selector: '[responsive]'
})
export class IfViewportSizeDirective {

  view;

  config = {
    large: 1200,
    medium: 700,
    small: 500
  };

  constructor(private readonly viewRef: ViewContainerRef,
        private readonly templateRef: TemplateRef<any>) {  this.checkView();}

  ngOnInit(){
      window.addEventListener('resize', this.checkView.bind(this));
  }

 
  checkView() {
    this.viewRef.clear();

     console.log(window.innerWidth);
     if((0 < window.innerWidth) && (window.innerWidth < this.config['small'] )){
       
       this.view = VIEW.MOBILE;
       
     } else if((this.config['small'] < window.innerWidth) && (window.innerWidth < this.config['medium'])){

       this.view = VIEW.TABLET;
       
     } else if(this.config['medium'] < window.innerWidth){
      
      this.view = VIEW.PC;
     }
     
  
     this.viewRef.createEmbeddedView(this.templateRef,{
                    $implicit: this.view
     });
    }
  

}

FULL DEMO

NOTE: you can still optimize it.

Upvotes: 1

Related Questions