← 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.