/**
* @module module:lib/components/layout/layout.component
* @description Layout component module.
* @summary Provides `LayoutComponent` which offers a responsive grid layout
* for arranging child components using configurable rows, columns and breakpoints.
* Useful for building responsive UIs that render model and component renderers.
*
* @link {@link LayoutComponent}
*/
import { Component, Input, OnInit} from '@angular/core';
import { TranslatePipe } from '@ngx-translate/core';
import { Primitives } from '@decaf-ts/decorator-validation';
import { UIElementMetadata } from '@decaf-ts/ui-decorators';
import { NgxParentComponentDirective } from '../../engine/NgxParentComponentDirective';
import { KeyValue } from '../../engine/types';
import { IComponentProperties } from '../../engine/interfaces';
import { Dynamic } from '../../engine/decorators';
import { filterString } from '../../utils/helpers';
import { ComponentRendererComponent } from '../component-renderer/component-renderer.component';
import { ModelRendererComponent } from '../model-renderer/model-renderer.component';
import { LayoutGridGap } from '../../engine/types';
import { LayoutGridGaps } from '../../engine/constants';
import { CardComponent } from '../card/card.component';
/**
* @description Layout component for creating responsive grid layouts in Angular applications.
* @summary This component provides a flexible grid system that can be configured with dynamic
* rows and columns. It supports responsive breakpoints and can render child components within
* the grid structure. The component extends NgxParentComponentDirective to inherit common functionality
* and integrates with the model and component renderer systems.
*
* @class LayoutComponent
* @extends {NgxParentComponentDirective}
* @implements {OnInit}
* @memberOf LayoutComponent
*/
@Dynamic()
@Component({
selector: 'ngx-decaf-layout',
templateUrl: './layout.component.html',
styleUrls: ['./layout.component.scss'],
imports: [TranslatePipe, CardComponent, ModelRendererComponent, ComponentRendererComponent],
standalone: true,
})
export class LayoutComponent extends NgxParentComponentDirective implements OnInit {
/**
* @description Media breakpoint for responsive behavior.
* @summary Determines the responsive breakpoint at which the layout should adapt.
* This affects how the grid behaves on different screen sizes, allowing for
* mobile-first or desktop-first responsive design patterns. The breakpoint
* is automatically processed to ensure compatibility with the UI framework.
*
* @type {UIMediaBreakPointsType}
* @default 'medium'
* @memberOf LayoutComponent
*/
@Input()
gap: LayoutGridGap = LayoutGridGaps.collapse;
/**
* @description Media breakpoint for responsive behavior.
* @summary Determines the responsive breakpoint at which the layout should adapt.
* This affects how the grid behaves on different screen sizes, allowing for
* mobile-first or desktop-first responsive design patterns. The breakpoint
* is automatically processed to ensure compatibility with the UI framework.
*
* @type {UIMediaBreakPointsType}
* @default 'medium'
* @memberOf LayoutComponent
*/
@Input()
grid: boolean = true;
/**
* @description Media breakpoint for responsive behavior.
* @summary Determines the responsive breakpoint at which the layout should adapt.
* This affects how the grid behaves on different screen sizes, allowing for
* mobile-first or desktop-first responsive design patterns. The breakpoint
* is automatically processed to ensure compatibility with the UI framework.
*
* @type {UIMediaBreakPointsType}
* @default 'medium'
* @memberOf LayoutComponent
*/
@Input()
flexMode: boolean = false;
/**
* @description Media breakpoint for responsive behavior.
* @summary Determines the responsive breakpoint at which the layout should adapt.
* This affects how the grid behaves on different screen sizes, allowing for
* mobile-first or desktop-first responsive design patterns. The breakpoint
* is automatically processed to ensure compatibility with the UI framework.
*
* @type {UIMediaBreakPointsType}
* @default 'medium'
* @memberOf LayoutComponent
*/
@Input()
rowCard: boolean = true;
/**
* @description Maximum number of columns allowed in the grid layout.
* @summary Specifies the upper limit for the number of columns that can be displayed in the grid.
* This ensures that the layout remains visually consistent and prevents excessive columns
* from being rendered, which could disrupt the design.
*
* @type {number}
* @default 6
* @memberOf LayoutComponent
*/
@Input()
private maxColsLength: number = 6;
/**
* @description Creates an instance of LayoutComponent.
* @summary Initializes a new LayoutComponent with the component name "LayoutComponent".
* This constructor calls the parent NgxParentComponentDirective constructor to set up base
* functionality and component identification.
*
* @memberOf LayoutComponent
*/
constructor() {
super('LayoutComponent')
}
/**
* @description Getter that converts columns input to an array format.
* @summary Transforms the cols input property into a standardized string array format.
* When cols is a number, it creates an array with that many empty string elements.
* When cols is already an array, it returns the array as-is. This normalization
* ensures consistent handling of column definitions in the template.
*
* @type {string[]}
* @readonly
* @memberOf LayoutComponent
*/
get _cols(): string[] {
let cols = this.cols;
if(typeof cols === Primitives.BOOLEAN) {
cols = 1;
this.flexMode = true;
}
if (typeof cols === Primitives.NUMBER)
cols = Array.from({length: Number(cols)}, () => '');
return cols as string[];
}
/**
* @description Calculates the number of columns for a given row.
* @summary Determines the effective number of columns in a row based on the row's column definitions,
* the total number of columns in the layout, and the maximum allowed columns.
*
* @param {KeyValue | IComponentProperties} row - The row object containing column definitions.
* @returns {number} The number of columns for the row, constrained by the layout's maximum column limit.
* @memberOf LayoutComponent
*/
getRowColsLength(row: KeyValue | IComponentProperties): number {
let length: number = (row.cols as [])?.length ?? 1;
const colsLength = (this.cols as [])?.length;
const rowsLength = (typeof this.rows === Primitives.NUMBER ? this.rows : (this.rows as [])?.length) as number;
if (length > this.maxColsLength)
length = this.maxColsLength;
if (length !== colsLength) {
length = colsLength;
if (this.flexMode) {
length = row.cols.reduce((acc: number, curr: KeyValue) => {
if (rowsLength > 1)
return acc + (typeof curr['col'] === Primitives.NUMBER ? curr['col']: 1);
return acc + (
typeof curr['col'] === Primitives.NUMBER ? curr['col']:
curr['col'] === 'full' ? 0 : curr['col']
);
}, 0);
}
}
return length;
}
/**
* @description Getter that converts rows input to an array format.
* @summary Transforms the rows input property into a standardized string array format.
* When rows is a number, it creates an array with that many empty string elements.
* When rows is already an array, it returns the array as-is. This normalization
* ensures consistent handling of row definitions in the template.
*
* @type {KeyValue[]}
* @readonly
* @memberOf LayoutComponent
*/
get _rows(): KeyValue[] {
let rows = this.rows;
if (typeof rows === Primitives.NUMBER)
rows = Array.from({length: Number(rows)}, () => ({title: ''})) as Partial<IComponentProperties>[];
let rowsLength = (rows as string[]).length;
if (rowsLength === 1 && this.flexMode) {
this.children.forEach((child) => {
const row = child?.['props'].row || 1;
if (row > rowsLength) {
rowsLength += 1;
(rows as KeyValue[]).push({title: ''});
}
});
this.rows = rowsLength;
};
return (rows as KeyValue[]).map((row, index) => {
const rowsLength = this.rows;
return {
title: typeof row === Primitives.STRING ? row : row?.['title'] || "",
cols: this.children.filter((child) => {
let row = (child as UIElementMetadata).props?.['row'] ?? 1;
if (row > rowsLength)
row = rowsLength as number;
child['col'] = (child as UIElementMetadata).props?.['col'] ?? (this.cols as string[])?.length ?? 1;
if (row === index + 1)
return child;
})
};
}).map((row, index) => {
const colsLength = this.getRowColsLength(row);
row.cols = row.cols.map((c: KeyValue) => {
let {col} = c;
if (typeof col === Primitives.STRING)
col = col === 'half' ? '1-2' : col === 'full' ? '1-1': col;
if (!this.flexMode) {
if (typeof col === Primitives.NUMBER) {
col = (col === colsLength ?
`1-1` : col > colsLength ? `${colsLength}-${col}` : `${col}-${colsLength}`);
}
} else {
if (typeof col === Primitives.NUMBER)
col = (colsLength <= this.maxColsLength) ? `${col}-${colsLength}` : `${index + 1}-${col}`;
col = ['2-4', '3-6'].includes(col) ? `1-2` : col;
}
col = `dcf-child-${col}-${this.breakpoint} dcf-width-${col}`;
const childClassName = c?.['props']?.className || '';
const colClass = `${col}@${this.breakpoint} ${filterString(childClassName ,'-width-')}`;
// to prevent layout glitches, before send class to child component remove width classes
if (c?.['props']?.className)
c['props'].className = filterString(c?.['props']?.className ,'-width-', false);
return Object.assign(c, {colClass});
})
return row;
})
}
/**
* @description Angular lifecycle hook that runs after component initialization.
* @summary Called once, after the first ngOnChanges(). This method triggers the
* component's initialization process, which includes property parsing and grid
* setup. It ensures the component is properly configured before rendering.
*
* @memberOf LayoutComponent
*/
override async ngOnInit(): Promise<void> {
super.parseProps(this);
if (this.breakpoint)
this.breakpoint = `${this.breakpoint.startsWith('x') ? this.breakpoint.substring(0,2) : this.breakpoint.substring(0,1)}`.toLowerCase();
this.cols = this._cols;
this.rows = this._rows;
this.initialized = true;
}
}
Source