<bmx-report-viewer>
A report, laid out in pages and shown the way it will print: scroll through it, zoom, search it, jump from its contents, fill in its parameters, print it, or download it as a PDF - all in the browser, with no report server.
15 properties · 5 events · 12 methods · 29 parts
Example
Show markup
<bmx-report-viewer id="ex-rv" style="--bmx-report-viewer-height: 34rem"></bmx-report-viewer>
<div class="row" style="margin-block-start: 1rem">
<span class="note" id="ex-rv-out">
<strong>Everything runs in the browser.</strong> The report is a JSON definition and the rows are an array; the
viewer lays them out into A4 pages, and the same pages print and download as a tagged PDF. Open
<strong>Parameters</strong> to pick a region, type in <strong>Find</strong>, or open the side panel for the
contents. Close a region with the arrow beside it, click a column heading to sort, and use
<strong>Export</strong> for an Excel workbook, a CSV file or a web page.
</span>
</div>
<script type="module">
await customElements.whenDefined('bmx-report-viewer');
const viewer = document.getElementById('ex-rv');
const out = document.getElementById('ex-rv-out');
// The report: bands of positioned items, in points. Every computed value is an
// expression in the report language - never script - so a definition from a
// database is safe to open.
viewer.report = {
title: 'Orders by region',
language: 'en-GB',
currency: 'GBP',
page: { size: 'A4', margin: 40 },
parameters: [{ name: 'region', label: 'Region', type: 'choice', choices: [{ value: '', label: 'All regions' }, 'North', 'South'], default: '' }],
filter: "params.region = '' or region = params.region",
fields: [{ name: 'amount', value: 'quantity * price' }],
reportHeader: {
height: 210,
items: [
{ type: 'text', x: 0, y: 0, width: 515, height: 24, text: '{{ ReportTitle }}', heading: 1, style: { size: 18, bold: true, color: '#1b3f8f' } },
{ type: 'text', x: 0, y: 26, width: 515, height: 12, text: '{{ count() }} orders worth {{ format(sum(amount), "C") }}', style: { color: '#52606d' } },
// A chart is an item like any other: here, the whole report's sales by customer.
{ type: 'chart', x: 0, y: 46, width: 515, height: 150, kind: 'bar', category: 'customer', order: 'value', series: [{ value: 'amount', label: 'Sales' }], format: 'C0', labels: true },
],
},
// Headings with `sort` re-sort the report when the reader clicks them.
pageHeader: {
height: 18,
items: [
{ type: 'text', x: 90, y: 3, width: 200, height: 11, text: 'CUSTOMER', sort: 'customer', style: { size: 7.5, bold: true, color: '#52606d' } },
{ type: 'text', x: 395, y: 3, width: 116, height: 11, text: 'AMOUNT', sort: 'amount', style: { size: 7.5, bold: true, color: '#52606d', align: 'end' } },
],
},
groups: [
{
by: 'region',
bookmark: "region + ' region'",
// The reader can close and open each region in the viewer.
drilldown: 'expanded',
header: { height: 24, items: [{ type: 'text', x: 16, y: 6, width: 300, height: 14, text: '{{ region }}', heading: 2, style: { size: 11, bold: true } }] },
footer: {
height: 22,
items: [
{ type: 'line', x: 395, y: 2, width: 120, height: 0, color: '#9aa5b1' },
{ type: 'text', x: 395, y: 6, width: 120, height: 12, value: 'sum(amount)', style: { align: 'end', format: 'C', bold: true } },
],
},
},
],
detail: {
height: 15,
alternateBackground: '#f5f7fa',
items: [
{ type: 'text', x: 4, y: 2, width: 80, height: 11, value: 'date', style: { format: 'dd MMM yyyy' } },
{ type: 'text', x: 90, y: 2, width: 200, height: 11, value: 'customer' },
{ type: 'text', x: 300, y: 2, width: 60, height: 11, value: 'quantity', style: { align: 'end' } },
{ type: 'text', x: 395, y: 2, width: 116, height: 11, value: 'amount', style: { align: 'end', format: 'C' } },
],
},
pageFooter: {
height: 14,
items: [{ type: 'text', x: 315, y: 2, width: 200, height: 10, text: 'Page {{ PageNumber }} of {{ PageCount }}', style: { size: 7, align: 'end', color: '#7b8794' } }],
},
};
const customers = ['Acme Ltd', 'Bolt & Co', 'Cogworks', 'Delta Supplies', 'Evergreen Farms', 'Fulcrum Tools'];
viewer.data = Array.from({ length: 120 }, (_, i) => ({
date: `2026-${String(1 + (i % 9)).padStart(2, '0')}-${String(1 + ((i * 7) % 28)).padStart(2, '0')}`,
region: i % 3 ? 'North' : 'South',
customer: customers[i % customers.length],
quantity: 1 + ((i * 13) % 20),
price: 20 + ((i * 37) % 180),
}));
viewer.addEventListener('bmxReportRender', event => {
const { pageCount, rowCount } = event.detail;
out.textContent = `bmxReportRender: ${rowCount} rows on ${pageCount} pages.`;
});
</script>
<bmx-report-viewer
report-url="/reports/sales-by-region.json"
data-url="/api/orders"
></bmx-report-viewer>
The report is a JSON definition - page size, bands, groups, totals, formats - and the data is an array of rows. Both can be given as URLs, as attributes holding JSON, or as properties from script:
viewer.report = salesReport;
viewer.data = orders;
viewer.parameters = { region: 'North' };
WHAT THE READER GETS
Pages at their real size, laid out once and drawn the same on screen, on paper and in the PDF. A toolbar with page navigation, zoom (page width, whole page, or a percentage), find with every match highlighted, print, and PDF download. A side panel with page thumbnails and the report's contents, built from its group bookmarks. A parameters panel when the report asks for values, which runs the report again when the reader applies them.
ACCESSIBLE ON SCREEN AND IN THE PDF
The pages are real text in reading order, with headings and links a screen reader can navigate; the toolbar is buttons with names; the page and the search results are announced. The PDF it writes is tagged the same way.
INTERACTIVE REPORTS
A drill-down group shows a toggle that opens and closes it. A heading with a
sort expression sorts the report when it is clicked. An item with a
drill opens another report - one of reports, or fetched from a URL -
with parameters worked out from its row, and Back returns to where the
reader was.
EXPORTS
PDF, and the report's data as Excel or CSV, and the pages as a web page.
LARGE REPORTS
Layout runs in slices, so a hundred thousand rows never freeze the page, and only the pages in view are in the document at any moment.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
data |
data |
readonly Record<string, unknown>[] | string |
[] |
The rows: an array of objects, or JSON text. |
dataUrl |
data-url |
string |
— | A URL to fetch the rows from, as a JSON array. Takes the place of data. |
drillData |
property only | (report: string, parameters: Readonly<Record<string, unknown>>, signal: AbortSignal) => readonly unknown[] | undefined | null | Promise<readonly unknown[] | undefined | null> |
— | Rows for a drill-through, when it needs other rows than the viewer's: called with the report's name and the parameters it opens with. Return the rows, or nothing to use the viewer's own. Script only. |
exports |
exports |
readonly BmxReportFormat[] | string |
['pdf', 'xlsx', 'csv', 'html'] |
The exports the toolbar offers, in order: any of pdf, xlsx, csv, html. A space-separated list in markup. |
fileName |
file-name |
string |
— | The name the PDF is saved under. Defaults to the report's title. |
label |
label |
string |
— | The viewer's accessible name. Defaults to the report's title. |
locale |
locale |
string |
— | The locale for numbers and dates. Defaults to the report's language, then the page's. |
parameters |
parameters |
Record<string, unknown> | string |
{} |
Parameter values: an object, or JSON text. The reader can change them in the parameters panel. |
report |
report |
BmxReport | string | null |
null |
The report definition: an object, or JSON text. |
reportUrl |
report-url |
string |
— | A URL to fetch the report definition from. Used when report is not set. |
reports |
property only | Readonly<Record<string, BmxReport | string | BmxReportSource>> |
— | Reports a drill-through can open, by name: definitions, or URLs to fetch them from, or { report, data } for a report with rows of its own. A name that is not here is fetched as a URL. Script only. |
requestInit |
property only | RequestInit |
— | Options for both fetches - headers, credentials. Script only. |
sidebar |
sidebar |
'pages' | 'contents' | 'none' |
'none' |
The side panel: pages (thumbnails), contents, or none. |
toolbar |
toolbar |
boolean |
true |
Shows the toolbar. |
zoom |
zoom |
string | number |
'page-width' |
page-width, whole-page, or a scale: 1 is actual size, 1.5 is 150%. |
Events
| Event | Detail | Description |
|---|---|---|
bmxPageChange |
BmxReportPageDetail |
The page in view changed. |
bmxParametersChange |
BmxReportParametersDetail |
The reader applied new parameter values. |
bmxReportDrill |
BmxReportDrillDetail |
The reader opened a drill-through. Cancel it to open the report yourself. |
bmxReportRender |
BmxReportRenderDetail |
The report has been laid out, or could not run for want of parameters. |
bmxReportSort |
BmxReportSortDetail |
The reader sorted the report by clicking a column heading. |
Methods
| Method | Signature | Description |
|---|---|---|
back |
back() => Promise<boolean> |
Goes back from a drill-through to the report it was opened from. Resolves to whether there was one. |
download |
download(format?: BmxReportFormat, fileName?: string) => Promise<void> |
Saves the report as a file, under fileName or the report's title. |
downloadPdf |
downloadPdf(fileName?: string) => Promise<void> |
Saves the report as a PDF, under fileName or the report's title. |
exportAs |
exportAs(format?: BmxReportFormat) => Promise<Blob> |
The report as a file: pdf, xlsx (its data), csv (its data) or html (its pages). |
exportPdf |
exportPdf() => Promise<Blob> |
The report as a PDF file. |
find |
find(text: string) => Promise<number> |
Finds text in the report and shows the first match. Resolves to the number of matches. |
getDocument |
getDocument() => Promise<BmxReportDocument | null> |
The laid-out report: its pages, bookmarks and problems. |
goToPage |
goToPage(page: number) => Promise<void> |
Scrolls to a page, 1-based. |
print |
print() => Promise<void> |
Prints the report: every page, at its own paper size. |
refresh |
refresh() => Promise<void> |
Lays the report out again - after the data behind a URL has changed, say. |
zoomIn |
zoomIn() => Promise<void> |
Zooms in a step. |
zoomOut |
zoomOut() => Promise<void> |
Zooms out a step. |
Slots
| Slot | Description |
|---|---|
empty |
what to show when there is no report. |
toolbar-end |
extra controls at the end of the toolbar. |
CSS shadow parts
| Part | Description |
|---|---|
back-button |
|
body |
|
bookmark |
|
button |
|
contents |
|
drill |
|
empty |
|
error |
|
export-list |
|
export-option |
|
find |
|
find-count |
|
find-input |
|
page |
|
page-input |
|
parameter |
|
parameters |
|
parameters-hint |
|
run-button |
|
sidebar |
|
sidebar-tab |
|
sort |
|
status |
|
thumbnail |
|
thumbnails |
|
toggle |
|
toolbar |
|
viewport |
|
zoom |
CSS custom properties
| Property | Description |
|---|---|
--bmx-report-viewer-canvas |
The area behind the pages. |
--bmx-report-viewer-control |
The colour of a group's toggle and a heading's sort arrow on the page. |
--bmx-report-viewer-control-hover |
The wash over a sortable heading or a drill-through under the pointer. @part toolbar - the row of controls. @part button - a toolbar button. The Back button is also back-button. @part export-list - the list of file types under the Export button. @part export-option - one of them. @part page-input - the page number field. @part zoom - the zoom list. @part find - the search group. @part find-input - the search field. @part find-count - "3/12" beside it. @part parameters - the parameters panel. @part parameters-hint - the note that required values are missing. @part parameter - one parameter's field. @part run-button - the button that runs the report. @part body - the side panel and the pages. @part sidebar - the side panel. @part sidebar-tab - its Pages and Contents buttons. @part thumbnails - the list of page thumbnails. @part thumbnail - one of them. @part contents - the contents list. @part bookmark - one entry in it. @part viewport - the scrolling area that holds the pages. @part page - one page. Pages are always white: they are paper. @part match - a search match. Also match-current. @part toggle - the button beside a group's heading that shows or hides its details. @part sort - a heading that sorts the report when clicked. @part drill - a value that opens another report when clicked. @part status - the "Preparing report" notice. @part empty - what is shown when there is no report. @part error - the message when the report could not be loaded. |
--bmx-report-viewer-height |
How tall the viewer is. |
--bmx-report-viewer-match |
A search match on the page. |
--bmx-report-viewer-match-current |
The match being shown. |
--bmx-report-viewer-page-shadow |
The shadow under each page. |
--bmx-report-viewer-sidebar-width |
How wide the side panel is. |