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

+ +
+
setSubmitted(sub.data)} + /> +
+ {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"