← Back to JavaScript Development
10.2 QR Code controller
app/assets/js/controllers/qrcode-controller.js generates a QR code into a
canvas with the qrcode npm package. The controller registers as qrcode and
dynamically imports the library when it connects.
The Gatherings record page uses it to share the public event URL.
API
Target
| Target | Required | Purpose |
|---|---|---|
canvas |
Yes | Container into which the controller creates a <canvas> |
Values
| Value | Type | Default | Purpose |
|---|---|---|---|
url |
String | none | Value encoded in the QR code; required before generation |
size |
Number | 256 |
Canvas width in pixels |
modalId |
String | none | Wait for that element’s shown.bs.modal event |
colorDark |
String | #000000 |
Dark module color |
colorLight |
String | #ffffff |
Background color |
errorCorrectionLevel |
String | H |
QR level L, M, Q, or H |
Methods
| Method | Behavior |
|---|---|
connect() |
Imports qrcode; generates immediately unless modalId is set |
generate() |
Creates the canvas once and resolves after toCanvas() completes |
regenerate() |
Clears the generated guard and creates the code again |
download() |
Generates if needed and downloads qrcode.png |
copyToClipboard() |
Generates if needed and writes a PNG ClipboardItem |
disconnect() |
Removes the Bootstrap modal listener and resets the guard |
generate() throws when the URL value or canvas target is missing. A QR library
error replaces the target with an error message and rejects the returned
promise.
Immediate generation
<div
data-controller="qrcode"
data-qrcode-url-value="<?= h($shareUrl) ?>"
data-qrcode-size-value="256">
<p id="share-qr-description">
<?= __('Scan this code to open the public event page.') ?>
</p>
<div
data-qrcode-target="canvas"
aria-describedby="share-qr-description"></div>
<button
type="button"
class="btn btn-outline-primary"
data-action="click->qrcode#download">
<i class="bi bi-download me-1" aria-hidden="true"></i>
<?= __('Download QR code') ?>
</button>
</div>
The generated canvas is visual content, so retain a visible description and the underlying URL elsewhere in the UI. A QR code must never be the only way to reach a destination.
Lazy generation in a Bootstrap modal
<div
class="modal fade"
id="eventQrModal"
tabindex="-1"
aria-labelledby="eventQrModalLabel"
aria-hidden="true"
data-controller="qrcode"
data-qrcode-url-value="<?= h($publicLandingUrl) ?>"
data-qrcode-modal-id-value="eventQrModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title fs-5" id="eventQrModalLabel">
<?= __('Share event') ?>
</h2>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="<?= h(__('Close')) ?>"></button>
</div>
<div class="modal-body">
<p><?= __('Scan this code to open the public event page.') ?></p>
<div data-qrcode-target="canvas"></div>
</div>
</div>
</div>
</div>
Set modalId to the actual element ID. The controller listens for Bootstrap’s
shown.bs.modal, not a generic click, and removes that listener when
disconnected.
Changing the encoded value
Stimulus updates urlValue when the data attribute changes, but the existing
canvas remains guarded. After changing the value, invoke regenerate():
element.dataset.qrcodeUrlValue = nextUrl
controller.regenerate()
Prefer providing the final CakePHP-generated URL on initial render. Do not encode secrets, internal numeric IDs that should not be public, or a tenant host derived from untrusted input. The linked endpoint must perform its own access checks.
Download and clipboard support
Downloading uses canvas.toDataURL('image/png'). Clipboard copy requires the
browser’s asynchronous Clipboard API, ClipboardItem, a permitted secure
context, and user activation. Provide a visible download or URL-copy fallback
when clipboard image support is not guaranteed.
The current controller logs copy/download errors. If the action is central to a
new flow, add accessible success/failure feedback with
KMP_accessibility.announce() and cover it in Jest.
Testing
Focused tests live in
app/tests/js/controllers/qrcode-controller.test.js and mock the dynamic
qrcode import. After changing the controller or its import, run:
cd app
npm run test:js -- qrcode-controller.test.js
npm run dev
Use a browser test when changing modal timing, download behavior, clipboard fallbacks, or focus/announcement behavior.