question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

CDK overlay auto calculate width when using ConnectedPositionStrategy

See original GitHub issue

Bug, feature request, or proposal:

Feature

What is the expected behavior?

That i can somehow configure the popup overlay to change it width to be the same as the parent, without having to add a bunch of custom styling to my popup component. good

What is the current behavior?

bad

What is the use-case or motivation for changing an existing behavior?

I would have to do less custom styling to get a nice dropdown

Is there anything else we should know?

In the screenshots we have changed the width using css, however this is suboptimal to having a responsive design, where the input can change their width all the time. To get around the problem we have to hard-code the width of both the input and the popups, so they look to fit together.

My best suggestion would be able to do something like this:

this.overlay.position()
      .connectedTo(target, {
        originY: 'bottom',
        originX: 'start'
      }, {
        overlayY: 'top',
        overlayX: 'start'
      })
      .immitateWidth();

Issue Analytics

  • State:open
  • Created 6 years ago
  • Reactions:15
  • Comments:10 (2 by maintainers)

github_iconTop GitHub Comments

9reactions
etiennecrbcommented, Apr 13, 2018

Hello, I have created a loading spinner overlay and encountered the same issues. I wanted the spinner to have exactly the same width and height of another component to display a transparent backdrop. The main issue was that overlay position and size were not updated when the target element was resize (for example, when the sidenav was toggled). It is because overlay position is updated only when window is resized (see ViewportRuler).

I found out a solution but not sure it’s working correctly among all browsers and for server side rendering. Suggestions are welcomed!

First, I have created a service to create watcher on element size:

import { Injectable, NgZone } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { auditTime } from 'rxjs/operators';

export interface ElementRulerRef {
  /** Emits element size when it changes */
  change: Observable<{ width: number; height: number }>;

  /** Stop watching element size */
  dispose(): void;
}

@Injectable()
export class ElementRuler {
  constructor(private zone: NgZone) {}

  /**
   * Creates an instance of ElementRulerRef to watch element size.
   */
  create(node: any, throttleTime = 100): ElementRulerRef {
    let width;
    let height;
    let animationFrameId;

    const _change = new BehaviorSubject({ width: 0, height: 0 });

    const watchOnFrame = () => {
      const currentWidth = node.clientWidth;
      const currentHeight = node.clientHeight;

      if (currentWidth !== width || currentHeight !== height) {
        width = currentWidth;
        height = currentHeight;
        _change.next({ width, height });
      }

      animationFrameId = requestAnimationFrame(watchOnFrame);
    };

    this.zone.runOutsideAngular(watchOnFrame);

    const dispose = () => {
      cancelAnimationFrame(animationFrameId);
      _change.complete();
    };

    const obs = _change.asObservable();
    const change = throttleTime > 0 ? obs.pipe(auditTime(throttleTime)) : obs;

    return { dispose, change };
  }
}

Then, I use the change observable in my overlay service:

@Injectable()
export class SpinnerService {
  constructor(private overlay: Overlay, private ruler: ElementRuler) {}

  create(el: ElementRef): () => void {
    const positionStrategy = this.overlay
      .position()
      .connectedTo(
        el,
        { originX: 'start', originY: 'top' },
        { overlayX: 'start', overlayY: 'top' }
      );
    const overlayRef = this.overlay.create({ positionStrategy });
    const spinnerPortal = new ComponentPortal(SpinnerComponent);
    overlayRef.attach(spinnerPortal);

    const rulerRef = this.ruler.create(el.nativeElement, 0);
    rulerRef.change.subscribe(({ width, height }) => {
      overlayRef.updateSize({ width, height });
      overlayRef.updatePosition();
    });

    return () => {
      overlayRef.dispose();
      positionStrategy.dispose();
      rulerRef.dispose();
    };
  }
}
5reactions
baxelson12commented, Nov 15, 2020

I took a page out of angular material’s book to overcome this.

import { ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ViewportRuler } from '@angular/cdk/scrolling';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';

@Component({
  selector: 'w-select',
  templateUrl: './select.component.html',
  styleUrls: ['./select.component.scss']
})
export class SelectComponent implements OnInit, OnDestroy {
  @ViewChild('trigger') trigger: ElementRef;
  protected readonly _destroy = new Subject<void>();
  _triggerRect: ClientRect;
  _isOpen = false;

  constructor(protected _viewportRuler: ViewportRuler, protected _changeDetectorRef: ChangeDetectorRef) {}

  ngOnInit() {
    // Check rect on resize
    this._viewportRuler
      .change()
      .pipe(takeUntil(this._destroy))
      .subscribe(() => {
        if (this._isOpen) {
          this._triggerRect = this.trigger.nativeElement.getBoundingClientRect();
          this._changeDetectorRef.markForCheck();
        }
      });
  }

  ngOnDestroy() {
    this._destroy.next();
    this._destroy.complete();
  }

  toggle() {
    // Check rect on toggle, this is for dropdown width
    this._triggerRect = this.trigger.nativeElement.getBoundingClientRect();
    this._isOpen = !this._isOpen;
  }
}
<div (click)="toggle()" cdkOverlayOrigin #trigger #origin="cdkOverlayOrigin">
<!-- ... -->
</div>

<ng-template
  cdkConnectedOverlay
  cdkConnectedOverlayHasBackdrop
  cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
  [cdkConnectedOverlayMinWidth]="_triggerRect?.width!"
  [cdkConnectedOverlayOrigin]="origin"
  [cdkConnectedOverlayOpen]="_isOpen"
  (backdropClick)="toggle()"
>
<!-- ... -->
</ng-template>

https://github.com/angular/components/tree/master/src/material/select

Seems to work well for my purposes. Idk, helped me - maybe it’ll help someone else.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Overlay | Angular Material
Overlays are dynamically added pieces of floating UI, meant to be used as a low-level building block for other components. Dialogs, tooltips, menus,...
Read more >
How do I configure a material cdk overlay position strategy ...
I solved the above problem, by implementing a check - after the dialog is opened. This is done with the resize-observer.
Read more >
Custom Overlays with Angular's CDK - Thoughtram Blog
In this post, we'll use the CDK to build a custom overlay that looks and ... How do we get the best of...
Read more >
Angular Overlay Service - Positioning Strategies - Infragistics
After flipped, if the element is still out of the viewport, Auto will use the ... (re-calculating width and/or height) in case the...
Read more >
Angular Cdk Overlay: Make Overlay Add To The Height
CDK overlay panel should get inline width and height properties defined by ... CDK overlay auto calculate width when using ConnectedPositionStrategy #10393.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found