SDK

@labelviewer/elements is one JavaScript module with both browser components: <zpl-viewer> (the Viewer) and <zpl-editor> (the Editor). Your install serves it at /sdk/, so there is nothing to install: load the module from the install and the fonts and the API come along automatically. This page covers loading it, for both components.

On this install: @labelviewer/elements 1.0.1

Copy this into your page. The URL and the hash are this server's; the module defines <zpl-viewer> and <zpl-editor>.

<script type="module"
        src="https://labelviewer.example.internal/sdk/1.0.1/labelviewer-elements.min.js"
        integrity="sha384-UiBvh+sXcRxua68FPRo4L0qmnW3QoOGISuBtFtqldU8koeeZIyyBenyqkujDz7gV"
        crossorigin="anonymous"></script>
  • https://labelviewer.example.internal/sdk/1.0.1/recommendedImmutable, cached for a year, pairs with integrity.
  • https://labelviewer.example.internal/sdk/latest/convenienceAlways the installed version, revalidated on every load; no hash possible.

Versioned or latest#

URLCachingUse it when
/sdk/<version>/labelviewer-elements.min.jsimmutable, one yearProduction. Pin the version and add integrity. After a server upgrade you update the URL (old versions are not kept).
/sdk/latest/labelviewer-elements.min.jsrevalidated every timeDevelopment, or when you want every server upgrade to reach your app without a change. No integrity possible.

labelviewer-elements.js (readable) sits next to the minified file. Every /sdk/ file is served with Access-Control-Allow-Origin: *, so your app can load it from any origin. Keep the URL in one place in your configuration: after an upgrade the old versioned URL answers 404 (Install › Upgrades).

An SDK loaded from the install

  • fetches its fonts from the same place (/sdk/<version>/fonts/),
  • uses that install for the Editor's Test with API and API tab (apiBase defaults to the install's origin), and
  • asks that install for its license (GET /v1/info, once per page every 10 minutes): a component whose product is not licensed shows "This label component is not licensed for this installation. Contact your administrator." instead of its content, and on a trial license the viewer's labels are watermarked and the editor's canvas shows an X (License). If the install cannot be reached the components keep working and log one console warning.

Load it per framework#

The module defines both tags once per page. Code that uses them can wait with await customElements.whenDefined('zpl-viewer') (or 'zpl-editor').

Plain HTML#

<script type="module" src="https://labelviewer.example.internal/sdk/1.0.1/labelviewer-elements.min.js"
        integrity="sha384-<hash from this page>" crossorigin="anonymous"></script>

<zpl-viewer zoom="fit"></zpl-viewer>

Dynamic import#

To load the module only where it is used (for example only on the template designer route), import the URL. Bundlers must be told to leave it to the browser: /* @vite-ignore */ for Vite, /* webpackIgnore: true */ for webpack and the Angular CLI.

const LABELVIEWER_SDK = 'https://labelviewer.example.internal/sdk/1.0.1/labelviewer-elements.min.js';
let sdk;
export const loadLabelviewer = () => (sdk ??= import(/* @vite-ignore */ /* webpackIgnore: true */ LABELVIEWER_SDK));

A dynamic import() cannot carry an integrity hash; use the script tag where you need Subresource Integrity.

Angular#

Either add the script tag to src/index.html, or load the module on demand from a service:

export const LABELVIEWER_SDK = 'https://labelviewer.example.internal/sdk/1.0.1/labelviewer-elements.min.js';

@Injectable({ providedIn: 'root' })
export class LabelviewerService {
    private loading?: Promise<unknown>;
    load() {
        return (this.loading ??= import(/* webpackIgnore: true */ /* @vite-ignore */ LABELVIEWER_SDK));
    }
}
  • Components that use the tags need schemas: [CUSTOM_ELEMENTS_SCHEMA]. [prop] binds DOM properties, (event) listens to the element's events; $event is typed Event in templates, so cast it to read detail.
  • Zones. With zone.js, the element's own work (pointer moves while dragging, timers, font downloads) triggers change detection, which is harmless but wasteful. Prefer zoneless change detection, or provideZoneChangeDetection({ eventCoalescing: true }).
  • Under a strict style-src, turn off critical CSS inlining ("optimization": { "styles": { "inlineCritical": false } }); that is about Angular's own CSS, not the elements.

React#

Load the module in an effect with the loadLabelviewer() above (or put the script tag in index.html).

  • React 19 sets custom element properties and adds listeners for on<event> props, so once the module is loaded the elements work as JSX. Write the event props in lower case (onchange, onsave, ondirty-change, onrender): the part after on is the event name. onChange (camel case) is React's own synthetic event and never fires for custom elements.
  • React 18 only sets string attributes on custom elements and attaches no listeners to them. Pass objects and functions through a ref and listen with addEventListener (the Editor's React recipe does).
  • Next.js: the elements need the browser (HTMLElement, customElements). Render the component on the client only, dynamic(() => import('./LabelDesigner'), { ssr: false }) (from a client component in the App Router).

Vue#

Load the module in onMounted (or put the script tag in index.html), and tell the compiler that the tags are custom elements:

// vite.config.ts
vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('zpl-') } } })
<script setup lang="ts">
import { onMounted, ref } from 'vue';
const ready = ref(false);
onMounted(async () => {
    await import(/* @vite-ignore */ 'https://labelviewer.example.internal/sdk/1.0.1/labelviewer-elements.min.js');
    ready.value = true;
});
</script>

<template>
    <zpl-viewer v-if="ready" :zpl="template" :data="{ name: 'Ada' }" zoom="fit" />
</template>

Bind object and function props in camelCase (:data, :sampleData, :saveHandler) so Vue sets them as properties.

TypeScript types without npm#

The declarations are served next to the module: index.d.ts, react.d.ts (React 19 JSX) and vue.d.ts (Vue templates). Download them into your project and map the package name with paths:

mkdir -p src/types/labelviewer && cd src/types/labelviewer
for f in index.d.ts react.d.ts vue.d.ts; do curl -fsSO https://labelviewer.example.internal/sdk/1.0.1/$f; done
{ "compilerOptions": { "paths": { "@labelviewer/elements": ["./src/types/labelviewer/index.d.ts"] } } }
import type { ZplEditorElement, ZplSaveDetail, ZplViewerElement } from '@labelviewer/elements';
import type {} from './types/labelviewer/react';   // JSX typings for <zpl-editor> / <zpl-viewer> (React 19); Vue: './types/labelviewer/vue'

import type imports disappear from the build, so nothing tries to bundle the package. index.d.ts also adds both tags to HTMLElementTagNameMap, so document.querySelector('zpl-viewer') is typed. Download the files again when you change the SDK version.

npm (optional)#

If you prefer a dependency in package.json, the install serves the package tarball too:

npm install https://labelviewer.example.internal/sdk/1.0.1/labelviewer-elements-1.0.1.tgz
import '@labelviewer/elements';   // defines <zpl-editor> and <zpl-viewer>

A bundled copy does not know where your install is. Point it there with fonts-base-url (or copy node_modules/@labelviewer/elements/dist/fonts/ to your static files) and api-base (the install whose license applies, and the Editor's API):

<zpl-viewer fonts-base-url="https://labelviewer.example.internal/sdk/1.0.1/fonts/" api-base="https://labelviewer.example.internal"></zpl-viewer>
<zpl-editor fonts-base-url="https://labelviewer.example.internal/sdk/1.0.1/fonts/" api-base="https://labelviewer.example.internal"></zpl-editor>

Fonts#

The renderer needs three TrueType files (RobotoCondensed-Bold.ttf, DejaVuSansMono.ttf, DejaVuSansMono-Bold.ttf, 844 KiB, about 490 KiB gzipped). They are fetched once per page and shared by every element.

  • Loaded from the install (script tag or import() of the /sdk/ URL): nothing to do; they load from fonts/ next to the script.
  • Bundled from npm: set fonts-base-url, see above.
  • Your own bytes: registerFonts({ 'RobotoCondensed-Bold.ttf': bytes, ... }) (ArrayBuffer or Uint8Array, every name in FONT_FILE_NAMES) before the first element connects. Nothing is fetched then.

The first element that connects decides the URL. loadFonts(url) starts the download early.

Content Security Policy#

The elements need no 'unsafe-inline' and no 'unsafe-eval' (the package build fails if the bundle contains eval or new Function). Styles are constructable stylesheets and CSSOM properties, which style-src does not restrict. For an app that loads the SDK from its install:

script-src 'self' https://labelviewer.example.internal;
connect-src 'self' https://labelviewer.example.internal;
img-src 'self' blob:;
  • script-src: the module itself.
  • connect-src: the font files are downloaded with fetch, so their origin goes here (not in font-src), and so does the API for the Editor's Test with API.
  • img-src blob:: <zpl-viewer> shows the label as a blob: image, and print() uses one too.
  • Trusted Types (require-trusted-types-for 'script'): allow the policy name labelviewer (trusted-types labelviewer). The elements use it only for their own static templates.

Browser support and size#

Current Chrome, Edge, Firefox and Safari: Chrome/Edge 111+, Firefox 113+, Safari 16.4+ (constructable stylesheets, container queries, color-mix(), ES2022). There are no polyfills. The tests run in Chrome.

FileRawgzip
labelviewer-elements.min.js1.43 MiB422 KiB
fonts (3 files)844 KiB~490 KiB

About two thirds of the script is the barcode library (bwip-js, used for Data Matrix, Aztec, MaxiCode, DataBar and other symbologies), then the font parser (~240 KiB), the renderer (~140 KiB) and the designer UI (~110 KiB). Versioned /sdk/ URLs are cached by browsers for a year. To keep the script off your first page load, load it with import() where it is used.

Security#

  • The elements render in the browser and store nothing by default. Only the Editor's autosave-key writes to localStorage on your origin.
  • Their only server calls are the font downloads, the license check (GET ${apiBase}/v1/info) and the Editor's Test with API (POST ${apiBase}/v1/merge), sent with credentials: 'omit' and no credentials of any kind.
  • The labelviewer API has no authentication, by design: keep the install on your internal network (Install › Access control).

Troubleshooting#

SymptomCause
Text on the label is missing, or boxes onlyThe fonts did not load: check the network tab for fonts/...ttf errors. Bundled from npm? Set fonts-base-url. CSP? Fonts need connect-src. An error event with source: 'fonts' says so too.
The module does not load (CORS or 404)Check the URL above: after a server upgrade old /sdk/<version>/ URLs answer 404. /sdk/ files always carry Access-Control-Allow-Origin: *; a proxy in between must not strip it.
integrity mismatchThe hash belongs to another version or build. Copy URL and hash together from this page.
"This label component is not licensed for this installation"The install's license does not cover that product, has expired or is not valid: see its Status page and License. The element also fires error with source: 'license'.
Console: "cannot check the license of this installation"The component could not reach ${apiBase}/v1/info (bundled without api-base, network, CSP connect-src). It keeps working.
The viewer shows nothing, and the console reports img-srcAdd blob: to img-src.
Angular: "zpl-editor is not a known element"Add CUSTOM_ELEMENTS_SCHEMA to the component's schemas.
Vue: "Failed to resolve component: zpl-viewer"Set compilerOptions.isCustomElement.
The bundler fails on the SDK URLAdd /* @vite-ignore */ (Vite) or /* webpackIgnore: true */ (webpack, Angular CLI) inside the import().
HTMLElement is not defined during SSRLoad the SDK only in the browser (see React).
Objects show up as [object Object] attributesReact 18, or the module was loaded after the framework rendered the element. Set them as properties (a ref), or wait for customElements.whenDefined(...).
CSP errorsSee Content Security Policy.

Files on this install#

Every file the install serves under /sdk/<version>/, with its size and SRI hash (the portal shows them for this install).

FileSizeSRI hash (sha384)
fonts/DejaVuSansMono-Bold.ttf324 KiBsha384-/HuWuQ8I5VcyDsTF+p5szVGtEA7khIUX8/tvN/J1Mifnzz/rkNU1zpG+GfnBwzyt
fonts/DejaVuSansMono.ttf333 KiBsha384-hH2n2s6XHqm1MrgAnRVYFnFUMMCbuKmyQRLhMOBYwnnouvzw0ezopjj83uhQNiM/
fonts/LICENSE-Adafruit-GFX.txt1 KiBsha384-qDKhzJTpVzuJ/vWlqMzwTm1dLD8DHCAw7xCe4/7w0ohbBDdzKDfP3blPx17AE87X
fonts/LICENSE-DejaVu.txt9 KiBsha384-grgle1y36XevlzjmAQ76+4tlxta1TkKWZ2GjVsyDXIcbdQIPjTxkl0eFDvB9Qe06
fonts/LICENSE-RobotoCondensed.txt11 KiBsha384-II9e1ieUDl5AxyiVq3/FflTua1Sr0kMJ25e6imG7rXg7SiAsA2VemsvEqVsLqM7/
fonts/RobotoCondensed-Bold.ttf166 KiBsha384-Q4NcziKMMWOqCt6K8cRF9LDPZd/70gWy33Be43cm5Io5IhToryVI6FiWbcJuvMpC
index.d.ts12 KiBsha384-67lwZ5tqQU0pPO0XvG24I+hMp1q8zPXwfMvGQS6cMIjP3difpZG5kUiI+Vl8qrwZ
labelviewer-elements-1.0.1.tgz1.47 MiBsha384-IbB4fadcJvErkhS35OW80V6K+5txsErPgvDA62WtVK1rUynVdNBSAiAc6iQ9B5Ea
labelviewer-elements.js1.64 MiBsha384-UiBvh+sXcRxua68FPRo4L0qmnW3QoOGISuBtFtqldU8koeeZIyyBenyqkujDz7gV
labelviewer-elements.min.js1.64 MiBsha384-UiBvh+sXcRxua68FPRo4L0qmnW3QoOGISuBtFtqldU8koeeZIyyBenyqkujDz7gV
react.d.ts3 KiBsha384-pk4E2NIyPkTgeTfuaRREk08/QwJaNkLK/OaGo22DdkQ2+MIF8a6K44yPaGF2eRBp
vue.d.ts2 KiBsha384-wkT/XoS3oTRkrKWjSakdMr1E5oIp24ZptuPsGfnTLipnaSfhbBz3ng39IS3PAJUL

Every file is also listed in manifest.json.