Skip to content

pontasan/tsreport-react

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tsreport-react

English | 日本語 | 简体中文 | 繁體中文 | 한국어 | Tiếng Việt | ไทย | Bahasa Indonesia | Deutsch | Français | Español | Português | العربية | עברית

A React component for previewing reports in the browser. What's displayed is the same layout that gets printed as a PDF.

Pass a template and data to <ReportPreview>, and the pages get assembled and drawn onto a Canvas. Since the preview and printing (PDF) go through the same layout engine, the appearance matches. Fetching templates, fonts, and images is delegated to a "connector," so with a minimal setup you don't even write code to load fonts.

The big picture: how the 4 packages relate

tsreport consists of 4 packages. tsreport-react is responsible only for "display in the browser."

flowchart LR
    subgraph browser["Browser"]
        react["tsreport-react\nReportPreview component"]
        coreB["tsreport-core\nLayout/rendering engine"]
        react -->|"calls internally"| coreB
    end
    subgraph app["Your app server"]
        sdk["tsreport-sdk\ncreatePreviewEndpoint (relays assets)\nTsreportClient (requests PDF printing)"]
    end
    subgraph rs["Report server"]
        editor["tsreport-editor\nCreate/publish templates\nManage fonts/images / printing API"]
        coreS["tsreport-core\nGenerates PDFs with the same engine"]
        editor --> coreS
    end
    react -->|"requests assets"| sdk
    sdk -->|"authenticates and forwards"| editor
Loading

The preview is rendered by tsreport-core inside the browser, and when printing, the report server generates the PDF using the same tsreport-core. This is the mechanism that makes "the on-screen appearance match the PDF."

Package Role
tsreport-editor The report server. Creates and publishes templates via a browser-based editor, manages fonts and images, and provides a PDF printing API
tsreport-core The layout/rendering engine. Both preview and printing go through this same engine, so the on-screen appearance matches the PDF
tsreport-react Runs core in the browser and draws onto a Canvas via a React component (this package)
tsreport-sdk A component that safely connects the browser and the report server. Provides an "asset relay endpoint" to place on your app server, and a browser-side "connector"

tsreport-react itself knows nothing about networking or authentication. Asset fetching is injected from outside as a connector, and layout and rendering are delegated to core. It has zero runtime dependencies (tsreport-core and React are peer dependencies).

Installation

npm install tsreport-react tsreport-core tsreport-sdk

React 18 or later is required. All components and hooks are provided as client components, so they can be used as-is with the Next.js App Router. tsreport-sdk is used in a configuration where assets are fetched from a server, as in the "First steps" section below (it's not needed if you're providing all fonts and templates yourself).

First steps: let the connector handle display

The simplest way to use this. Specify the address of a template already published on the report server, and the connector automatically fetches the template, fonts, and images, all on its own. You don't write any code to load fonts.

Just one thing needs to be set up. Place an asset relay endpoint on your app server (this configuration keeps credentials out of the browser — it only takes a few lines; see the tsreport-sdk README for how to build it).

Here's the browser side.

import { useMemo } from 'react'
import { ReportPreview } from 'tsreport-react'
import { createEndpointConnector } from 'tsreport-sdk'
import type { ReportTemplate } from 'tsreport-core'

function InvoicePreview() {
  // Connector pointing to the relay endpoint hosted on your app server
  const connector = useMemo(() => createEndpointConnector<ReportTemplate>({
    endpoint: '/api/report-preview',
    fetchInit: { credentials: 'include' }, // send the app's session cookie
  }), [])

  // Address of the published template to display
  const source = useMemo(() => ({
    workspace: 'workspace key',
    path: 'reports/invoice.report',
    tag: 'v1',
  }), [])

  // Data to feed into the report
  const dataSource = useMemo(() => ({
    rows: [{ item: 'Part A', amount: 12000 }],
  }), [])

  return (
    <ReportPreview
      connector={connector}
      source={source}
      dataSource={dataSource}
      workingDirectory=""
      loadingFallback={<p>Loading…</p>}
    />
  )
}

That's all it takes: the server reports the list of fonts the template needs, the connector fetches them, and the laid-out pages are displayed.

  • workingDirectory="" is a setting that fixes the base for relative paths within the template (such as images) to the root of the workspace. Since the browser has no notion of a "current directory," it's safer to specify this explicitly
  • The reason each value is wrapped in useMemo is explained next

A rule you must know: memoize the values you pass

ReportPreview and useReportDocument compare the values they receive by reference identity to decide whether re-layout is needed. This is intentional, by design.

// Bad example — the objects are recreated on every render, re-laying out all pages each time
<ReportPreview dataSource={{ rows: myRows }} fonts={{ jp: font }} />

// Good example — relayout happens only when the contents change
const dataSource = useMemo(() => ({ rows: myRows }), [myRows])
const fonts = useMemo(() => ({ jp: font }), [font])
<ReportPreview dataSource={dataSource} fonts={fonts} />

This applies to template, dataSource, fonts, source, fontIds, connector, resolveImage, and resolveSubreportTemplate. In particular, if connector is recreated every time, the font cache (held per connector instance) stops working, and fonts get fetched all over again.

Usage by purpose

I want to display a local template and only fetch fonts from the server — template + fontIds

A configuration for displaying a template being edited on the spot, like in an editor. When you pass the template directly, the connector's fetchTemplate() is not called — it's only used to fetch fonts (the connector here is the same one built in "First steps").

<ReportPreview
  template={editingTemplate}
  fontIds={useMemo(() => ['NotoSansJP'], [])}
  connector={connector}
  dataSource={dataSource}
/>

I want to load fonts myself too — fonts

A configuration where you provide both the template and the fonts yourself, without going through a server. Load fonts by passing the byte array obtained via fetch or similar to Font.load(), and pass them via the fonts property mapped by ID.

import { useEffect, useMemo, useState } from 'react'
import { ReportPreview } from 'tsreport-react'
import { Font, type ReportTemplate } from 'tsreport-core'

function Preview({ template }: { template: ReportTemplate }) {
  const [fonts, setFonts] = useState<Record<string, Font> | null>(null)

  useEffect(() => {
    let cancelled = false
    fetch('/fonts/NotoSansJP-Regular.otf')
      .then((response) => response.arrayBuffer())
      .then((buffer) => {
        if (!cancelled) setFonts({ jp: Font.load(buffer) })
      })
    return () => { cancelled = true }
  }, [])

  const dataSource = useMemo(() => ({
    rows: [{ item: 'Part A', amount: 12000 }],
  }), [])

  if (fonts === null) return <p>Loading fonts…</p>

  return (
    <ReportPreview
      template={template}
      dataSource={dataSource}
      fonts={fonts}
      loadingFallback={<p>Laying out…</p>}
    />
  )
}

The key (jp in this example) is matched against the fontFamily in the template's style.

To summarize, there are 3 ways to supply fonts. This package itself does not bundle fonts and does not fetch them automatically.

Method When to use
connector + source When displaying an already-published template. Fonts are loaded automatically according to the list of font IDs returned by the server (the configuration used in "First steps")
connector + fontIds When you have the template locally but want to fetch only the fonts from the server
Pass Font directly via fonts When you're loading fonts yourself

Note that you must not guess which fonts are needed based on the template yourself. Which fonts to use is correctly determined by the fontIds the server computes using the same logic as at print time, and following this is a condition for the preview to match printing.

I want to control errors and loading myself — useReportDocument()

Use this when you want to handle state yourself instead of leaving it to the component. ReportPreview is a thin wrapper around this hook.

import { useReportDocument, ReportPage } from 'tsreport-react'

import type { DataSource, Font, ReportTemplate } from 'tsreport-core'

function CustomPreview({ template, dataSource, fonts }: {
  template: ReportTemplate
  dataSource: DataSource
  fonts: Record<string, Font>
}) {
  const { document, fonts: effectiveFonts, loading, error } = useReportDocument(
    template, dataSource, { fonts },
  )

  if (loading) return <Spinner />
  if (error) return <ErrorPanel message={error.message} />
  if (document === null) return null

  return (
    <div>
      <p>{document.pages.length} pages</p>
      {document.pages.map((page, index) => (
        <ReportPage
          key={index}
          page={page}
          fonts={effectiveFonts ?? undefined}
          images={document.images}
        />
      ))}
    </div>
  )
}

This hook has no method for retrying. If you want to trigger layout again, recreate the reference of the value you're passing in as a dependency (template, dataSource, connector, etc.).

It's important that you pass the returned fonts to ReportPage. This is a record of "the fonts actually used for measurement," including those loaded via the connector and those brought in by subreports. If you don't pass this, the calculated character widths and the glyphs actually drawn will end up out of sync (ReportPreview does this automatically internally).

I want to render just one page — <ReportPage>

Use this when implementing your own page navigation, or when creating thumbnails.

<ReportPage page={document.pages[currentPage]} fonts={fonts} images={document.images} scale={0.4} />

I want large pages to display smoothly — tileSize

Splits a page into small tiles and draws the tiles that enter the viewport first. This is effective for very large pages or pages with a lot of transparency, where drawing with a single Canvas would freeze up.

<ReportPage page={page} fonts={fonts} images={images} tileSize={512} />

Tiles aren't drawn until they enter the viewport, and drawing is carried out one tile at a time with intervals in between, so scrolling and input responsiveness are preserved.

tileSize is exclusive to <ReportPage>. It is not forwarded to <ReportPreview>, so if you want to use tiled rendering, combine useReportDocument() with <ReportPage>.

I want to zoom in/out — scale

<ReportPreview template={template} dataSource={dataSource} fonts={fonts} scale={1.5} />

scale is the display magnification. Sharpness on high-resolution displays is handled separately by devicePixelRatio (by default window.devicePixelRatio is used, so you usually don't need to specify it).

Implementing your own PreviewConnector

If you're not using tsreport-sdk and want to set up your own fetching path, pass an object with the following 4 methods. This package has no involvement whatsoever in how these fetch the data.

import type { PreviewConnector } from 'tsreport-react'

const connector: PreviewConnector = {
  async fetchTemplate(source) {
    // source: { workspace, path, tag }
    const res = await fetch(`/my-api/template?path=${encodeURIComponent(source.path)}`)
    return await res.json() // { template, fontIds }
  },

  async fetchFont(fontId) {
    const res = await fetch(`/my-api/fonts/${encodeURIComponent(fontId)}`)
    if (res.status === 404) return null // return null when not found
    return new Uint8Array(await res.arrayBuffer())
  },

  async resolveImage(ref) {
    // ref is the reference string exactly as written in the template. Do not add your own interpretation rules
    const res = await fetch(`/my-api/file?ref=${encodeURIComponent(ref)}`)
    if (res.status === 404) return null
    return new Uint8Array(await res.arrayBuffer())
  },

  async fetchSubreportTemplate(ref, context) {
    // context: { workingDirectory }
    const res = await fetch(`/my-api/subreport?ref=${encodeURIComponent(ref)}&dir=${encodeURIComponent(context.workingDirectory)}`)
    if (res.status === 404) return null
    return await res.json() // { template, fontIds }
  },
}
Method When it's called Return value
fetchTemplate(source) When source is specified without passing template. If template is passed directly, this is not called { template, fontIds }
fetchFont(fontId) When a font ID that hasn't been tried yet appears (both from the template and from subreports) The font's byte array. null for an unknown ID
resolveImage(ref) When the resolveImage option is not specified and the image is not in the cache A byte array / data URI / URL string. null if not found
fetchSubreportTemplate(ref, context) When the resolveSubreportTemplate option is not specified and the subreport is not in the cache { template, fontIds }. null for an unknown reference

Rules to follow when implementing this.

  • null means "not found." It is not an exception. How things look when an image is null (icon display, blank, error) is determined by the template's onError setting
  • Return errors as exceptions (reject). Swallowing them causes the preview to silently break. A rejected exception is delivered as-is to the caller as error
  • Do not manipulate ref. The string written in the template is passed as-is. If you add your own path interpretation rules on the connector side, it will diverge from the printed output
  • Authentication is your app's responsibility. Don't design the connector itself to hold credentials (if sending a cookie, leave it to the browser's mechanism, e.g. credentials: 'include')

The same font ID is never fetched or parsed a second time as long as it's the same connector instance (this is cached even across components). Recreating the connector on every render loses this benefit, so be sure to memoize it.

How asynchronous assets and layout mesh together

tsreport-core's layout runs synchronously, but fetching assets in the browser is asynchronous. This gap is bridged as follows.

  1. Run the layout. When it encounters images or subreports that aren't at hand yet, it treats them as "absent" for the time being while starting to fetch them
  2. Wait for the fetch to finish
  3. Add the newly obtained assets (and fonts brought in by subreports), and run the layout again
  4. Repeat until there's nothing new left to fetch

Because of this convergence process, images are placed once their actual dimensions are known, and aspect-ratio-preserving settings like retainShape are calculated correctly. You don't need to be conscious of this behavior on the calling side, but it helps to know that it doesn't complete on the first pass.

API reference

<ReportPreview>

Only dataSource is required. The template is given via either template or connector + source (an error occurs if neither is given).

Property Type Required Description
dataSource DataSource The data to flow into the report ({ rows, parameters?, resources? })
template ReportTemplate The template definition. If omitted, it's fetched via connector + source
connector PreviewConnector The asset fetching path. Used together with source, it also fetches the template itself
source PreviewTemplateSource The address of the published template ({ workspace, path, tag }). Requires connector
fontIds string[] The list of font IDs to load via the connector, when passing template directly
fonts Record<string, Font> Font ID → Font. Used for both measuring character widths and drawing
resolveImage (ref: string) => Promise<Uint8Array | string | null> A function to resolve images. Takes priority over the connector's resolveImage if specified
resolveSubreportTemplate SubreportTemplateResolver A function to resolve subreports (synchronous). Takes priority over the connector if specified
workingDirectory string The base directory for relative references. In the browser, explicitly specifying "" (root) is recommended
scale number The display magnification. Default: 1
devicePixelRatio number An override for the device's pixel density. Default: window.devicePixelRatio
pageGap number The vertical gap between pages (CSS pixels). Default: 16
pageBackground string | null The page background color. null to not fill a background. Default: #FFFFFF
loadingFallback ReactNode The content displayed while laying out. If omitted, nothing is displayed
className / style string / CSSProperties Applied to the outer container. style can override the default styles
pageClassName / pageStyle string / CSSProperties Applied to each page's Canvas

There are no callback properties (onLoad, onError, etc.). On failure, an exception is thrown during render. If you want to handle errors yourself, use useReportDocument().

<ReportPage>

Property Type Required Description
page RenderPage One element of RenderDocument.pages
fonts Record<string, Font> If specified, glyphs are drawn. Pass the fonts returned by useReportDocument()
images Record<string, string | Uint8Array> Pass RenderDocument.images as-is
scale number The display magnification. Default: 1
devicePixelRatio number An override for the device's pixel density. Default: window.devicePixelRatio
background string | null The background color. null to not fill it. Default: #FFFFFF
tileSize number If specified, splits the page into tiles of this size (CSS pixels) and draws them in the order they enter the viewport
className / style string / CSSProperties Applied to the Canvas element (the wrapper, when tiles are used)

The Canvas's pixel count is page width × scale × devicePixelRatio, and its CSS-level size is page width × scale.

useReportDocument(template, dataSource, options?)

The first argument is a ReportTemplate or null (when fetching via a connector).

The options are the 7 properties fonts / resolveImage / resolveSubreportTemplate / workingDirectory / connector / source / fontIds, meaning the same as the identically-named properties of ReportPreview. Anything specified explicitly always takes priority over the connector.

The return value has the following 4 fields, always all present.

Field Type Description
document RenderDocument | null The completed layout. null while loading and after a failure (the previous successful result is not retained)
fonts Record<string, Font> | null A record of the fonts actually used for measurement. Pass this to ReportPage
loading boolean Whether layout or asset fetching is in progress
error Error | null The reason for failure. The hook does not throw an exception, but returns it here instead

createFontMap(fonts) / getTextMeasurer(font)

Helper functions for building the measurer to pass to tsreport-core. Normally these are called internally when you use the fonts property, so there's no need to use them directly. getTextMeasurer() returns the same measurer for the same Font, so the shaping cache is shared.

Testing

Command Content
npm test Unit/integration tests (verified by loading real fonts on jsdom)
npm run test:live Real-server integration tests, which assume a running tsreport-editor and seed data

test:live reproduces, in a single process, a production-like 3-tier configuration of browser → app endpoint → API server, and verifies using actual fonts, images, and subreports.

Runtime environment

  • React 18+ / React DOM 18+ (peer dependency)
  • tsreport-core (peer dependency)
  • A browser with Canvas 2D support
  • No runtime dependency packages
  • Both ESM / CommonJS supported
  • All exports are client components (Next.js App Router compatible)

Related projects

License

tsreport-react is available under either the MIT License or the Apache License 2.0 (Apache License 2.0), at the user's choice (SPDX: MIT OR Apache-2.0).

About

React components for rendering multi-page report previews in the browser with tsreport-core

Topics

Resources

License

Unknown and 2 other licenses found

Licenses found

Unknown
LICENSE
Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors