Skip to content

Instantly share code, notes, and snippets.

@mrmeku
Created December 14, 2018 20:35
Show Gist options
  • Save mrmeku/7f6d556740f3d686c771383b661b2333 to your computer and use it in GitHub Desktop.
Save mrmeku/7f6d556740f3d686c771383b661b2333 to your computer and use it in GitHub Desktop.
Match Height Directive
import { AfterViewInit, Directive, ElementRef, Input, NgZone, OnDestroy } from '@angular/core';
import ResizeObserver from 'resize-observer-polyfill';
const HEIGHT_MATCHER_MAP: Map<string, HeightMatcher> = new Map();
class HeightMatcher {
private readonly targets = new Set<HTMLElement>();
private readonly resizeObserver = new ResizeObserver(() => {
this.matchHeightOfTargets();
});
addTarget(target: HTMLElement) {
this.targets.add(target);
this.matchHeightOfTargets();
this.resizeObserver.observe(target);
}
removeTarget(target: HTMLElement) {
this.resizeObserver.unobserve(target);
this.targets.delete(target);
// Rematch heights in case removed target was the tallest.
this.matchHeightOfTargets();
}
private matchHeightOfTargets() {
const targets = Array.from(this.targets);
const maxHeightOfTargets = targets.reduce((max, { offsetHeight }) => Math.max(max, offsetHeight), 0);
const updates = targets
.map(target => {
const marginPx = `${Math.max(maxHeightOfTargets - target.offsetHeight, 0)}px`;
if (target.style.marginBottom !== marginPx) {
return { target, marginPx };
} else {
return null;
}
})
.filter(update => Boolean(update)) as { target: HTMLElement; marginPx: string }[];
if (!updates.length) {
return;
}
requestAnimationFrame(() => {
updates.forEach(({ marginPx, target }) => {
target.style.marginBottom = marginPx;
});
});
}
}
@Directive({
selector: '[sdprefMatchHeight]'
})
export class MatchHeightDirective implements AfterViewInit, OnDestroy {
// class name to match height
@Input() sdprefMatchHeight: string;
private parsedChildSelectors: string[];
private parentElement: HTMLElement;
constructor(private readonly el: ElementRef, private readonly ngZone: NgZone) {}
ngAfterViewInit() {
this.ngZone.runOutsideAngular(() => {
this.parentElement = this.el.nativeElement;
this.parsedChildSelectors = this.sdprefMatchHeight.split(',');
this.parsedChildSelectors.forEach(selector => {
let heightMatcher = HEIGHT_MATCHER_MAP.get(selector);
if (!heightMatcher) {
heightMatcher = new HeightMatcher();
HEIGHT_MATCHER_MAP.set(selector, heightMatcher);
}
const elementToMatch = this.parentElement.querySelector(selector) as HTMLElement;
if (!elementToMatch) {
throw new Error(`Invalid selector ${selector}`);
}
heightMatcher.addTarget(elementToMatch);
});
});
}
ngOnDestroy() {
this.ngZone.runOutsideAngular(() => {
this.parsedChildSelectors.forEach(selector => {
const heightMatcher = HEIGHT_MATCHER_MAP.get(selector);
if (heightMatcher) {
const elementToMatch = this.parentElement.querySelector(selector) as HTMLElement;
if (elementToMatch) {
heightMatcher.removeTarget(elementToMatch);
}
}
});
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment