Adding WASM component
You can find the ready-to-connect component in the @react-form-builder/components-fast-qr NPM package. You can find information on connecting the component in this section.
Overview
WebAssembly is a popular technology that is becoming more and more popular on the Internet. Let's see how you can connect a WASM (WebAssembly) component to FormEngine.
Currently, WASM components use JavaScript helper code to work with the DOM. So we will take a component that already has this code.
In this article we will connect a component that generates a QR code from the fast_qr library. The author of the library claims that it is a very fast generator. The library is written in Rust, compiled in WASM, and has TypeScript type definitions. A great set of technologies. Well, let's get started!
Bootstrapping application
Let's create a React application, we will use Vite, open the console and run the following commands:
npm create vite@latest my-react-app -- --template react-ts
cd my-react-app
npm install
npm run dev
OK, now stop the application and install the FormEngine dependencies (see this article for details):
npm install @react-form-builder/core @react-form-builder/designer @react-form-builder/components-rsuite
Let's remove the default styles from src/main.tsx
, your main.tsx
will look like this:
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App/>
</React.StrictMode>,
)
Replace the contents of the src/App.tsx
file with the following:
import {rSuiteComponents} from '@react-form-builder/components-rsuite'
import {BuilderView, FormBuilder} from '@react-form-builder/designer'
const components = rSuiteComponents.map(c => c.build())
const builderView = new BuilderView(components)
function App() {
return <FormBuilder view={builderView}/>
}
export default App
Basically the application is ready, you can run it using the npm run dev
command. In the browser you will see a ready window with the
designer:
Adding QR code component
First we need to install the fast_qr
library as a dependency:
npm install fast_qr
Second, we need to understand how the component should be used. GitHub has the following example in description:
/// Once `init` is called, `qr_svg` can be called any number of times
import init, {qr_svg, SvgOptions, Shape} from '/pkg/fast_qr.js'
const options = new SvgOptions()
.margin(4)
.shape(Shape.Square)
.image("") // Can be a URL or a base64 encoded image
.background_color("#b8a4e5")
.module_color("#ffffff");
// Using then / catch:
init()
.then(() => {
for (let i = 0; i < 10; i++) {
const svg = qr_svg("https://fast-qr.com", options);
console.log(svg);
}
})
.catch(console.error);
// Or using modern async await:
await init();
for (let i = 0; i < 10; i++) {
const svg = qr_svg("https://fast-qr.com", options);
console.log(svg);
}
It looks simple and straightforward. First you need to call the init
function, and then you can call the qr_svg
function, which will
return a line containing the SVG with the QR code. Let's make a React component that encapsulates this logic. Create a file src/QrCode.tsx
with the following content:
import init, {qr_svg, Shape, SvgOptions} from 'fast_qr'
import {RefObject, useEffect, useRef} from 'react'
import {define} from '@react-form-builder/core'
export interface QrCodeProps {
/**
* The QR code width.
*/
width?: number
/**
* The CSS class name.
*/
className?: string
}
export const QrCode = (props: QrCodeProps) => {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
renderQr(ref).catch(console.error)
}, [])
return <div className={props.className} style={{width: props.width}} ref={ref}></div>
}
async function renderQr(ref: RefObject<HTMLDivElement>) {
await init()
if (!ref.current) return
const options = new SvgOptions()
.margin(4)
.shape(Shape.Square)
.image("") // Can be a URL or a base64 encoded image
.background_color("#b8a4e5")
.module_color("#ffffff")
ref.current.innerHTML = qr_svg("https://fast-qr.com", options)
}
export const qrCode = define(QrCode, 'QrCode')
.name('QR code')
.category('static')
So, we have defined the component properties as the QrCodeProps
interface. We also defined the React functional component QrCode
that
displays div
. The imperative logic is defined in the renderQr
function - here the init
method is called, and if we already have a
reference to the rendered div
, then the created SVG with QR code is installed via the innerHTML
property. At the end, we defined a
constant qrCode
with the description of the component to connect it to the list of components.
It's time to add the component and run the application. Modify the src/App.tsx
file as shown in the example below:
import {rSuiteComponents} from '@react-form-builder/components-rsuite'
import {BuilderView, FormBuilder} from '@react-form-builder/designer'
import {qrCode} from './QrCode'
const components = rSuiteComponents.map(c => c.build())
const builderView = new BuilderView([...components, qrCode.build()])
function App() {
return <FormBuilder view={builderView}/>
}
export default App
Run the application with the npm run dev
command, open the browser, you should see that the QR code component is in the left pane:
Great, now drag and drop the QR code component into the center panel:
Wait, what's that blue line, and where's our generated QR code? Well, the Frontend is dark and full of terrors. Just kidding!
If you open the browser console, you will see an error there:
Two things are evident here:
- WASM module is being loaded - in the console the message starts with
WebAssembly.instantiateStreaming
. There was an error due to a MIME-type mismatch, the web server (Vite in this case) returnedtext/html
whileapplication/wasm
was expected. - There is a stack-trace of the error.
Judging from the stack trace, the problem occurred in QrCode.tsx:28
. And this is nothing more than calling the init
function, which
internally calls the __wbg_init
function, which in turn calls the __wbg_load
function. If you click on the links from the console, you
can see the code of these functions. At the moment of writing this article we are using the library version 0.12.5
, in your case the code
may be different.
Below is the code of these functions, the lines we are interested in are highlighted.
async function __wbg_init(input) {
if (wasm !== undefined) return wasm;
if (typeof input === 'undefined') {
input = new URL('fast_qr_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();
if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
input = fetch(input);
}
__wbg_init_memory(imports);
const {instance, module} = await __wbg_load(await input, imports);
return __wbg_finalize_init(instance, module);
}
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return {instance, module};
} else {
return instance;
}
}
}
If you're curious about the cryptic prefixes in the function name __wbg_
, it's short for
wasm-bindgen, a utility that generates helper code for WASM modules written in Rust.
Okay, how do we solve this problem? Let's load the WASM module using Vite.
Loading WASM module
Vite supports loading WebAssembly modules out of the box. We just need to add ?init
to the import statement as indicated in the example in the documentation. Also, the init
function in the fast_qr
library can take the
loaded WASM module as a parameter. The init
function can also take the path to a WASM module as an argument. You can see the init
function declaration, usually - in the IDE you can just do a "Go to definition" (Ctrl+Click / ⌘+
Click). Here is
the type description from the library (yes, yes, the init
function is __wbg_init
):
export default function __wbg_init(module_or_path?: InitInput | Promise<InitInput>): Promise<InitOutput>;
Loading the module path is also supported in Vite out of the box, all you need
to do is add ?url
to the import statement.
Okay, which module should I load? It's easy to understand if you open the "Network" tab in the browser, and we can see that only one
module fast_qr_bg.wasm
is loaded:
The fact that the module loads twice is normal, we are in development mode, and we have React StrictMode enabled.
In this article, we will load the path to the module, and pass this path to the init
function. Here is the full text of
the src/QrCode.tsx
file (changed lines are highlighted):
import init, {qr_svg, Shape, SvgOptions} from 'fast_qr'
import module from 'fast_qr/fast_qr_bg.wasm?url'
import {RefObject, useEffect, useRef} from 'react'
import {define} from '@react-form-builder/core'
export interface QrCodeProps {
/**
* The QR code width.
*/
width?: number
/**
* The CSS class name.
*/
className?: string
}
export const QrCode = (props: QrCodeProps) => {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
renderQr(ref).catch(console.error)
}, [])
return <div className={props.className} style={{width: props.width}} ref={ref}></div>
}
async function renderQr(ref: RefObject<HTMLDivElement>) {
await init(module)
if (!ref.current) return
const options = new SvgOptions()
.margin(4)
.shape(Shape.Square)
.image("") // Can be a URL or a base64 encoded image
.background_color("#b8a4e5")
.module_color("#ffffff")
ref.current.innerHTML = qr_svg("https://fast-qr.com", options)
}
export const qrCode = define(QrCode, 'QrCode')
.name('QR code')
.category('static')
Go back to your browser, add the QR code to the form, and you'll see something like this:
Great, all that's left is to add properties to the component and make them editable in the designer.
Adding properties
As you may have noticed, the library uses the SvgOptions
class to customize the parameters of the QR code. This class is a regular builder
that is configured through a chain of function calls:
const options = new SvgOptions()
.margin(4)
.shape(Shape.Square)
.image("") // Can be a URL or a base64 encoded image
.background_color("#b8a4e5")
.module_color("#ffffff")
You can find the type descriptions in the file fast_qr.d.ts
(in the fast_qr
package in the node_modules
folder), the file is quite
large, so it is under the cut:
fast_qr.d.ts
/* tslint:disable */
/* eslint-disable */
/**
* Generate a QR code from a string. All parameters are automatically set.
* @param {string} content
* @returns {Uint8Array}
*/
export function qr(content: string): Uint8Array;
/**
* Generate a QR code from a string. All parameters are automatically set.
* @param {string} content
* @param {SvgOptions} options
* @returns {string}
*/
export function qr_svg(content: string, options: SvgOptions): string;
/**
* Enum containing all possible `QRCode` versions
*/
export enum Version {
/**
* Version n°01
*/
V01 = 0,
/**
* Version n°02
*/
V02 = 1,
/**
* Version n°03
*/
V03 = 2,
/**
* Version n°04
*/
V04 = 3,
/**
* Version n°05
*/
V05 = 4,
/**
* Version n°06
*/
V06 = 5,
/**
* Version n°07
*/
V07 = 6,
/**
* Version n°08
*/
V08 = 7,
/**
* Version n°09
*/
V09 = 8,
/**
* Version n°10
*/
V10 = 9,
/**
* Version n°11
*/
V11 = 10,
/**
* Version n°12
*/
V12 = 11,
/**
* Version n°13
*/
V13 = 12,
/**
* Version n°14
*/
V14 = 13,
/**
* Version n°15
*/
V15 = 14,
/**
* Version n°16
*/
V16 = 15,
/**
* Version n°17
*/
V17 = 16,
/**
* Version n°18
*/
V18 = 17,
/**
* Version n°19
*/
V19 = 18,
/**
* Version n°20
*/
V20 = 19,
/**
* Version n°21
*/
V21 = 20,
/**
* Version n°22
*/
V22 = 21,
/**
* Version n°23
*/
V23 = 22,
/**
* Version n°24
*/
V24 = 23,
/**
* Version n°25
*/
V25 = 24,
/**
* Version n°26
*/
V26 = 25,
/**
* Version n°27
*/
V27 = 26,
/**
* Version n°28
*/
V28 = 27,
/**
* Version n°29
*/
V29 = 28,
/**
* Version n°30
*/
V30 = 29,
/**
* Version n°31
*/
V31 = 30,
/**
* Version n°32
*/
V32 = 31,
/**
* Version n°33
*/
V33 = 32,
/**
* Version n°34
*/
V34 = 33,
/**
* Version n°35
*/
V35 = 34,
/**
* Version n°36
*/
V36 = 35,
/**
* Version n°37
*/
V37 = 36,
/**
* Version n°38
*/
V38 = 37,
/**
* Version n°39
*/
V39 = 38,
/**
* Version n°40
*/
V40 = 39,
}
/**
* Error Correction Coding has 4 levels
*/
export enum ECL {
/**
* Low, 7%
*/
L = 0,
/**
* Medium, 15%
*/
M = 1,
/**
* Quartile, 25%
*/
Q = 2,
/**
* High, 30%
*/
H = 3,
}
/**
* Different possible Shapes to represent modules in a [`crate::QRCode`]
*/
export enum Shape {
/**
* Square Shape
*/
Square = 0,
/**
* Circle Shape
*/
Circle = 1,
/**
* RoundedSquare Shape
*/
RoundedSquare = 2,
/**
* Vertical Shape
*/
Vertical = 3,
/**
* Horizontal Shape
*/
Horizontal = 4,
/**
* Diamond Shape
*/
Diamond = 5,
}
/**
* Different possible image background shapes
*/
export enum ImageBackgroundShape {
/**
* Square shape
*/
Square = 0,
/**
* Circle shape
*/
Circle = 1,
/**
* Rounded square shape
*/
RoundedSquare = 2,
}
/**
* Configuration for the SVG output.
*/
export class SvgOptions {
free(): void;
/**
* Updates the shape of the QRCode modules.
* @param {Shape} shape
* @returns {SvgOptions}
*/
shape(shape: Shape): SvgOptions;
/**
* Updates the module color of the QRCode. Tales a string in the format `#RRGGBB[AA]`.
* @param {string} module_color
* @returns {SvgOptions}
*/
module_color(module_color: string): SvgOptions;
/**
* Updates the margin of the QRCode.
* @param {number} margin
* @returns {SvgOptions}
*/
margin(margin: number): SvgOptions;
/**
* Updates the background color of the QRCode. Tales a string in the format `#RRGGBB[AA]`.
* @param {string} background_color
* @returns {SvgOptions}
*/
background_color(background_color: string): SvgOptions;
/**
* Updates the image of the QRCode. Takes base64 or a url.
* @param {string} image
* @returns {SvgOptions}
*/
image(image: string): SvgOptions;
/**
* Updates the background color of the image. Takes a string in the format `#RRGGBB[AA]`.
* @param {string} image_background_color
* @returns {SvgOptions}
*/
image_background_color(image_background_color: string): SvgOptions;
/**
* Updates the shape of the image background. Takes an convert::ImageBackgroundShape.
* @param {ImageBackgroundShape} image_background_shape
* @returns {SvgOptions}
*/
image_background_shape(image_background_shape: ImageBackgroundShape): SvgOptions;
/**
* Updates the size of the image. Takes a size and a gap (unit being module size).
* @param {number} size
* @param {number} gap
* @returns {SvgOptions}
*/
image_size(size: number, gap: number): SvgOptions;
/**
* Updates the position of the image. Takes an array [x, y] (unit being module size).
* @param {Float64Array} image_position
* @returns {SvgOptions}
*/
image_position(image_position: Float64Array): SvgOptions;
/**
* Updates the error correction level of the QRCode (can increase the size of the QRCode)
* @param {ECL} ecl
* @returns {SvgOptions}
*/
ecl(ecl: ECL): SvgOptions;
/**
* Forces the version of the QRCode
* @param {Version} version
* @returns {SvgOptions}
*/
version(version: Version): SvgOptions;
/**
* Creates a new SvgOptions object.
*/
constructor();
}
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly qr: (a: number, b: number, c: number) => void;
readonly __wbg_svgoptions_free: (a: number) => void;
readonly svgoptions_shape: (a: number, b: number) => number;
readonly svgoptions_module_color: (a: number, b: number, c: number) => number;
readonly svgoptions_margin: (a: number, b: number) => number;
readonly svgoptions_background_color: (a: number, b: number, c: number) => number;
readonly svgoptions_image: (a: number, b: number, c: number) => number;
readonly svgoptions_image_background_color: (a: number, b: number, c: number) => number;
readonly svgoptions_image_background_shape: (a: number, b: number) => number;
readonly svgoptions_image_size: (a: number, b: number, c: number) => number;
readonly svgoptions_image_position: (a: number, b: number, c: number) => number;
readonly svgoptions_ecl: (a: number, b: number) => number;
readonly svgoptions_version: (a: number, b: number) => number;
readonly svgoptions_new: () => number;
readonly qr_svg: (a: number, b: number, c: number, d: number) => void;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {SyncInitInput} module
*
* @returns {InitOutput}
*/
export function initSync(module: SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {InitInput | Promise<InitInput>} module_or_path
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init(module_or_path?: InitInput | Promise<InitInput>): Promise<InitOutput>;
What's left to do:
- Add the necessary properties to the
QrCodeProps
interface for ourQrCode
component. - Pass the property values to the corresponding methods of the
SvgOptions
object. - Describe the added properties in the
qrCode
object usingprops
.
Below is the code of the component, it adds all of the above. In the code you will also find a color converter from the rgba format, which is passed by the designer property editor, to the hex format used by the component.
src/QrCode.tsx
import init, {qr_svg, SvgOptions} from 'fast_qr'
import module from 'fast_qr/fast_qr_bg.wasm?url'
import {RefObject, useEffect, useRef} from 'react'
import {color, define, number, oneOf, string} from '@react-form-builder/core'
export interface QrCodeProps {
/**
* The QR code width.
*/
width?: number
/**
* The CSS class name.
*/
className?: string
/**
* The QR code content.
*/
content: string
/**
* The different possible shapes to represent modules.
*/
shape?: number
/**
* The background color.
*/
backgroundColor?: string
/**
* The module color.
*/
moduleColor?: string
/**
* The margin.
*/
margin?: number
/**
* The image, base64 or URL.
*/
image?: string
/**
* The image background color.
*/
imageBackgroundColor?: string
/**
* The image background shape.
*/
imageBackgroundShape?: number
/**
* The image X position in module units.
*/
imagePositionX?: number
/**
* The image Y position in module units.
*/
imagePositionY?: number
/**
* Image size in module units.
*/
imageSize?: number
/**
* Image gap in module units.
*/
imageGap?: number
/**
* The error correction coding level.
*/
errorCorrectionLevel?: number
/**
* The QR code version.
*/
version?: number
}
export const QrCode = (props: QrCodeProps) => {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
renderQr(props, ref).catch(console.error)
}, [props])
return <div className={props.className} style={{width: props.width}} ref={ref}></div>
}
const colorPartToHex = (color: number) => {
if (color % 1 === 0) return (color | 1 << 8).toString(16).slice(1)
return Math.trunc((color * 255) | 1 << 8).toString(16).slice(1)
}
const rgbaColorToHex = (color?: string) => {
if (!color) return undefined
const index = color.indexOf(')')
if (color.startsWith('rgba(') && index >= 0) {
const colors = color.substring(5, index)
.split(',')
.map(s => s.trim())
.map(s => Number(s))
.map(colorPartToHex)
.join('')
return `#${colors}`
}
}
async function renderQr(props: QrCodeProps, ref: RefObject<HTMLDivElement>) {
await init(module)
if (!ref.current) return
let svgOptions = new SvgOptions()
if (props.margin) svgOptions = svgOptions.margin(props.margin)
const backgroundColor = rgbaColorToHex(props.backgroundColor)
if (backgroundColor) svgOptions = svgOptions.background_color(backgroundColor)
const moduleColor = rgbaColorToHex(props.moduleColor)
if (moduleColor) svgOptions = svgOptions.module_color(moduleColor)
if (props.shape) svgOptions = svgOptions.shape(props.shape)
if (props.image) svgOptions = svgOptions.image(props.image)
const imageBackgroundColor = rgbaColorToHex(props.imageBackgroundColor)
if (imageBackgroundColor) svgOptions = svgOptions.image_background_color(imageBackgroundColor)
if (props.imageBackgroundShape) svgOptions = svgOptions.image_background_shape(props.imageBackgroundShape)
if (props.imageSize !== undefined && props.imageGap !== undefined) {
svgOptions = svgOptions.image_size(props.imageSize, props.imageGap)
}
if (props.imagePositionX && props.imagePositionY) {
const position = new Float64Array([props.imagePositionX, props.imagePositionY])
svgOptions = svgOptions.image_position(position)
}
if (props.errorCorrectionLevel) svgOptions = svgOptions.ecl(props.errorCorrectionLevel)
if (props.version) svgOptions = svgOptions.version(props.version)
ref.current.innerHTML = qr_svg(props.content, svgOptions)
}
const nonNegNumber = number.withEditorProps({min: 0})
export const qrCode = define(QrCode, 'QrCode')
.name('QR code')
.category('static')
.props({
width: nonNegNumber.default(300).hinted('The QR code width'),
content: string.default('https://formengine.io').hinted('The QR code content'),
moduleColor: color.hinted('The module color'),
shape: oneOf(0, 1, 2, 3, 4, 5).default(0)
.labeled('Square', 'Circle', 'RoundedSquare', 'Vertical', 'Horizontal', 'Diamond')
.withEditorProps({creatable: false})
.hinted('The different possible shapes to represent modules'),
errorCorrectionLevel: oneOf(0, 1, 2, 3).default(0)
.labeled('Low, 7%', 'Medium, 15%', 'Quartile, 25%', 'High, 30%')
.withEditorProps({creatable: false})
.hinted('The error correction coding level'),
version: number.withEditorProps({min: 0, max: 39})
.named('QR version')
.hinted('The QR code version'),
image: string.hinted('The image, base64 or URL'),
imageBackgroundColor: color.hinted('The image background color'),
imageBackgroundShape: oneOf(0, 1, 2).default(0)
.labeled('Square', 'Circle', 'RoundedSquare')
.withEditorProps({creatable: false})
.hinted('The image background shape'),
})
Now we have a fully functional component that we can play with in the designer:
Conclusion
Connecting a WASM component to a React application is not a difficult task. We will add the QR code component to the FormEngine component collection so that you can easily connect it via the usual npm dependency.