From 948fbc20143339651a2963921eb400b2395748bb Mon Sep 17 00:00:00 2001
From: Preshin P S
Date: Thu, 2 Apr 2026 20:57:10 +0530
Subject: [PATCH 1/2] Fix number leading zeros and required validation in
custom components
- Patch NumberComponent.createNumberMask to set allowLeadingZeroes: true,
preventing the mask from stripping leading zeros when a second digit is
typed (e.g. typing "07" no longer becomes "7").
- Fix createFormComponent updateValue: accept and forward flags, return the
actual changed boolean instead of always true, and only call updateOnChange
when the value has changed (matches formio base class behaviour).
- Add _onReactChange helper that passes { modified: true } when the React
component's onChange fires, so custom components correctly mark themselves
as non-pristine and show inline validation errors in real time.
- Add resolve.dedupe for react/react-dom in vite.config dev mode to prevent
duplicate React instance errors when running the example dev server.
- Add BugFixDemo to example/main.jsx to verify both fixes interactively.
Made-with: Cursor
---
example/main.jsx | 74 ++++++++++++++++++++++++++++++++
src/core/createFormComponent.jsx | 47 +++++++++++---------
src/index.js | 22 ++++++++++
vite.config.js | 5 +++
4 files changed, 128 insertions(+), 20 deletions(-)
diff --git a/example/main.jsx b/example/main.jsx
index 8d46226..005bb46 100644
--- a/example/main.jsx
+++ b/example/main.jsx
@@ -74,6 +74,43 @@ const contactForm = {
],
};
+// Form specifically for testing the two bug fixes:
+// 1. Number field — typing 0 as first digit should not be stripped
+// 2. Required validation — submit with empty fields must show errors
+const bugFixForm = {
+ display: 'form',
+ components: [
+ {
+ type: 'number',
+ key: 'quantity',
+ label: 'Quantity (try typing 0, then another digit — should keep the 0)',
+ input: true,
+ validate: { required: true },
+ placeholder: 'e.g. 07, 0.5, 042',
+ },
+ {
+ type: 'number',
+ key: 'price',
+ label: 'Price (required — leave empty and hit Submit to test validation)',
+ input: true,
+ validate: { required: true },
+ },
+ {
+ type: 'textfield',
+ key: 'name',
+ label: 'Name (required)',
+ input: true,
+ validate: { required: true },
+ },
+ {
+ type: 'button',
+ action: 'submit',
+ label: 'Submit',
+ theme: 'primary',
+ },
+ ],
+};
+
// ---------- Demo Components ----------
function FormRendererDemo() {
@@ -188,6 +225,41 @@ function FormBuilderDemo() {
);
}
+function BugFixDemo() {
+ const [submitted, setSubmitted] = useState(null);
+
+ return (
+
+
Bug Fix Verification
+
+ -
+ Number — leading zero: Type
07 or 0.5 in
+ the Quantity field. The 0 should NOT be stripped.
+
+ -
+ Required validation: Leave fields empty and click Submit.
+ Inline error messages should appear (not silently blocked).
+
+
+
+
+ {submitted && (
+
+
✅ Submitted Data:
+
+ {JSON.stringify(submitted, null, 2)}
+
+
+ )}
+
+ );
+}
+
// ---------- App ----------
function App() {
@@ -200,6 +272,8 @@ function App() {
provider work correctly.
+
+
diff --git a/src/core/createFormComponent.jsx b/src/core/createFormComponent.jsx
index e5f7f64..b876fec 100644
--- a/src/core/createFormComponent.jsx
+++ b/src/core/createFormComponent.jsx
@@ -58,18 +58,39 @@ export function createFormComponent({
this._root = null;
}
- updateValue = (value) => {
+ updateValue = (value, flags = {}) => {
const newValue = value === undefined || value === null ? this.getValue() : value;
const changed = newValue !== undefined ? this.hasChanged(newValue, this.dataValue) : false;
- this.dataValue = Array.isArray(newValue) ? [...newValue] : newValue;
- this.updateOnChange({}, changed);
- return true;
+ if (changed) {
+ this.dataValue = Array.isArray(newValue) ? [...newValue] : newValue;
+ this.updateOnChange(flags, changed);
+ }
+ return changed;
};
render() {
return super.render(``);
}
+ // Wraps updateValue with { modified: true } so that user-driven React
+ // onChange calls correctly mark the component as no longer pristine,
+ // enabling proper real-time validation error display.
+ _onReactChange = (value) => {
+ this.updateValue(value, { modified: true });
+ };
+
+ _renderReact(value) {
+ if (!this._root) return;
+ this._root.render(
+ render({
+ component: this.component,
+ value,
+ onChange: this._onReactChange,
+ data: this.data,
+ })
+ );
+ }
+
attach(element) {
super.attach(element);
this.loadRefs(element, { [`react-${this.id}`]: 'single' });
@@ -77,14 +98,7 @@ export function createFormComponent({
const reactEl = this.refs[`react-${this.id}`];
if (reactEl) {
this._root = createRoot(reactEl);
- this._root.render(
- render({
- component: this.component,
- value: this.dataValue,
- onChange: this.updateValue,
- data: this.data,
- })
- );
+ this._renderReact(this.dataValue);
if (this.shouldSetValue) {
this.setValue(this.dataForSetting);
@@ -105,14 +119,7 @@ export function createFormComponent({
setValue(value) {
if (this._root) {
- this._root.render(
- render({
- component: this.component,
- value: value,
- onChange: this.updateValue,
- data: this.data,
- })
- );
+ this._renderReact(value);
this.shouldSetValue = false;
} else {
this.shouldSetValue = true;
diff --git a/src/index.js b/src/index.js
index 4337c1f..3bade3f 100644
--- a/src/index.js
+++ b/src/index.js
@@ -11,6 +11,7 @@ export { registerComponent } from './core/registry';
// Re-export from @formio/js — accessed via Components to avoid restricted deep imports
import { Components, Formio as _Formio } from '@formio/js';
+import { createNumberMask } from '@formio/text-mask-addons';
// ---------------------------------------------------------------------------
// @formio/js v5 runtime patches
@@ -53,6 +54,27 @@ if (EditGrid?.prototype?.saveRow) {
};
}
+// Patch 3: Number component — allow leading zeros as first digit
+// createNumberMask defaults allowLeadingZeroes to false, which strips the leading
+// zero when a second digit is typed (e.g. typing "07" becomes "7"). This breaks
+// use cases where users need to enter values starting with 0 (e.g. "0.5", "007").
+const NumberComponent = Components.components?.number;
+if (NumberComponent?.prototype?.createNumberMask) {
+ NumberComponent.prototype.createNumberMask = function () {
+ return createNumberMask({
+ prefix: '',
+ suffix: '',
+ requireDecimal: this.component?.requireDecimal ?? false,
+ thousandsSeparatorSymbol: this.delimiter || '',
+ decimalSymbol: this.component?.decimalSymbol ?? this.decimalSeparator,
+ decimalLimit: this.component?.decimalLimit ?? this.decimalLimit,
+ allowNegative: this.component?.allowNegative ?? true,
+ allowDecimal: this.isDecimalAllowed(),
+ allowLeadingZeroes: true,
+ });
+ };
+}
+
// ---------------------------------------------------------------------------
// ReactComponent (Field class) for backward compat with custom components
diff --git a/vite.config.js b/vite.config.js
index 8515803..f428ebf 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -7,6 +7,11 @@ export default defineConfig(({ command }) => {
if (command === 'serve') {
return {
plugins: [react()],
+ // Force a single React instance regardless of where it's imported from
+ // (prevents "Invalid hook call" when @formio/js pulls in its own React)
+ resolve: {
+ dedupe: ['react', 'react-dom'],
+ },
// Handle JSX in .js and .jsx files
esbuild: {
loader: 'jsx',
From 7daa43882f6e37aab6ce19aff9174646e6c4d247 Mon Sep 17 00:00:00 2001
From: Preshin P S
Date: Thu, 2 Apr 2026 21:08:10 +0530
Subject: [PATCH 2/2] Add CI workflow to gate PRs against main on a passing
build
Runs npm ci + npm run build on every PR targeting main and on pushes
to main. Also verifies that the expected dist output files are present
after the build so a silent vite misconfiguration cannot slip through.
Made-with: Cursor
---
.github/workflows/ci.yml | 34 ++++++++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
create mode 100644 .github/workflows/ci.yml
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..595c7af
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,34 @@
+name: CI
+
+on:
+ pull_request:
+ branches: [main]
+ push:
+ branches: [main]
+
+jobs:
+ build:
+ name: Build check
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build library
+ run: npm run build
+
+ - name: Verify dist output
+ run: |
+ if [ ! -f dist/form-engine.es.js ] || [ ! -f dist/form-engine.umd.js ]; then
+ echo "❌ Expected dist files not found"
+ exit 1
+ fi
+ echo "✅ dist/form-engine.es.js and dist/form-engine.umd.js present"