Quick start
Step by step, from nothing to a working builder that saves and loads. No prior knowledge of the package assumed.
Words we use
Four words appear constantly in these pages. If they already mean something to you, skip ahead — but they are worth two minutes, because everything else builds on them.
| Word | What it means here |
|---|---|
| Document | A plain JavaScript object describing one printable file — its pages, its blocks, its colours. It is data, not a PDF. You can print it with console.log, store it in a database column, and email it to yourself. Everything the builder does is "change this object". |
| Block | One piece of content on the page — a title, an address, a table of line items, a signature. Your users drag them from a list on the left onto the page in the middle. |
| Prop | A setting you pass to the component, written like an HTML attribute: <PdfBuilder readOnly />. Every feature in this manual is switched on by passing a prop. |
| Callback | A function you hand to the package so it can call you when something happens. onSave is a callback: the package does not know how to save, so it calls your function and lets you do it. |
Before you start
You need two things.
-
Node.js installed, so you can run
npm. Check by opening a terminal and typingnode -v. If you see a version number, you are fine. -
A project with a JavaScript build step. A React app, a Symfony app with
Webpack Encore, a Laravel app with Vite — any of these. If you have none of these yet,
the fastest starting point is
npm create vite@latest my-app -- --template react.
Step 1 · Install it
In your project folder, run:
Why three packages and not one?
react and react-dom are the engine the builder runs on. The package
deliberately does not bring its own copy, because if two copies of React end up on one page,
things break in confusing ways. So it uses yours.
You should now see
Step 2 · Put it on the screen
Create a file. This is the entire thing — copy it exactly.
Three lines matter, and it is worth knowing what each does:
import { PdfBuilder } from … |
Brings the component into your file. |
import '…/styles.css' |
Brings in the appearance. Forget this and you get a page of plain unstyled text that looks broken. This is the most common first mistake. |
<PdfBuilder /> |
The whole editor. No settings required. |
Now render <Editor /> wherever your app shows pages.
Step 3 · Give it a height
At this point most people see almost nothing — a thin sliver, or an empty strip. That is expected, and it is not broken.
<div> takes up no space.
So give it a box with a real height:
80vh means "80% of the height of the browser window". Anything real works —
700px, calc(100vh - 64px) if you have a fixed navbar above it.
Checkpoint · what you should see
Left — Blocks. The catalogue. Drag one onto the page.
Middle — Canvas. The page itself, at real proportions. This is what prints.
Right — Properties. Click a block and its settings appear here.
Try it now: drag "Title" onto the page, then click it and change the words.
Step 4 · Catch what the user made
The builder is working, but if the user refreshes the page their work is gone. Nothing has been stored anywhere yet.
To get hold of the work, add onChange. The builder calls it every time anything
changes, and hands you the document.
Open your browser console, type something into a block, and watch. You will see a plain object appear:
onChange fires on every keystroke.
That is useful for a "you have unsaved changes" flag. It is not where you send a
request to your server — you would send one per letter typed. Use Step 5 for that.
Step 5 · Save it
Add onSave and a Save button appears in the toolbar. When the user clicks it, the
builder calls your function with the document. What happens next is entirely up to you.
On your server, store that JSON text in a column. That is the whole storage design.
async and await, as above.
That is what lets the builder know the save is still in progress, so the button can show a
spinner and stop the user clicking twice. Leave them out and the button reports success
instantly — including when the request later fails.
Step 6 · Load it back
Hand the stored object back through defaultDocument:
That is it. You now have a full save-and-load cycle.
version. That number
is how a document saved today still opens after a future upgrade — the package reads it and
quietly brings the document up to date.
Step 7 · Language and currency
By default a new document is US English with dollars. To start somewhere else, use
createDocument:
locale | How things are written — date order, decimal separators, digit grouping. 'en-IN' gives 1,23,456.00; 'de-DE' gives 123.456,00. |
currency | Which money it is. A three-letter code — 'INR', 'USD', 'EUR'. Never a symbol. |
en-IN is correct. en_IN is what PHP, Laravel and Rails hand you, and
the browser's formatting tools reject it outright. We convert it for you here — but nothing
else in your app will.
If you get stuck
Almost every first-time problem is one of these five.
| What you see | What it means |
|---|---|
| Plain text, no colours or panels | The stylesheet was not imported. Check line 2 of Step 2. If you use Symfony Encore, also check that your layout links the JavaScript entry's CSS file, not only css/app. |
| Nothing at all, or a thin strip | No height. Step 3. |
Invalid hook call in the console |
Two copies of React on the page. Run npm ls react — if it lists React twice, that is the cause. |
| An empty box, and no error anywhere | The component never mounted. In Symfony UX React this means the components were never registered; elsewhere it usually means the target element did not exist yet when your script ran. |
| It works, then breaks after a refresh | You are not loading the saved document back. Step 6. |
Longer list, including framework-specific traps, in Installation and API reference.
When you are ready for more
You have a working, saving editor. Everything below is optional, for when you need it.
Controlling it from your own buttons
Suppose Save lives in your toolbar, not the builder's. You need a way to say "give me the document now". That is what a ref is — a handle onto the component.
getDocument() | The document right now. |
loadDocument(doc) | Replace it. Counts as one undo step. |
undo() / redo() | The same as the toolbar buttons. |
builder.current is null until the component has appeared on screen.
That is why loading happens inside onReady — it fires at the exact moment the
handle becomes usable.
Two ways to hold the document
| Prop | Who owns the document | Use it when |
|---|---|---|
defaultDocument |
The builder | Almost always. Simplest and fastest, and undo works with no effort from you. |
document |
You | Only when something outside the builder must change the document while the user is editing. |
defaultDocument.
Where to go next
| Core concepts | How the document is put together, and where a style comes from. Read this before customising anything. |
| Images & data | Uploading logos, and putting real customer details into a reusable template. |
| Customising | Hide fields, hide interface, make it look like your product. |
| Custom blocks | Build a block of your own. |