import { Component, Input, Output, EventEmitter, OnChanges, AfterViewInit, OnDestroy, ViewChild, ElementRef, SimpleChanges, } from '@angular/core'; import MasonrySnapGridLayout from '../vanilla'; import type { ColumnsOption, LayoutInfo, LayoutMode, MasonryOptions, } from '../core'; /** * Angular standalone component for masonry-snap-grid-layout. * * Usage: * ```html * * ``` * * @example * // In your component: * renderFn = (item: MyItem): HTMLElement => { * const el = document.createElement('div'); * el.style.height = item.height + 'px'; * el.textContent = item.title; * return el; * }; */ @Component({ selector: 'masonry-snap-grid', standalone: true, template: `
`, styles: [ ` :host { display: block; width: 100%; } `, ], }) export class MasonrySnapGridComponent implements AfterViewInit, OnChanges, OnDestroy { /** Array of data items to render. */ @Input({ required: true }) items: T[] = []; /** Engine strategy. Default: 'auto' */ @Input() layoutMode: LayoutMode = 'auto'; /** Space between items in pixels. Default: 16 */ @Input() gutter = 16; /** Minimum column width in pixels. Default: 250 */ @Input() minColWidth = 250; /** * Fixed column count, or a mobile-first map of `minContainerWidth -> columns` * such as `{ 0: 1, 640: 2, 1024: 3 }`. Overrides `minColWidth` when set. */ @Input() columns?: ColumnsOption; /** Enable smooth animations. Default: true */ @Input() animate = true; /** Transition duration in ms. Default: 400 */ @Input() transitionDuration = 400; /** * Watch each item for size changes so the layout self-heals when content * settles after first measurement — images decoding, fonts swapping, * embeds resizing. Default: true */ @Input() observeItemResize = true; /** Also listen for image `load`/`error` inside items. Default: true */ @Input() watchImages = true; /** Height assumed for items that measure as zero. */ @Input() estimatedItemHeight?: number; /** * Stable identity per item. Supplying it lets the engine reuse DOM nodes * across updates instead of rebuilding them, preserving focus, selection, * and in-flight media playback. */ @Input() getItemKey?: (item: T, index: number) => string | number; /** * Function that receives a data item and returns an HTMLElement. * Required. */ @Input({ required: true }) renderItem!: MasonryOptions['renderItem']; /** Emits after every layout pass. */ @Output() layout = new EventEmitter(); @ViewChild('containerRef') private containerRef!: ElementRef; private masonry?: MasonrySnapGridLayout; /** Inputs that map straight onto engine options. */ private static readonly OPTION_INPUTS = [ 'layoutMode', 'gutter', 'minColWidth', 'columns', 'animate', 'transitionDuration', 'observeItemResize', 'watchImages', 'estimatedItemHeight', 'getItemKey', 'renderItem', ] as const; ngAfterViewInit(): void { this.masonry = new MasonrySnapGridLayout(this.containerRef.nativeElement, { items: this.items, gutter: this.gutter, minColWidth: this.minColWidth, columns: this.columns, animate: this.animate, transitionDuration: this.transitionDuration, layoutMode: this.layoutMode, observeItemResize: this.observeItemResize, watchImages: this.watchImages, estimatedItemHeight: this.estimatedItemHeight, getItemKey: this.getItemKey, renderItem: this.renderItem, onLayout: (info) => this.layout.emit(info), }); } ngOnChanges(changes: SimpleChanges): void { if (!this.masonry) return; // Previously only `items` was honoured, so binding [gutter] or // [minColWidth] to a signal or form control silently did nothing after // first render. Forward every option input. const optionChanges = MasonrySnapGridComponent.OPTION_INPUTS.filter( (name) => changes[name] ); if (optionChanges.length > 0) { this.masonry.setOptions({ layoutMode: this.layoutMode, gutter: this.gutter, minColWidth: this.minColWidth, columns: this.columns, animate: this.animate, transitionDuration: this.transitionDuration, observeItemResize: this.observeItemResize, watchImages: this.watchImages, estimatedItemHeight: this.estimatedItemHeight, getItemKey: this.getItemKey, renderItem: this.renderItem, }); } if (changes['items']) { this.masonry.updateItems(this.items); } } /** Recompute the layout immediately. */ refresh(): void { this.masonry?.refresh(); } ngOnDestroy(): void { this.masonry?.destroy(); } }