Charts & plugins
Heavy third-party libraries are wrapped in thin components that lazy-load their dependency only on the browser, only when the component is used. Install the peers you need; skip the ones you don't.
How lazy loading works
The plugin wrappers (LteChart, LteDatatable, LteInputFlatpickr, LteInputTomSelect) declare their library as an optional peer dependency and import it with a dynamic import() guarded by a platform check. So the library never bloats your main bundle, never runs on the server during SSR/prerender, and degrades gracefully (a static table, a plain input, an empty chart box) if the peer isn't installed — logging a single console warning. Install only what you use:
npm install chart.js flatpickr tom-select simple-datatables
Charts (Chart.js)
Charts use Chart.js 4 (MIT). <lte-chart> (ChartComponent) replaces <lte-apex-chart> and takes a standard Chart.js config through signal inputs: type and data (both required), options, plugins, height and width (a px number or any CSS length) and label (the canvas aria-label). It updates in place when data/options change, re-creates the chart only when type or plugins change, resizes with its container and destroys the chart on teardown. The chart getter returns the live Chart.js instance.
import { Component, computed, inject, signal } from '@angular/core';
import {
AppContentComponent,
CardComponent,
ChartComponent,
ChartThemeService,
verticalGradient,
} from '@adminlte/angular';
@Component({
selector: 'app-dashboard',
imports: [AppContentComponent, CardComponent, ChartComponent],
template: `
<lte-app-content title="Dashboard">
<lte-card title="Sales">
<lte-chart type="line" [data]="chartData()" [options]="chartOptions" [height]="300" label="Revenue by weekday" />
</lte-card>
</lte-app-content>
`,
})
export class DashboardPage {
private readonly theme = inject(ChartThemeService);
private readonly series = signal([30, 40, 45, 50, 49, 60, 70]);
readonly chartData = computed(() => {
const primary = this.theme.palette().colors.primary;
return {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
datasets: [
{
label: 'Revenue',
data: this.series(),
borderColor: primary,
backgroundColor: verticalGradient(primary),
fill: 'origin' as const,
},
],
};
});
readonly chartOptions = { plugins: { legend: { display: false } } };
}
Build data as a computed() so the chart reacts to your signals. An area chart is type="line" with a dataset fill; use "bar" (options.indexAxis: 'y' for horizontal), "doughnut", "pie", "radar" or "polarArea" for the rest. Without a [height] the chart keeps Chart.js' aspect ratio. For a sparkline, pass a small [height] and options that hide both scales, the legend and the tooltip.
Theme preset and dark mode
The first <lte-chart> applies the AdminLTE preset to Chart.defaults (applyChartDefaults): Bootstrap font, muted ticks, subtle horizontal-only gridlines, smooth 2px lines, rounded bars, card-coloured slice separators, circular legend markers and a Bootstrap-style tooltip, with right-to-left legends and tooltips in RTL documents. ChartThemeService resolves the --bs-* variables into a palette signal (colors.primary, colors.teal, …, text, muted, grid, surface, …) and re-reads it when <html> changes data-bs-theme, dir or data-lte-primary, so every chart re-themes live on a colour-mode switch. Take dataset colours from palette().colors rather than hard-coded hex so they follow the theme too; verticalGradient(color) (area fill) and withAlpha(color, alpha) expect those resolved rgb() colours. Chart.defaults is global, so charts you draw with Chart.js directly pick up the preset once an <lte-chart> has rendered.
Chart.js plugins
Pass inline plugins for one chart through [plugins], or register a plugin globally with Chart.register() from chart.js (in the browser, e.g. inside afterNextRender). Keep the plugins array in a field — a new array re-creates the chart:
import type { Plugin } from 'chart.js';
readonly centerText: Plugin<'doughnut'>[] = [{
id: 'centerText',
afterDraw(chart) {
const { ctx, chartArea } = chart;
ctx.save();
ctx.textAlign = 'center';
ctx.fillText('72%', (chartArea.left + chartArea.right) / 2, (chartArea.top + chartArea.bottom) / 2);
ctx.restore();
},
}];
// template:
// <lte-chart type="doughnut" [data]="usage" [plugins]="centerText" [height]="240" />
chart.js is an optional peer — install it to activate <lte-chart>. Without it the component stays empty and logs one console warning.
Datatables
<lte-datatable> renders a plain Bootstrap table from columns + data, then progressively enhances it with simple-datatables (search, sort, paginate). Columns are DatatableColumn — { key, label?, sortable? }.
<lte-datatable
[columns]="[
{ key: 'name', label: 'Name' },
{ key: 'role', label: 'Role' },
{ key: 'joined', label: 'Joined', sortable: false }
]"
[data]="rows"
[options]="{ perPage: 10 }"
/>
Flatpickr & Tom Select
Two form controls wrap popular pickers. Both are ControlValueAccessors with a [(value)] model, and accept extra library settings.
<lte-input-flatpickr
label="Event date"
[options]="{ dateFormat: 'Y-m-d' }"
[(value)]="date"
/>
<lte-input-tom-select
label="Tags"
[multiple]="true"
[options]="[{ value: 'ng', text: 'Angular' }, { value: 'ts', text: 'TypeScript' }]"
[(value)]="tags"
/>
<lte-input-flatpickr> forwards extra flatpickr options through [options]; <lte-input-tom-select> forwards Tom Select settings through [settings] and takes its choices as TomSelectOption[].
Plugin-heavy demo pages
Some demo pages use libraries that have no first-class wrapper (FullCalendar, Quill, SortableJS, jsVectorMap). The pattern there is to load the library inside Angular's afterNextRender using dynamic imports, so SSR/prerender never touches the browser-only code, and to wrap the whole init in try/catch so a failed load degrades to an empty card instead of breaking the page:
import { Component, ElementRef, afterNextRender, viewChild } from '@angular/core';
@Component({ /* ... */ })
export class CalendarPage {
private readonly calendarEl = viewChild.required<ElementRef>('calendar');
constructor() {
afterNextRender(async () => {
try {
const { Calendar } = await import('@fullcalendar/core');
const dayGrid = (await import('@fullcalendar/daygrid')).default;
const calendar = new Calendar(this.calendarEl().nativeElement, {
plugins: [dayGrid],
initialView: 'dayGridMonth',
});
calendar.render();
} catch {
// optional progressive enhancement — leave the card empty on failure
}
});
}
}
The calendar, kanban (SortableJS) and editor (Quill) demo pages all follow this afterNextRender + dynamic-import recipe. Use it whenever you integrate a browser-only library that the library doesn't wrap for you.