Repository navigation
Conversation
- React 19.3, MUI 9.4, MUI X 9.14, react-hook-form 7.89, TypeScript 6.0, Vite 8.3 (library mode via a shared vite.library.ts), Vitest 5 + jsdom 30, ESLint 10 flat config (typescript-eslint strict, @eslint-react, react-hooks 7, jsx-a11y-x), dprint (Stackworx TS profile from chore/dprint), Storybook 10, Lerna 10. - Drop Prettier, eslint-plugin-jest/react/prettier, babel-loader, @types/eslint__js, date-fns 2 and dayjs. - Packages: ESM exports map, files: [dist], sideEffects: false, engines, peer ranges for React 19 / MUI 9 / RHF >=7.62; MIT LICENSE. - Shared test harness (test/renderWithForm.tsx) and a smoke test. - 0.0.x components move to src/legacy (excluded from the gate) until each is rewritten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
useFieldController wraps useController and resolves the error text: a rule's own message wins, a known type (required, min, max, minLength, maxLength, pattern, validate) uses FormErrorMessagesProvider (English defaults), anything else shows the type name. splitControllerProps forwards name/control/rules/defaultValue/shouldUnregister/disabled to RHF so they no longer spill onto MUI. FieldControllerProps accepts a control whose transformed values differ from its input (resolvers). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bound through useFieldController: value falls back to '' (no uncontrolled warnings), consumer onChange/onBlur compose with the RHF binding, inputRef merges with field.ref so setFocus works, disabled reaches RHF, and helperText shows the error text or the consumer's helper. An optional transform maps the form value to text and back. Shipped source uses .js relative specifiers (lint-enforced) so the emitted declarations resolve under node16/nodenext too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Built from MUI's documented NumberField recipe (Base UI NumberField in FormControl/InputLabel/OutlinedInput/FormHelperText). onValueChange stores number | null, field.ref reaches the visible input so setFocus works, min/max/step/format pass through, and the error text drives the helper text, aria-invalid and aria-describedby. The arrow icons are inlined so the package needs no @mui/icons-material peer. It ships as the @stackworx/react-hook-form-mui/number-field subpath so @base-ui/react stays an optional peer; library entries now come from each package's exports map. setFocus tests wait for RHF's deferred focus instead of passing on focus left over from typing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Options drive the menu items; the form value is the chosen option's value (TValue | null, or TValue[] with multiple), mapped back through the options so numbers stay numbers even for a comma-joined autofill string. Error text replaces the helper text, the ref reaches the select for setFocus, and consumer slotProps.select (object or function form) keeps working with multiple forced on top. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both take a label and render FormControl > FormControlLabel > control plus a FormHelperText with the error text or the consumer's helper (linked through aria-describedby, with aria-invalid on error). The ref travels through slotProps.input (MUI 9 has no inputRef) and merges with a consumer's slotProps.input ref, object or function form. The name must point at a boolean field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All three take options ({value, label, disabled}), label, helperText,
row and required, use one controller per group and name/describe the
group for assistive tech (fieldset legend or aria-labelledby, helper
or error text via aria-describedby).
- CheckboxGroup stores TValue[], adding/removing by Object.is; an
undefined value starts empty instead of crashing.
- RadioGroup stores TValue | null, mapping MUI's string value back to
the typed option by index (numbers and booleans survive).
- ToggleButtonGroup stores TValue | null (exclusive, default) or
TValue[]; enforceValue ignores a click that would clear it.
setFocus lands on the selected option, else the first enabled one.
Select now shares the FieldOption type.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without getOptionValue the form stores the selected option, as MUI does; with it, what it returns, such as the id. Stored options are matched by getOptionKey, not by reference, and the form field's type decides whether getOptionValue is needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alues It stores the option or getOptionValue's result, like Autocomplete. Selected options keep their labels across pages and searches; only stored ids need knownOptions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DatePicker, TimePicker, DateTimePicker, DateField, TimeField and DateTimeField bind through one usePickerController: value (with an optional transform to and from the form value), onChange, onError, blur, ref (the hidden input, which forwards focus to the sections), name and disabled. Errors go to the text field (slotProps.textField for pickers, object or function form, merged with the consumer's). MUI's validation error becomes a validate rule that reads a ref set from onChange's validationError before RHF validates, so the message appears on the change that causes it. A function-form rules.validate still runs. Default English messages cover every MUI X validation code and never interpolate dates; messages overrides them per field. No adapter or /internals imports. Tests run on AdapterLuxon (dev only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DateRangePicker, DateTimeRangePicker and SingleInputDateRangeField
reuse the pickers package's controller with a [start, end] value that
defaults to [null, null]; transform maps the tuple to the app's shape
(e.g. {from, to}). MUI's components are imported under aliases, so a
wrapper can no longer render itself, and the value is passed through.
Range validation errors (including invalidRange) resolve to messages.
The Pro package depends on the pickers wrapper package, which now
exports its binding helpers. Tests filter MUI X's missing-licence
console error without setting a key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0.1.0 rather than 1.0.0 while the API meets its first real forms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One CI workflow (checkout@v5, setup-node@v5, npm ci, npm run check) on a Node 24/26 matrix, with a Storybook build on Node 24 so the Pages deploy cannot break silently. It replaces the lint and per-package build workflows, which ran Node 18/20 with the old toolchain. The Pages deploy (docs plus Storybook under /storybook on gh-pages) is kept on the same actions and Node 24; the Docusaurus build was checked locally on a current Node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An empty helper line keeps its space, as helperText=' ' did, so an error appearing doesn't move the form. reserveHelperText turns it off for a field and HelperTextProvider for a form or section. The picker packages take the core package as a peer so the provider reaches them too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One form with every component: a searched destination stored as the option, an airport stored as its code, linked dates, a field that appears when switched on, and values in the ISO formats an API takes. Its city search and the location stories share one in-memory source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every story renders from args: shared Form controls (mode, HelperTextProvider, disabled), Field controls (label, helper text, disabled, reserved line, required message) and the component's own. Control types and Docs tables come from react-docgen-typescript, limited to our props and a short list of MUI's. Each picker gets required, min and max, custom messages, disabled and ISO-string stories; the ranges add both-or-neither and end-before-start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release:version bumps the changed packages with Lerna without committing, so the bump reaches main in a PR. Publishing a GitHub release, or running the workflow by hand, runs the checks, publishes each version npm doesn't have yet in dependency order through npm trusted publishing, and tags it <package>@<version>. The workflow follows stackworx/relay-network's release.yml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
✅ Deploy Preview for aquamarine-choux-75c766 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Strings and numbers need no getOptionKey or getOptionLabel, and an object's label defaults to its label, as in MUI. Object options still need getOptionKey. The prop and selection types now come from MUI's own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Context providers use Provider and useContext, and the listbox forwards its ref, so the packages run on React 18 as well as 19. Lint targets React 18, and CI runs the packages' typecheck and tests on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every component takes handleChange, which runs after the form stores a change, and handleBlur, which runs after the form's blur handler. With suppressFormChange the form stores nothing and handleChange decides. MUI's onChange and onBlur are no longer accepted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One MuiHandlerProps helper replaces the per-component handler-argument aliases, and changeHandler infers the arguments from handleChange. TextField and the pickers share FieldTransform, the pickers use core's FieldControllerProps and ReserveHelperTextProps, and the unused AutocompleteFieldValue is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before and after code for the changes most forms need, linked from the changelog, the README and each package's README, which is what npm shows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cliedeman
approved these changes
Sep 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Rebuilds the bindings for MUI 9 and MUI X 9 on React 18 or 19, and releases all three packages as 0.1.0. It's 0.1.0 rather than 1.0.0 while the API meets its first real forms.
What changed
distonly.react ^18.0.0 || ^19.0.0,react-hook-form >=7.62.0 <8and@mui/material ^9.0.0, plus MUI X 9 for the pickers.useController.disabledreaches React Hook Form.FormErrorMessagesProvider.handleChangeandhandleBlur, on every component. MUI's ownonChangeandonBlurare no longer accepted.handleChangeruns after the form stores a change, and takes the MUI component'sonChangearguments. The option groups pass the typed option value, andNumberFieldpasses Base UI's(value, eventDetails).handleBlurruns after the form's blur handler marks the field touched.suppressFormChange, the form stores nothing.handleChangestores the changes it keeps withsetValue, so it can refuse one.Select,RadioGroup,CheckboxGroupandToggleButtonGroup, with typed values.getOptionValuestores something else instead, such as the id.getOptionKeynorgetOptionLabel, and an object's label defaults to itslabel.getOptionKey, which can return a string or a number. Stored options are matched by it, not by reference.getOptionValueis needed, and the option type whethergetOptionKeyandgetOptionLabelare.NumberField(Base UI).AsyncAutocomplete: server-backed options, a label cache and a load-more footer.DateField,TimeFieldandDateTimeField.DateTimeRangePickerandSingleInputDateRangeField.messagessets the error text per field, and every picker takes atransform.DateRangePickerrecursion is fixed.helperText=' 'did, so an error appearing doesn't move the form.reserveHelperTextturns this off for a field, andHelperTextProviderfor a form or section.npm run release:version(Lerna) bumps the versions in a PR. Publishing a GitHub release runsrelease.yaml:<package>@<version>.relay-network.See
CHANGELOG.mdfor every breaking change since 0.0.x, andMIGRATION.mdfor how to update, with beforeand after code.
Before the first release
@cliedeman: trusted publishing needs a trusted publisher on each package on npmjs.com, set to organisation
stackworx, repositoryreact-hook-form-muiand workflowrelease.yaml. The packages are:@stackworx/react-hook-form-mui@stackworx/react-hook-form-mui-x-date-pickers@stackworx/react-hook-form-mui-x-date-pickers-proOnce that's done, publishing a GitHub release from
mainpublishes 0.1.0.Reviewing
Each commit is one idea and passes
npm run checkon its own, so it reads well commit by commit.Testing
npm run check: dprint, ESLint, TypeScript, 127 Vitest tests and the library builds.Follow-ups
FormErrorMessagesProvidertoo. Today they only use their ownmessages.requiredon the range pickers never fires, because an empty range is[null, null]. We need to decide whether it should mean both dates.suppressFormChange, a date field keeps typed text the form refused, because MUI X keeps its own text for each part of the date.NumberFieldshows it until the field loses focus.suppressFormChange, a picker's own error, such asminDate, only appears once the field is touched or the form is submitted.suppressFormBlur, if a form ever needs to skip the form's blur handler.🤖 Generated with Claude Code