← Back to JavaScript Development
QR Code Controller
Status: Active
Controller: qrcode-controller.js
NPM Package: qrcode ^1.5.4
Overview
The QR Code Stimulus controller generates QR codes dynamically using the qrcode npm package. Supports lazy generation in modals, download as PNG, and copy to clipboard.
Controller API
Location
File: app/assets/js/controllers/qrcode-controller.js
Static Targets
static targets = ["canvas"]
canvas: Container<div>where a<canvas>element is dynamically created for the QR code
Static Values
static values = {
url: String, // URL to encode (required)
size: { type: Number, default: 256 }, // QR code size in pixels
modalId: String, // Optional modal ID for lazy generation
colorDark: { type: String, default: '#000000' }, // QR code dark color
colorLight: { type: String, default: '#ffffff' }, // QR code light color
errorCorrectionLevel: { type: String, default: 'H' } // L, M, Q, or H
}
Note: The default
errorCorrectionLevelis H (High, ~30% damage tolerance), not M.
Methods
connect()
Sets up the controller. If modalIdValue is provided, listens for the Bootstrap shown.bs.modal event to defer generation. Otherwise generates immediately.
connect() {
this.generated = false;
if (this.hasModalIdValue) {
const modal = document.getElementById(this.modalIdValue);
if (modal) {
this._onModalShown = () => this.generate();
modal.addEventListener('shown.bs.modal', this._onModalShown);
}
} else {
this.generate();
}
}
disconnect()
Removes the modal event listener to prevent memory leaks.
generate() → Promise
Generates the QR code. Returns a Promise. Only generates once (guard via this.generated flag).
Error handling:
- Throws
ErrorifurlValueis missing orcanvasTargetis missing - On QR library error, replaces canvas content with
<p class="text-danger">Error generating QR code</p>
generate() {
if (this.generated) return Promise.resolve();
if (!this.hasUrlValue) throw new Error('QR Code: URL value is required');
if (!this.hasCanvasTarget) throw new Error('QR Code: Canvas target is required');
this.canvasTarget.innerHTML = '';
const canvas = document.createElement('canvas');
this.canvasTarget.appendChild(canvas);
return new Promise((resolve, reject) => {
QRCode.toCanvas(canvas, this.urlValue, {
width: this.sizeValue,
margin: 2,
color: { dark: this.colorDarkValue, light: this.colorLightValue },
errorCorrectionLevel: this.errorCorrectionLevelValue
}, (error) => {
if (error) {
this.canvasTarget.innerHTML = '<p class="text-danger">Error generating QR code</p>';
reject(error);
} else {
this.generated = true;
resolve();
}
});
});
}
regenerate()
Resets the generated flag and calls generate(). Use when the URL changes dynamically.
download() → async
Downloads the QR code as a PNG file. Awaits generate() first, then finds the <canvas> element inside the target div and uses canvas.toDataURL('image/png') to create a download link.
async download() {
try {
await this.generate();
const canvas = this.canvasTarget.querySelector('canvas');
if (!canvas) { console.error('Canvas not found'); return; }
const link = document.createElement('a');
link.download = 'qrcode.png';
link.href = canvas.toDataURL('image/png');
link.click();
} catch (error) {
console.error('QR Code generation failed:', error);
}
}
copyToClipboard() → async
Copies the QR code to clipboard as a PNG image using the Clipboard API. Awaits generate(), then converts the canvas to a blob.
async copyToClipboard() {
try {
await this.generate();
const canvas = this.canvasTarget.querySelector('canvas');
if (!canvas) { console.error('Canvas not found'); return; }
const blob = await new Promise((resolve, reject) => {
canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error('Failed to create blob')));
});
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
} catch (error) {
console.error('Failed to copy QR code:', error);
}
}
Registration
The controller registers itself via the standard window.Controllers pattern:
if (!window.Controllers) {
window.Controllers = {};
}
window.Controllers["qrcode"] = QrcodeController;
It is also explicitly imported in index.js:
import './controllers/qrcode-controller.js';
Usage Examples
Basic Usage
<div data-controller="qrcode"
data-qrcode-url-value="https://example.com/event/123">
<div data-qrcode-target="canvas"></div>
</div>
Important: The
canvastarget should be a<div>, not a<canvas>. The controller creates the<canvas>element dynamically inside it.
With Custom Size and Colors
<div data-controller="qrcode"
data-qrcode-url-value="https://example.com/event/123"
data-qrcode-size-value="300"
data-qrcode-color-dark-value="#1a5490"
data-qrcode-color-light-value="#f0f0f0">
<div data-qrcode-target="canvas"></div>
</div>
Lazy Loading in Modal
QR code generation is deferred until the Bootstrap modal is shown:
<button type="button" data-bs-toggle="modal" data-bs-target="#qrModal">
Show QR Code
</button>
<div class="modal fade" id="qrModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Event QR Code</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center"
data-controller="qrcode"
data-qrcode-url-value="https://example.com/event/123"
data-qrcode-modal-id-value="qrModal">
<div data-qrcode-target="canvas"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary"
data-action="click->qrcode#download">
<i class="bi bi-download"></i> Download
</button>
<button type="button" class="btn btn-secondary"
data-action="click->qrcode#copyToClipboard">
<i class="bi bi-clipboard"></i> Copy
</button>
</div>
</div>
</div>
</div>
Error Correction Levels
| Level | Tolerance | Description |
|---|---|---|
| L | ~7% | Low |
| M | ~15% | Medium |
| Q | ~25% | Quartile |
| H | ~30% | High (default) |
<div data-controller="qrcode"
data-qrcode-url-value="https://example.com"
data-qrcode-error-correction-level-value="L">
<div data-qrcode-target="canvas"></div>
</div>
CakePHP Integration Example
Used on gathering public landing pages:
<div class="modal-body text-center"
data-controller="qrcode"
data-qrcode-url-value="<?= $this->Url->build([
'controller' => 'Gatherings',
'action' => 'publicLanding',
$gathering->public_id
], ['fullBase' => true]) ?>"
data-qrcode-size-value="300"
data-qrcode-modal-id-value="qrCodeModal">
<div data-qrcode-target="canvas" class="mx-auto"></div>
</div>
Troubleshooting
| Problem | Check |
|---|---|
| QR code not generating | Console for thrown Error; verify data-qrcode-url-value is set |
| Modal QR not appearing | data-qrcode-modal-id-value must match the modal’s id attribute |
| Download not working | Ensure canvas has rendered; check browser console |
| Clipboard copy failing | Requires HTTPS in production; check Clipboard API support |
Quick Reference
<!-- Basic -->
<div data-controller="qrcode" data-qrcode-url-value="https://example.com">
<div data-qrcode-target="canvas"></div>
</div>
<!-- Actions -->
<button data-action="click->qrcode#download">Download</button>
<button data-action="click->qrcode#copyToClipboard">Copy</button>
<button data-action="click->qrcode#regenerate">Refresh</button>
<!-- In Modal (lazy) -->
<div data-controller="qrcode"
data-qrcode-url-value="..." data-qrcode-modal-id-value="myModal">
<div data-qrcode-target="canvas"></div>
</div>
Values:
| Value | Type | Default | Required |
|——-|——|———|———-|
| url | String | — | ✅ |
| size | Number | 256 | ❌ |
| modalId | String | — | ❌ |
| colorDark | String | #000000 | ❌ |
| colorLight | String | #ffffff | ❌ |
| errorCorrectionLevel | String | H | ❌ |