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.


AdminLTE 4 · Angular port Edit on GitHub