Positioning in ng-bootstrap

杨旭 bio photo By 杨旭

Positioning in ng-bootstrap

A set of utility methods that can be use to retrieve position of DOM elements.It is meant to be used where we need to absolute-position DOM elements in relation to other, existing elements (this is the case for tooltips, popovers, typeahead suggestions etc.).

There is only one public function exported for the consumer

The positionElements function is used set the position of targetElement depends on the position of hostElement. Specify the position with placement parameter, which can be "top" | "bottom" | "left" | "right" or mixed value like top-center. The appendToBody is a boolean value that tells Positioning to calculate position relative to body. After calculation, set the target element’s top and left.

const positionService = new Positioning();
export function positionElements(
    hostElement: HTMLElement, targetElement: HTMLElement, placement: string, appendToBody?: boolean): void {
  const pos = positionService.positionElements(hostElement, targetElement, placement, appendToBody);

  targetElement.style.top = `${pos.top}px`;
  targetElement.style.left = `${pos.left}px`;
}

The positionElements function returns a ClientRect instance to describe the position in the screen.

positionElements(hostElement: HTMLElement, targetElement: HTMLElement, placement: string, appendToBody?: boolean):
      ClientRect

ClientRect is an Object that representing the area of the screen occupied by the range.

interface ClientRect {
    bottom: number;
    readonly height: number;
    left: number;
    right: number;
    top: number;
    readonly width: number;
}

If appendToBody is true

Use this.offset(hostElement, false) to get the offset of the HostElement first.

    const hostElPosition = appendToBody ? this.offset(hostElement, false) : this.position(hostElement, false);

Calculate the shiftWidth and shiftHeight that will be used to move the target element depends on the placementSecondary

    const shiftWidth: any = {
      left: hostElPosition.left, 
      center: hostElPosition.left + hostElPosition.width / 2 - targetElement.offsetWidth / 2,
      right: hostElPosition.left + hostElPosition.width
    };
    const shiftHeight: any = {
      top: hostElPosition.top,
      center: hostElPosition.top + hostElPosition.height / 2 - targetElement.offsetHeight / 2,
      bottom: hostElPosition.top + hostElPosition.height
    };

Horizontal

  • left - the left position of the host element.
  • center - the target element and the host element align by center line.
  • right - the right position of the hose element.

Vertical

  • top - the top position of the host element.
  • center - the target element and the host element align by middle line.
  • bottom: the bottom position of the host element.

Get the placement by splitting the parameter

    const placementPrimary = placement.split('-')[0] || 'top';
    const placementSecondary = placement.split('-')[1] || 'center';

Init the target element’s position, just to the top-left of body or parent element.

    const targetElPosition: ClientRect = {
      height: targetElBCR.height || targetElement.offsetHeight,
      width: targetElBCR.width || targetElement.offsetWidth,
      top: 0,
      bottom: targetElBCR.height || targetElement.offsetHeight,
      left: 0,
      right: targetElBCR.width || targetElement.offsetWidth
    };

Then calculate the final position by placement

    switch (placementPrimary) {
      case 'top':
        targetElPosition.top = hostElPosition.top - targetElement.offsetHeight;
        targetElPosition.bottom += hostElPosition.top - targetElement.offsetHeight;
        targetElPosition.left = shiftWidth[placementSecondary];
        targetElPosition.right += shiftWidth[placementSecondary];
        break;
      case 'bottom':
        targetElPosition.top = shiftHeight[placementPrimary];
        targetElPosition.bottom += shiftHeight[placementPrimary];
        targetElPosition.left = shiftWidth[placementSecondary];
        targetElPosition.right += shiftWidth[placementSecondary];
        break;
      case 'left':
        targetElPosition.top = shiftHeight[placementSecondary];
        targetElPosition.bottom += shiftHeight[placementSecondary];
        targetElPosition.left = hostElPosition.left - targetElement.offsetWidth;
        targetElPosition.right += hostElPosition.left - targetElement.offsetWidth;
        break;
      case 'right':
        targetElPosition.top = shiftHeight[placementSecondary];
        targetElPosition.bottom += shiftHeight[placementSecondary];
        targetElPosition.left = shiftWidth[placementPrimary];
        targetElPosition.right += shiftWidth[placementPrimary];
        break;
    }

Round the offset value and returns.

    targetElPosition.top = Math.round(targetElPosition.top);
    targetElPosition.bottom = Math.round(targetElPosition.bottom);
    targetElPosition.left = Math.round(targetElPosition.left);
    targetElPosition.right = Math.round(targetElPosition.right);

    return targetElPosition;

How to use it?

There is a button and a tooltip content

<button #hostButton class="btn">A button</button>

<div #toptipWin class="tooltip show">
  <div class="tooltip-inner">
    Content in tooltip!
  </div>
</div>

Get element reference in component and use positioning to set the position

  @ViewChild('hostButton') host: ElementRef;
  @ViewChild('toptipWin') target: ElementRef;
  ngOnInit() {
    positionElements(
      this.host.nativeElement,
      this.target.nativeElement,
      'top-right',
    );
  }