Angular/Template components
Loading live demo…from dhtmlx.github.io
Angular
Template components
Render task text, status buttons, and header filters with standalone Angular template components.
- template-components.ts
- template-components.html
- template-components.css
- header-filter-template.component.ts
- status-button-template.component.ts
- task-text-template.component.ts
src/app/samples/template-components/template-components.ts View on GitHub
import { Component, ViewEncapsulation } from '@angular/core';
import type { GanttStatic } from '@dhtmlx/trial-angular-gantt';
import { DhxGanttComponent, templateComponent, type TaskFilter } from '@dhtmlx/trial-angular-gantt';
import { DemoToolbarComponent } from '../../shared/demo-toolbar/demo-toolbar';
import { createBasicDemoData } from '../../shared/demo-data';
import { HeaderFilterTemplateComponent } from './header-filter-template.component';
import { StatusButtonTemplateComponent } from './status-button-template.component';
import { TaskTextTemplateComponent } from './task-text-template.component';
@Component({
selector: 'app-template-components',
standalone: true,
imports: [DemoToolbarComponent, DhxGanttComponent],
templateUrl: './template-components.html',
styleUrl: './template-components.css',
encapsulation: ViewEncapsulation.None,
})
export class TemplateComponentsComponent {
private readonly initial = createBasicDemoData();
private ganttInstance: GanttStatic | null = null;
tasks = this.initial.tasks.map((task, index) => ({
...task,
completed: index % 2 === 0,
}));
links = this.initial.links;
theme = 'terrace';
locale = 'en';
filterLabel = 'All';
taskFilter: TaskFilter = null;
readonly templates = {
task_text: (_start: Date, _end: Date, task: any) =>
templateComponent(TaskTextTemplateComponent, {
task,
onIconClick: () => this.handleTaskIconClick(task),
}),
};
readonly config: any = {
scales: [
{ unit: 'year', step: 1, format: '%Y' },
{ unit: 'month', step: 1, format: '%F, %Y' },
{ unit: 'day', step: 1, format: '%d %M' },
],
columns: [
{ name: 'text', tree: true, width: 200, resize: true },
{ name: 'start_date', width: 130, align: 'center', resize: true },
{ name: 'duration', width: 90, align: 'center', resize: true },
{
name: 'custom',
align: 'center',
width: 160,
resize: true,
label: this.createHeaderLabelDescriptor(),
template: (task: any) =>
templateComponent(StatusButtonTemplateComponent, {
task,
onToggle: () => this.toggleCompleted(task),
}),
},
{ name: 'add', width: 44 },
],
date_format: '%Y-%m-%d %H:%i',
row_height: 50,
scale_height: 90,
};
onReady({ instance }: { instance: GanttStatic }): void {
this.ganttInstance = instance;
}
toggleTheme(): void {
this.theme = this.theme === 'terrace' ? 'dark' : 'terrace';
}
toggleLocale(): void {
this.locale = this.locale === 'en' ? 'es' : 'en';
}
private handleTaskIconClick(task: any): void {
this.toggleCompleted(task);
}
private handleFilterSelected(filterType: string): void {
if (filterType === 'done') {
this.filterLabel = 'Done';
} else if (filterType === 'notDone') {
this.filterLabel = 'Not Done';
} else {
this.filterLabel = 'All';
}
this.updateTaskFilter();
const customColumn = this.getCustomColumn();
if (customColumn) {
customColumn.label = this.createHeaderLabelDescriptor();
}
}
private updateTaskFilter(): void {
const filterLabel = this.filterLabel;
if (filterLabel === 'All') {
this.taskFilter = null;
return;
}
this.taskFilter = (task) => {
if (filterLabel === 'Done') return !!task['completed'];
if (filterLabel === 'Not Done') return !task['completed'];
return true;
};
}
private toggleCompleted(task: any): void {
task.completed = !task.completed;
this.ganttInstance?.updateTask(task.id);
}
private getCustomColumn(): any | null {
const columns = Array.isArray(this.config?.columns) ? this.config.columns : [];
return columns.find((column: any) => column?.name === 'custom') ?? null;
}
private createHeaderLabelDescriptor() {
return templateComponent(HeaderFilterTemplateComponent, {
currentFilterLabel: this.filterLabel,
onFilterSelected: (filterType: string) => this.handleFilterSelected(filterType),
});
}
}
src/app/samples/template-components/template-components.html View on GitHub
<div class="card template-components-demo">
<dhx-demo-toolbar title="Template Components">
<button (click)="toggleTheme()">
Toggle Theme ({{ theme }})
</button>
<button (click)="toggleLocale()">
Toggle Locale ({{ locale }})
</button>
</dhx-demo-toolbar>
<div class="gantt-host">
<dhx-gantt
[tasks]="tasks"
[links]="links"
[templates]="templates"
[config]="config"
[filter]="taskFilter"
[theme]="theme"
[locale]="locale"
(ready)="onReady($event)"
>
</dhx-gantt>
</div>
<div class="note">
Demonstrates Angular components inside template hooks:
<code>templates.task_text</code>,
<code>config.columns[].template</code>,
and <code>config.columns[].label</code>.
</div>
</div>
src/app/samples/template-components/template-components.css View on GitHub
@import "@dhtmlx/trial-angular-gantt/dist/angular-gantt.css";
.dhx-gantt-root {
height: 100%;
width: 100%;
}
.template-components-demo .gantt_grid_scale {
overflow: visible;
position: relative;
z-index: 4;
}
.template-components-demo .gantt_grid_head_cell.gantt_grid_head_custom {
overflow: visible;
}
.template-components-demo .gantt_grid_data {
position: relative;
z-index: 1;
}
.template-components-demo .header-filter-root {
position: relative;
z-index: 20;
}
.template-components-demo .header-filter-menu {
z-index: 30;
}
src/app/samples/template-components/header-filter-template.component.ts View on GitHub
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'dhx-header-filter-template',
standalone: true,
imports: [CommonModule],
template: `
<div class="header-filter-root">
<button type="button" class="header-filter-trigger" (click)="toggleMenu($event)">
Show: {{ currentFilterLabel }}
</button>
<div class="header-filter-menu" *ngIf="menuOpen">
<button type="button" (click)="select('done', $event)">Show Done</button>
<button type="button" (click)="select('notDone', $event)">Show Not Done</button>
<button type="button" (click)="select('all', $event)">Show All</button>
</div>
</div>
`,
styles: [`
.header-filter-root {
position: relative;
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
text-align: center;
line-height: 20px;
}
.header-filter-trigger {
border: 0;
border-radius: 6px;
padding: 4px 8px;
font-size: 12px;
font-weight: 600;
background: #f2f6f9;
color: #1c2b36;
cursor: pointer;
}
.header-filter-menu {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
z-index: 10;
display: flex;
flex-direction: column;
background: #fff;
border: 1px solid #d9dee3;
border-radius: 6px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
}
.header-filter-menu button {
border: 0;
background: transparent;
padding: 6px 8px;
text-align: left;
cursor: pointer;
font-size: 12px;
}
.header-filter-menu button:hover {
background: #f4f7fa;
}
`],
})
export class HeaderFilterTemplateComponent {
@Input() currentFilterLabel = 'All';
@Input() onFilterSelected: ((filterType: string) => void) | null = null;
menuOpen = false;
toggleMenu(event?: Event): void {
event?.stopPropagation();
this.menuOpen = !this.menuOpen;
}
select(filterType: string, event?: Event): void {
event?.stopPropagation();
this.menuOpen = false;
this.onFilterSelected?.(filterType);
}
}
src/app/samples/template-components/status-button-template.component.ts View on GitHub
import { Component, Input, ChangeDetectionStrategy, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'dhx-status-button-template',
standalone: true,
imports: [CommonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<button
type="button"
class="status-button"
[class.completed]="isCompleted()"
(click)="onToggle?.()">
{{ buttonText() }}
</button>
`,
styles: [`
.status-button {
line-height: normal;
border: 0;
border-radius: 6px;
padding: 4px 8px;
font-size: 12px;
font-weight: 600;
color: #fff;
cursor: pointer;
background: #0f7ed9;
}
.completed {
background: #2f9d44;
}
`],
})
export class StatusButtonTemplateComponent {
@Input() set task(value: any) {
this._task = value;
}
private _task: any = null;
@Input() onToggle: (() => void) | null = null;
//Computed signals - IMMUNE to NG0100
isCompleted = computed(() => !!this._task?.completed);
buttonText = computed(() => this._task?.completed ? 'Done' : 'Not Done');
}
src/app/samples/template-components/task-text-template.component.ts View on GitHub
import { Component, Input, ChangeDetectionStrategy, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'dhx-task-text-template',
standalone: true,
imports: [CommonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="task-text-chip">
<button type="button" class="task-text-icon" (click)="onIconClick?.()">
{{ statusIcon() }}
</button>
<span class="task-text-value">{{ taskText() }}</span>
</div>
`,
styles: [`
.task-text-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 6px;
border: 1px solid #c7ccd1;
border-radius: 999px;
background: #fff;
color: #1c2b36;
line-height: 1.2;
}
.task-text-icon {
width: 18px;
height: 18px;
border: 0;
border-radius: 50%;
font-size: 12px;
background: #edf1f4;
color: #1c2b36;
cursor: pointer;
padding: 0;
line-height: 18px;
text-align: center;
}
.task-text-value {
font-size: 12px;
font-weight: 600;
white-space: nowrap;
}
`],
})
export class TaskTextTemplateComponent {
@Input() set task(value: any) {
this._task = value;
}
private _task: any = null;
@Input() onIconClick: (() => void) | null = null;
// Computed signals - IMMUNE to NG0100
statusIcon = computed(() => this._task?.completed ? '[x]' : '[ ]');
taskText = computed(() => this._task?.text || '');
}