@beforeyoubid/form
v1.0.1
Published
BYB form system — TanStack Form field abstractions composed from @beforeyoubid/design-system primitives
Keywords
Readme
@beforeyoubid/form
BYB's form system — TanStack Form field abstractions composed from @beforeyoubid/design-system primitives. The single source of truth for forms across the BYB platform.
What you get
Three layers:
- Standalone composed fields —
InputField,SelectField,CheckboxField, … each bundles a design-system primitive with a label, hint, error and consistent spacing. Use them anywhere, with or without TanStack Form. createBybFormHook— a thin skin over TanStack'screateFormHookthat pre-populatesfieldComponents/formComponentswith the bound versions of those fields. You supply only the contexts and the functional form config.- (Internal) the context-bound field components wired between the two.
Install
yarn add @beforeyoubid/form @beforeyoubid/design-system @tanstack/react-form@beforeyoubid/design-system, @tanstack/react-form, react, react-dom and tailwindcss (v4) are peer dependencies.
No extra CSS import is required: every class these fields render is already part of the design-system's emitted vocabulary, so the design-system's globals.css (which you already import) covers them.
Usage with TanStack Form
import { createFormHookContexts } from '@tanstack/react-form';
import { createBybFormHook } from '@beforeyoubid/form';
// Create the contexts once (the "functional portion" you own) and build the hook.
const { fieldContext, formContext } = createFormHookContexts();
export const { useAppForm, withForm } = createBybFormHook({ fieldContext, formContext });
function SignupForm() {
const form = useAppForm({
defaultValues: { email: '', agree: false },
onSubmit: ({ value }) => console.log(value),
});
return (
<form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
<form.AppField
name="email"
validators={{ onChange: ({ value }) => (value.includes('@') ? undefined : 'Invalid email') }}
>
{(field) => <field.InputField label="Email" type="email" />}
</form.AppField>
<form.AppField name="agree">
{(field) => <field.CheckboxField label="I agree to the terms" />}
</form.AppField>
<form.AppForm>
<form.SubmitButton>Sign up</form.SubmitButton>
</form.AppForm>
</form>
);
}The bound fields read their value and errors from the active field and call handleChange/handleBlur for you. Errors surface into the field's error slot once the field is touched.
Typing
Everything is fully typed: form-data inference (field-name autocomplete, field.state.value, validators, form.state) and the bound components on form.AppField's render prop. field.InputField autocompletes, rejects unknown/mis-typed props, and flags missing required props (e.g. SelectField without options).
This is achieved by hand-writing the hook's return type (BybFormApiFor) from TanStack's exported generic types, rather than inferring it — TanStack's inferred createFormHook return includes a recursive extendForm member that overflows the TypeScript .d.ts serializer (TS7056). extendForm is therefore the one piece of the return surface not exposed here; if you need it, call TanStack's createFormHook directly.
Caveat — the bound component is not cross-checked against the field's value type. field.InputField (string model) used on a numeric field still type-checks, because TanStack exposes the same field-component map on every field regardless of its value type. Match the component to the field's value model yourself: InputField/TextareaField/PasswordField/… for string, NumberField/CurrencyField for number, CheckboxField/SwitchField for boolean, MultiSelectField/CheckboxGroupField for string[], and so on.
Standalone usage (no TanStack Form)
import { InputField, SelectField } from '@beforeyoubid/form';
<InputField label="Email" value={email} onChange={(e) => setEmail(e.target.value)} error={error} />
<SelectField label="Country" value={country} onValueChange={setCountry} options={options} />FormFieldWrapper is exported too, for composing bespoke fields with the same label/hint/error chrome.
Every field is controlled — drive it with value (and the matching onChange/onValueChange). DateField, DateRangeField, RatingField and CheckboxGroupField do not take a defaultValue; for an uncontrolled-style default, seed your own useState.
Fields
Text & numeric: InputField, NumberField, TextareaField, InputGroupField, PasswordField, SearchField, PhoneField, CurrencyField, TimeField, ColorField, InputOTPField
Choice: SelectField, ComboboxField, MultiSelectField, RadioGroupField, RadioCardField, CheckboxField, CheckboxGroupField, SwitchField, ToggleField, ToggleGroupField
Other: SliderField, RatingField, DateField, DateRangeField, FileUploadField
Plus FormFieldWrapper for composing bespoke fields with the same label/hint/error chrome. Every field works standalone and as a bound form.AppField component.
Scripts
| Command | What it does |
|---|---|
| npm run build | Production build — ESM + CJS + types via tsup |
| npm run dev | Watch-mode build |
| npm run type-check | TypeScript strict check |
| npm run lint | ESLint (flat config) across src/ |
| npm test | Vitest unit + binding tests |
| npm run test:coverage | Vitest with a V8 coverage report |
| npm run verify | type-check + lint + test (the pre-publish gate) |
| npm run storybook | Storybook on port 6006 |
| npm run release | Publish to npm (runs verify + build first) |
Publishing
@beforeyoubid/form is published under the public @beforeyoubid scope (npm auth required).
npm login # one-time, must be a @beforeyoubid maintainer
npm version patch # or minor / major / 0.1.0-alpha.0
npm run release # = npm publish --access publicprepublishOnly runs verify (type-check + lint + test) and a fresh build automatically, so a publish can never ship code that doesn't pass checks. For a pre-release, use npm run release -- --tag alpha to keep it off the latest tag.
