JS API
Drive the widget from your product page, and read back what the customer did.
Your product page already knows what the customer has selected — the JS API is how you tell the widget. Everything else is optional: a page that only ever sets one product can do it in markup and never touch JavaScript at all.
Reaching the element
With async on the script tag the element upgrades whenever the script lands. Attributes
are safe to set before that; methods are not, so wait for the definition:
await customElements.whenDefined('viewa-visualizer')
const widget = document.querySelector('viewa-visualizer')Pushing a selection
When the customer changes product or material in your customizer, push it in:
widget.setSelection({ product: 'YOUR-SKU-123' })product takes either a minted prod_… id or one of your registered
SKU aliases. The API resolves both through this
one field, so there is no separate sku to set.
Materials are per slot, keyed by slot token:
widget.setSelection({
product: 'YOUR-SKU-123',
materials: [{ optionId: 'slot_fabric', material: 'CHARCOAL-LINEN' }],
})setSelection merges. Only the keys you pass are written, so a finish picker can
send materials alone without knowing the product ref. Passing a key explicitly as
undefined clears it.
A selection triggers a render straight away. Pass { render: false } to batch several
coupled changes, or set auto-render="off" to own the trigger entirely and call
visualize() yourself.
Reading state
getStatus() returns everything the widget knows as one snapshot. Reach for it first —
the events carry transitions, and a listener attached late has missed them.
{
phase: 'done', // idle | uploading | preparing | editing |
// measuring | ready | rendering | done | error
step: 'render', // upload | areas | render — the coarse step
busy: false, // waiting on the API — for your spinner
capabilities: ['upload', 'areas', 'render'],
ui: ['chrome', 'controls', 'zoom', 'measurements', 'compare', 'download', 'change-photo'],
productName: 'Vibe Roller Blind',
autoRender: true,
error: null, // { message, code } — customer-safe copy only
image: { id: 'img_…', status: 'ready', url: '…', width: 1024, height: 768 },
areaSet: { id: 'aset_…', status: 'ready' },
areas: [{ id: 'area_…', name: null, measurements: { width, height, unit } }],
activeArea: null, // which area is being edited, in the editor only
measurements: true, // whether the sizes are drawn on the photo
render: { id: 'ren_…', status: 'succeeded', url: '…' },
selection: { product: 'prod_…', materials: [...], animation: [1] },
can: { upload, addArea, removeArea, saveAreas, editAreas, visualize },
}The ids are the part worth keeping. Persist image and areaSet against a cart line and
a later page can pick the customer up where they left off — see
Embedding.
can is the widget's own answer to "would this do anything right now?". Sync your chrome
off it rather than re-deriving the rule from the phase and getting an edge wrong.
Everything it hands back is a copy, all the way down. Mutating the object you get from
getStatus() or getSelection() changes nothing inside the widget.
Methods
| Method | What it does |
|---|---|
getStatus() | Everything the widget knows, as one snapshot |
setSelection(selection, { render }) | Push the product and material choice. Merges |
getSelection() | What it currently holds |
setAnimation(values) | The open/closed position, 0..1 per animation |
visualize() | Render now. What auto-render="off" hands the trigger to |
canVisualize | Whether there is enough to render — areas, product, capability |
getAreas() | The measured areas, once they are ready |
chooseFile() | Open the file picker from your own button |
addArea() / removeArea() | Drive the corner editor from your own chrome |
canAddArea / canRemoveArea | Whether those two will do anything |
selectArea(index) | Make an area the one being edited |
renameArea(index, name) | Name an area. Empty clears it. Stored on the area set |
saveAreas() / editAreas() | Commit and measure; reopen the editor |
showMeasurements(on) | Draw the sizes on the photo, or not |
getPhase() | Where the customer is, paired with the phase event |
getCapabilities() | What this instance is allowed to do |
reset() | Back to the upload panel, abandoning anything in flight |
Methods that could be a no-op — chooseFile, addArea, removeArea, selectArea,
renameArea, saveAreas, editAreas, visualize, showMeasurements — return a boolean
saying whether they did anything. The setters and reset() return nothing.
selectArea matters more than it looks. Only the active area's handles can be grabbed, so
with two overlapping outlines on one photo, picking by index is the only reliable way to
say which one you mean.
With ui="none" you own every control, including the one that commits the areas.
Nothing calls saveAreas() for you, so without it the customer cannot finish.
Events
CustomEvents on the element, prefixed viewa:, bubbling and composed.
| Event | detail |
|---|---|
ready | {} — the element is upgraded and listening |
phase | { phase, step, capabilities } |
image-uploaded | { id } — accepted, not yet processed |
image-ready | { id } — processed and safe to render against |
areas-changed | { count, active, canAdd, canRemove } — fires on every drag |
areas-ready | { id, areas } — the API's measurement of them |
selection-changed | { selection } |
render-started | { areaSet } — the render's own id arrives with render-ready |
render-ready | { id, url } |
error | { message, code } |
widget.addEventListener('viewa:render-ready', (event) => {
cartLine.renderId = event.detail.id
})areas-changed and areas-ready are not the same thing. The first fires while the
customer is still dragging, before anything is sent; the second is the measurement that
came back. Bind enabled states to phase, which is emitted after the widget has redrawn,
so a listener reading can sees the new state rather than the old one.
error is the single failure channel. Job failures — image preprocessing, area
occlusion, render capture — arrive from the API on a 200 with an error.code, and go
out through this same event rather than a parallel set of *-failed ones. message is
always customer-safe copy; code is the stable thing to branch on. See
Errors.