Form Integration
The Typograph Form Widget turns a form you designed in the portal into a fillable form on your own website, with a live preview of the document beside it. Someone fills it in, and you receive the personalised document back, ready to store or turn into a PDF.
It is one CDN script and one <div>. There is no npm package and no backend of ours involved
at runtime.
The one thing to understand first
The widget renders what you give it. It fetches nothing from Typograph on its own.
Three files have to come from your side:
| File | What it is | Where you get it |
|---|---|---|
| Form JSON | The fields, their types, and which template element each one writes to | Download it from the portal: the form's menu, or the Download button in the builder |
| Template JSON | The document being personalised | The template's presigned URL, fetched server-side |
| Manifest | The fonts, images and swatches the template depends on | Stored with the template, or assembled by you |
There is no endpoint that hands out a form to an unauthenticated caller, and no machine token can read one either. You download the definition from the portal and serve these three from your own origin, the way you would any other asset. That is a deliberate choice: your customers' form definitions are not public documents.
The consequence to plan for: a downloaded definition is a copy. Editing the form in the portal does not change what your server sends until you download it again.
The manifest is the one people forget. A template refers to its fonts and images by id, and resolving those ids is the manifest's job. Ship a template without its manifest and the preview renders with fallback fonts and missing images — the widget has no way to go looking for them.
Quick start
<script src="https://cdn.typograph.nl/form/latest/form.js"></script>
<div id="typograph-form"
data-typograph-form-url="/api/forms/welcome-card.json"
data-typograph-template-url="/assets/templates/welcome-card.json"
data-typograph-manifest-url="/assets/templates/welcome-card.manifest.json"
data-typograph-locale="nl"></div>
<script>
document.addEventListener('typograph_form_ready', () => {
const doc = window.typograph.get_typograph_document();
doc.set_save_template_callback(async ({ name, data, files }) => {
// `data` is the template JSON with the user's answers written into it.
// `files` are the images they picked, base64, not uploaded anywhere yet.
const template = JSON.parse(data);
for (const file of files) {
const blob = await (await fetch(file.data)).blob();
const url = await uploadToYourStorage(blob, file.name);
// patch `url` into `template` for file.elementId
}
await fetch('/api/personalised', {
method: 'POST',
body: JSON.stringify(template),
});
});
});
</script>
That is the whole integration. Everything below is detail.
What happens when the user submits
Nothing leaves the browser until you send it. While the form is being filled in, images the user picks are previewed client-side as base64 and the template is mutated in memory.
On submit, your callback fires once per template with three things:
name— the template's namedata— the mutated template JSON, as a stringfiles—{ elementId, name, data }for every image the user chose, base64
You upload the files, patch the resulting URLs into the template, and then store it or generate a PDF from it. The widget deliberately does not upload anything itself: it does not know your storage, and your customers' uploads should not pass through us.
What the widget checks first
Your callback does not fire until the answers pass. The form is marked novalidate on
purpose, because the browser's own validation popups cannot be styled and speak the
browser's language rather than the one you configured, so the widget checks the rules
itself and shows them in its own styles and locale.
It enforces what the form's author set per field: required, min_length and
max_length on text, min, max and step on numbers, and max_file_size on images.
An oversized image is refused as soon as it is chosen, not held back until submit.
If something fails, the offending fields are marked, the first is focused, and nothing is handed over. You do not need to re-check these rules before using the payload.
When a field does nothing
Every field drives one template element through its element_id. A field can end up
driving nothing at all, and it looks perfectly normal while doing so: it was never
linked, it names an element this template does not contain, or it is the wrong sort of
field for the element it names — a text field on a rectangle, say.
The widget reports this to the browser console as soon as the template has loaded, naming each field and what its element actually is. Open the console the first time you wire a form up; a form that "does nothing when I type" is almost always this.
Add data-typograph-debug="true" (or debug: true) to mark those fields in the form as
well. Leave it off in production: it explains a problem only the form's author can fix,
and your visitors cannot act on it.
Configuration
Every option exists as a data attribute and as a JavaScript option. Use attributes for the common case, the JavaScript API when you need more than one template or want the instance handle.
Data attributes
| Attribute | Description |
|---|---|
data-typograph-form-url | Form JSON URL (required) |
data-typograph-template-url | Template JSON to preview and personalise |
data-typograph-manifest-url | Manifest with fonts, images and swatches |
data-typograph-asset-base-url | Absolute base for relative asset paths |
data-typograph-images-url | JSON array of library images, for image fields set to library or both |
data-typograph-styles-url | Stylesheet injected after the widget's own |
data-typograph-show-preview | "false" hides the canvas preview |
data-typograph-locale | en or nl (default en) |
data-typograph-canvas-url | Canvas viewer bundle, for staging against a different build |
data-typograph-debug | "true" marks fields that are wired to nothing. Off by default, and meant for while you build |
data-typograph-step-nav | "host" stops the widget drawing Back/Next for a form with steps, leaving the page to do it |
JavaScript API
const widget = TypographForm.init({
container: '#my-form',
formUrl: '/api/forms/welcome-card.json',
templateUrls: ['/assets/templates/welcome-card.json'],
manifestUrl: '/assets/templates/welcome-card.manifest.json',
locale: 'nl',
onChange: (values) => console.log(values),
});
widget.set_save_template_callback(({ name, data, files }) => { /* … */ });
init returns a handle:
widget.getValues() // { uuid: value } as entered right now
widget.getMutatedTemplates() // the template JSON(s) with those values applied
widget.setLocale('en') // switch language, keeping what was typed
widget.reset() // clear the fields, back to the first step
widget.destroy() // remove it from the page
widget.getStep() // { index, total, id, name, isFirst, isLast }
widget.nextStep() // check this step and go on; false if it did not move
widget.previousStep() // one back, no checking
Forms with steps
A form built with steps in the portal is walked through one step at a time. The widget draws a step bar, shows one group of fields, and puts the preview on the spread that step is about. Nothing about the embed changes: the steps come out of the form JSON.
Going on checks the fields of the step you are leaving, unless the step says otherwise. Submitting checks the whole form — a step may be marked as not blocking, so the last word is still the submit — and if the first problem sits on an earlier step, the widget goes back to it rather than reporting an error you cannot see.
A form of one step, or a form with no steps at all, behaves exactly as it always has: no step bar, no Next, one submit button.
When the page owns the button
If your page hides the widget's submit button and puts its own underneath — the usual arrangement for a webshop — the widget's Next would be a second button doing the same thing. Turn its navigation off and drive it yourself:
const widget = TypographForm.init({
container: '#my-form',
formUrl: '/api/forms/welcome-card.json',
stepNav: 'host',
onStepChange: (step) => {
button.textContent = step.isLast ? 'Add to basket' : 'Next';
backButton.hidden = step.isFirst;
},
});
button.addEventListener('click', () => {
if (widget.getStep().isLast) {
form.requestSubmit(); // the save callback fires
} else {
widget.nextStep();
}
});
onStepChange also fires once when the form first renders, so the button has the right
label before anything is touched.
A page that just calls requestSubmit() and knows nothing about steps keeps working:
on a step that is not the last, submitting means going on. Only the button's label will
be wrong, which is what onStepChange is for.
Giving image fields a library to pick from
An image field can let the visitor upload their own picture, choose from a set you supply, or both. Which of the three is set per field in the form builder; your side of it is supplying the set.
Hand the widget a list of images and every field set to library or both gets a picker. Without a list those fields quietly fall back to upload, so a missing library never breaks a form.
TypographForm.init({
container: '#typograph-form',
formUrl: '/forms/welcome-card.json',
templateUrls: ['/templates/welcome-card.json'],
images: [
{ name: 'Mountains', thumbnail_url: '/img/mountains-thumb.jpg', web_url: '/img/mountains.jpg' },
{ name: 'Coast', thumbnail_url: '/img/coast-thumb.jpg', web_url: '/img/coast.jpg' },
],
});
For a longer or a changing catalogue, serve the same array as JSON and point at it
instead — with imagesUrl, or data-typograph-images-url on the container:
[
{ "name": "Mountains", "thumbnail_url": "/img/mountains-thumb.jpg", "web_url": "/img/mountains.jpg" },
{ "name": "Coast", "thumbnail_url": "/img/coast-thumb.jpg", "web_url": "/img/coast.jpg" }
]
| Field | Required | What it is |
|---|---|---|
name | Yes | Shown under the thumbnail, and what the visitor picks by |
thumbnail_url | Yes | The picker's preview. Keep it small; this is the one loaded for every image at once |
web_url | Recommended | The image placed in the document. Falls back to the thumbnail when absent, which will look soft in print |
print_url | No | A higher-resolution rendition for the printed result |
editor_url | No | The rendition the canvas preview loads |
id | No | Your own reference, passed through untouched |
Two things worth knowing before you build the list.
A library pick is not an upload. The chosen image's URLs are written straight into
the document you receive, and it does not appear in payload.files — there is
nothing for you to store, because the file was already yours. Uploads still arrive as
files, exactly as before.
The URLs must resolve from the page the widget runs on. They are used as given, so a relative path is resolved against your page, not against ours.
Styling
The form renders inside a Shadow DOM, so its styles never touch your page and your CSS never touches it. Two supported ways in:
Custom properties, which inherit through the shadow boundary. Set them on the widget element or any ancestor:
<div data-typograph-form-url="…"
style="--tw-primary:#0aa; --tw-radius:2px; --tw-border:#ddd;"></div>
--tw-primary, --tw-primary-hover, --tw-primary-subtle, --tw-radius, --tw-border,
--tw-bg, --tw-label, --tw-hint, --tw-error, --tw-focus-ring,
--tw-input-padding-y, --tw-input-padding-x.
A stylesheet, for structural changes: point data-typograph-styles-url at a CSS file and
it is injected into the shadow root after the base styles, so your rules win.
Before you go live
- The form is published. A draft form is a portal-only object; the widget will not render one.
- The manifest is served alongside the template, and its font and image URLs resolve from the page the widget runs on.
jsColorEngineWeb.jssits next toform.jsif you host the bundle yourself. Serving from our CDN handles this for you.- Your save callback is registered before the user can submit — inside the
typograph_form_readylistener, not after an async fetch that may still be pending. - You have tested with an image field. That is the path where the base64 handover, your upload, and the URL patching all have to line up.
- The browser console is clean when the form loads. A warning there means a field is wired to nothing and will silently do nothing when filled in.
- If you supply an image library, its thumbnails load from the page the widget runs on. A broken path shows an empty picker rather than an error.
Field types
Text, textarea, number, date, colour, dropdown, radio, checkbox, toggle, and image. Each field names the template element it writes to and what it does to it: replace the text, load an image, or set a fill colour.
Some template elements cannot be personalised, and the portal refuses to link them: elements locked in the editor, text inside a clip, and text frames that overflow into another frame. Those restrictions are enforced when the form is built, so a published form never contains a field that cannot work.