Grid Angular Component in Grid Column

Angular Component in Grid Column

Angular component templates are not always fixed and, sometimes, an application may need to load new components at runtime. The goal of this help topic is to show how to add an Angular component to the cells of a Smart.Grid column dynamically, but the shown approach can be used with other Smart HTML Elements, too. In order to load components dynamically, Angular's ComponentFactoryResolver will be used.

smart-grid (app.component.html)

The use of the smart-grid component in our main component is defined in app.component.html:

<smart-grid [dataSource]="dataSource" [columns]="columns" #grid id="grid"></smart-grid>

The properties of the Grid are bound to dataSource and columns, defined in app.component.ts. The dynamic angular component will be used in the columns definition.

Custom Angular Component (grid-cell.component.ts)

The custom Angular component, named GridCellComponent, that we will be injecting, is defined in the file grid-cell.component.ts:

import { Component } from "@angular/core";

@Component({
	selector: 'grid-cell-component',
	inputs: ['value'],
	template: `<div [ngStyle]="{'color': value > 500 ? '#6db33b' : '#e62727'}">{{ value }}</div>`
})
export class GridCellComponent {
	value: number;
}

This Angular component displays the numeric value passed to it and applies text color based on the value with [ngStyle]. This makes it suitable to display data in a Grid column.

DOM Manipulation Service (smart-dom.service.ts)

In order to load the component dynamically, we will need a Service that will load and compile the component at runtime. Below you can find the code of the Service, called smartDomService, including the method loadComponent that we will use in app.component.ts:

import {
	Injectable,
	Injector,
	EmbeddedViewRef,
	ComponentFactoryResolver,
	ApplicationRef
} from '@angular/core';
@Injectable()
export class smartDomService {
	componentRef: any;

	constructor(
		private componentFactoryResolver: ComponentFactoryResolver,
		private appRef: ApplicationRef,
		private injector: Injector
	) { }

	loadComponent(component: any, ownerElement: any) {
		// 1. Create a component reference from the component 
		const componentRef = this.componentFactoryResolver
			.resolveComponentFactory(component)
			.create(this.injector, ownerElement);
		// 2. Attach component to the appRef so that it's inside the ng component tree
		this.appRef.attachView(componentRef.hostView);

		// 3. Get DOM element from component
		const domElement = (componentRef.hostView as EmbeddedViewRef)
			.rootNodes[0] as HTMLElement;

		if (ownerElement) {
			ownerElement.appendChild(domElement);
		}

		this.componentRef = componentRef;

		return { componentRef: componentRef, domElement: domElement }
	}

	destroy() {
		this.appRef.detachView(this.componentRef.hostView);
		this.componentRef.destroy();
	}
}

Our custom Service makes use of Angular's ComponentFactoryResolver that maps our custom component to a generated ComponentFactory class that can be used to create instances of the component.

App Component and Including GridCellComponent Dynamically (app.component.ts)

The app component, which has smart-grid in its template, imports the aforementioned GridCellComponent and smartDomService.

To display custom content in the cells of a Smart.Grid column, the column's template callback has to be implemented (in the columns array). In it, we dynamically create a GridCellComponent using an instance of smartDomService and set the current cell value as the value of the GridCellComponent's instance. The column's template is then set to the dynamic component's containing element.

import { Component, ViewChild, OnInit, AfterViewInit, ViewEncapsulation, Inject } from '@angular/core';
import { GridComponent, Smart } from 'smart-webcomponents-angular/grid';
import { smartDomService } from './smart-dom.service';
import { GridCellComponent } from './grid-cell.component';

@Component({
	selector: 'app-root',
	templateUrl: './app.component.html',
	styleUrls: ['./app.component.css'],
	encapsulation: ViewEncapsulation.None
})

export class AppComponent implements AfterViewInit, OnInit {
	@ViewChild('grid', { read: GridComponent, static: false }) grid!: GridComponent;

	constructor(@Inject(smartDomService) service) {
		this.service = service;
	}

	service: smartDomService;

	getOrdersData() {
		const csvData = `Order ID,Month,Customer,Region,State,City,Salesperson,Payment Method,Category,Product Name,Unit Price,Quantity,Revenue,Shipping Cost
		1001,January,Company E,Northeast,New York,New York City,Laszlo Horvath,Credit Card,Condiments,Ketchup,5.45,500,2725,27.69
		1002,January,Company D,South,Texas,Dallas,Laura Simpson,Bank Transfer,Condiments,BBQ Sauce,7.65,750,5737.5,38.73
		1297,December,Company C,Northeast,New Jersey,Atlantic City,Marcus Kowalski,Credit Card,Fruit Preserves,Marmelade,4.5,300,1350,21.04`;
		const rows = csvData.split('\n');
		const header = rows[0].split(',');
		const data = [];
		const columns = [];
		for (let i = 0; i < header.length; i++) {
			const column = { dataField: header[i].replace(/ /ig, ''), label: header[i].trim() };
			columns.push(column);
		}
		for (let i = 1; i < rows.length; i++) {
			const row = rows[i].split(',');
			const dataRow = {};
			for (let j = 0; j < columns.length; j++) {
				const dataField = columns[j].dataField;
				dataRow[dataField] = row[j].trim();
			}
			data.push(dataRow);
		}

		return data;
	}

	dataSource = new Smart.DataAdapter({
		dataSource: this.getOrdersData(),
		dataFields: [
			'Customer: string',
			'Salesperson: string',
			'City: string',
			'PaymentMethod: string',
			'Category: string',
			'ProductName: string',
			'Quantity: number',
			'Revenue: number'
		]
	});

	columns = [
		{ label: 'Sales person', dataField: 'Salesperson', icon: 'fa-user', showIcon: true },
		{ label: 'City', dataField: 'City', showIcon: true, icon: 'fa-university' },
		{ label: 'Product', dataField: 'ProductName', icon: 'fa-product-hunt', showIcon: true },
		{ label: 'Payment Method', dataField: 'PaymentMethod', icon: 'fa-money', showIcon: true },
		{
			label: 'Quantity', dataField: 'Quantity', showIcon: true, align: 'center', icon: 'fa-plus-circle', cellsAlign: 'center', width: 120,
                template: (formatObject) => {
				const container = document.createElement('div');

				let component = this.service.loadComponent(GridCellComponent, container);

				(component.componentRef.instance as GridCellComponent).value = formatObject.value;

				formatObject.template = container;
			}
		}
	];

	ngOnInit(): void {
		// onInit code.
	}

	ngAfterViewInit(): void {
		// afterViewInit code.
		this.init();
	}

	init(): void {
		// init code.
	}
}

app.module.ts

In app.module.ts, the components, the service and the grid Angular module are imported. Of particular importance is adding GridCellComponent to entryComponents, which defines the set of components to compile when the NgModule is defined, so that they can be dynamically loaded into the view.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { GridModule } from 'smart-webcomponents-angular/grid';

import { AppComponent } from './app.component';
import { GridCellComponent } from './grid-cell.component';
import { smartDomService } from './smart-dom.service';

@NgModule({
    declarations: [AppComponent, GridCellComponent],
    imports: [BrowserModule, GridModule],
    bootstrap: [AppComponent],
    providers: [smartDomService]
})

export class AppModule { }

Result

A live version of the code presented in this tutorial can be found in the demo Angular Component in Grid Column. Here is what the application looks like when launched. GridCellComponents with conditional style are displayed in the column "Quantity".