Charts & JS Plugins
AdminLTE ships no charting library of its own — instead the demo shows the canonical Blazor pattern for driving any third-party JS plugin (ApexCharts, FullCalendar, SortableJS, Quill, jsvectormap) through JS interop.
The pattern in three steps
- Load the plugin from a CDN in
App.razor(CSS in<head>, JS before</body>), alongside a small interop module of your own. - Capture the target element in your page with
@refinto anElementReference. - Initialize the plugin on first render from
OnAfterRenderAsync(firstRender)via@inject IJSRuntime, passing the element reference to your interop function.
1. Load plugin CDNs in App.razor
@* <head> — plugin CSS *@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jsvectormap@1.5.3/dist/css/jsvectormap.min.css">
@* before </body>, after blazor.web.js and <AdminLteScripts /> *@
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.37.1/dist/apexcharts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.20/index.global.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.7/Sortable.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jsvectormap@1.5.3/dist/js/jsvectormap.min.js"></script>
<script src="js/demo-interop.js"></script>
2. A small interop module
Put one function per plugin on a namespaced global. Each takes the DOM element(s) passed from Blazor:
// wwwroot/js/demo-interop.js
window.adminlteDemo = {
salesAreaChart(el) {
if (!el) return;
new ApexCharts(el, {
series: [{ name: 'Digital Goods', data: [28, 48, 40, 19, 86, 27, 90] }],
chart: { height: 300, type: 'area', toolbar: { show: false } },
colors: ['#0d6efd'],
stroke: { curve: 'smooth' },
}).render();
},
};
3. Initialize from the page
Capture the element with @ref, inject IJSRuntime, and call your function once on first render:
@page "/"
@inject IJSRuntime JS
<AppContent Title="Dashboard">
<Card Title="Sales Value">
<ChildContent>
<div @ref="_chartRef"></div>
</ChildContent>
</Card>
</AppContent>
@code {
private ElementReference _chartRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("adminlteDemo.salesAreaChart", _chartRef);
}
}
}
Always guard plugin init with if (firstRender). JS interop is not available during prerendering — it only works after the component is interactive — so initialise in OnAfterRenderAsync, never in OnInitialized.
Passing multiple elements
An interop function can take several element references. The demo's FullCalendar page passes the calendar element, an external-events container, and a checkbox:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("adminlteDemo.calendar", _cal, _externalEvents, _removeAfterDrop);
}
}
The same approach drives SortableJS (Kanban lanes), Quill (rich-text editor) and jsvectormap (world map + sparklines) in the demo — see samples/AdminLte.Demo/wwwroot/js/demo-interop.js for the full set of verbatim option objects.