diff --git a/.changeset/adapter-test-kit.md b/.changeset/adapter-test-kit.md new file mode 100644 index 0000000..fed57ba --- /dev/null +++ b/.changeset/adapter-test-kit.md @@ -0,0 +1,5 @@ +--- +"@javaabu/forms": minor +--- + +New `@javaabu/forms/testing`: `testAdapter(myAdapter)` runs conformance tests that render every component through an adapter and check labels, values, native submission, errors and ARIA. The adapter contract is typed (`defineAdapter`, `FormsComponents` and a `…SlotProps` type per slot) and documented in `docs/advanced-usage/adapters.md`. `@testing-library/react` is an optional peer dependency. diff --git a/.changeset/automatic-errors.md b/.changeset/automatic-errors.md new file mode 100644 index 0000000..bb94c4b --- /dev/null +++ b/.changeset/automatic-errors.md @@ -0,0 +1,5 @@ +--- +"@javaabu/forms": minor +--- + +Validation errors are now handled by the core, for every adapter. Errors come from Inertia page props, `useForm()`, a JSON 422 response, the `errors` prop or `FormsProvider errors`. Fields match them in dot or bracket notation, and array fields (`tags[]`) also collect their items' errors (`tags.0`). Error bags work with `errorBag`. New: `ErrorSummary`, `ValidationError`, `errorsFor`, `selectErrorBag` and `useFormState()`. Invalid fields get `aria-invalid` and `aria-describedby`, and the first one is focused when errors arrive. diff --git a/.changeset/binding-tests.md b/.changeset/binding-tests.md new file mode 100644 index 0000000..9c12eab --- /dev/null +++ b/.changeset/binding-tests.md @@ -0,0 +1,5 @@ +--- +"@javaabu/forms": patch +--- + +Fields are controlled by the core and still submit natively. Model binding, old input (including `null` old values), nested and array names, checkbox groups, radio groups, relations and date formats follow javaabu/forms. Radios in a group now update together when one is picked. diff --git a/.changeset/docs-and-hidden-panels.md b/.changeset/docs-and-hidden-panels.md new file mode 100644 index 0000000..2802ed4 --- /dev/null +++ b/.changeset/docs-and-hidden-panels.md @@ -0,0 +1,5 @@ +--- +"@javaabu/forms": patch +--- + +Tab panels stay mounted in every adapter (and accordion items in the HTML and shadcn adapters), so fields in other tabs keep their values and submit, as in Blade. The tab or item with an invalid field opens when errors arrive. New for adapters: `TabPanel` and `useReveal`. `Textarea wysiwyg` passes `className` and `onChange` on. The docs are rewritten for the adapter-based API, with `llms.txt`, an API reference, an upgrade guide from `@javaabu/react-forms`, and a Laravel Boost guideline and skill (`javaabu-react-forms`). diff --git a/.changeset/i18n-strings.md b/.changeset/i18n-strings.md new file mode 100644 index 0000000..caaf9c2 --- /dev/null +++ b/.changeset/i18n-strings.md @@ -0,0 +1,5 @@ +--- +"@javaabu/forms": minor +--- + +Strings now come from `@javaabu/i18n` under the Blade package's keys (`forms::strings.nothing_selected`), so apps change them in `lang/vendor/forms/{locale}/strings.php` as they do for the Blade components. Generated labels go through `__()` by default. The locale follows `@javaabu/i18n` (``, `setLocale()`) instead of Onu's. `FormsProvider messages` is gone, and the string keys are now snake_case like Blade's (`t("nothing_selected")`). diff --git a/.changeset/inertia.md b/.changeset/inertia.md new file mode 100644 index 0000000..67669eb --- /dev/null +++ b/.changeset/inertia.md @@ -0,0 +1,5 @@ +--- +"@javaabu/forms": minor +--- + +New `@javaabu/forms/inertia`. `` submits every form as an Inertia visit, with files, method spoofing, error bags, progress, `preserveScroll` / `preserveState` (default `"errors"`) and `resetOnSuccess`. Errors and old input come from the page props. `
` binds the fields to `useForm` data, nested names and checkbox groups included. `inertiaLinks` puts Inertia's `Link` in the `Link` slot. Without Inertia, `fetchRouter` submits JSON requests and shows 422 errors. `@inertiajs/react` (v2 or v3) is an optional peer dependency. diff --git a/.changeset/initial-release.md b/.changeset/initial-release.md deleted file mode 100644 index 3c66eed..0000000 --- a/.changeset/initial-release.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@javaabu/react-forms": minor ---- - -First release: React versions of the javaabu/forms Blade components on Onu. Includes model binding, old input, Laravel validation errors, method spoofing, the select2 cascades and fallbacks, file uploads, conditional sections, and listing tables with select-all and sorting. diff --git a/.changeset/loading-buttons.md b/.changeset/loading-buttons.md new file mode 100644 index 0000000..55c440a --- /dev/null +++ b/.changeset/loading-buttons.md @@ -0,0 +1,5 @@ +--- +"@javaabu/forms": patch +--- + +Submit buttons of the HTML and shadcn adapters submit again with `animate`. While loading, they use `aria-disabled` and ignore clicks instead of `disabled`, because a button disabled during its own click doesn't submit the form. The shadcn adapter now styles `Form`, `DescriptionList`, `DescriptionItem` and `EmptyState`, and lays out `inline` fields side by side. New example pages for Inertia and fetch, with a mock Laravel endpoint. diff --git a/.changeset/parity-v2.md b/.changeset/parity-v2.md new file mode 100644 index 0000000..7adf44c --- /dev/null +++ b/.changeset/parity-v2.md @@ -0,0 +1,5 @@ +--- +"@javaabu/forms": minor +--- + +Parity with javaabu/forms v2. `Form summary` now defaults to `false`, as in Blade; a general message (a failed submit) still shows at the top. `Errors` and `ErrorSummary` take `bag`. `Textarea wysiwyg` renders the rich text editor. New strings: `per_page` (the `PerPage` default label), `export_title` and `export_options`. The parity matrix is in `docs/parity.md`. diff --git a/.changeset/portable-declarations.md b/.changeset/portable-declarations.md new file mode 100644 index 0000000..1b54e8a --- /dev/null +++ b/.changeset/portable-declarations.md @@ -0,0 +1,5 @@ +--- +"@javaabu/forms": patch +--- + +Installing from Git no longer fails in CI when pnpm's store is inside `node_modules` (TS2883 during the `prepare` build). `Wysiwyg`, and the Onu editor and maps slots, have explicit return types. diff --git a/.changeset/rename-to-javaabu-forms.md b/.changeset/rename-to-javaabu-forms.md new file mode 100644 index 0000000..0f70a34 --- /dev/null +++ b/.changeset/rename-to-javaabu-forms.md @@ -0,0 +1,5 @@ +--- +"@javaabu/forms": minor +--- + +Renamed from `@javaabu/react-forms` to `@javaabu/forms`. The package now ships CommonJS next to ESM, and builds itself when installed from Git (`github:Javaabu/react-forms#next-gen`). diff --git a/.changeset/rtl.md b/.changeset/rtl.md new file mode 100644 index 0000000..8d590eb --- /dev/null +++ b/.changeset/rtl.md @@ -0,0 +1,5 @@ +--- +"@javaabu/forms": patch +--- + +The HTML adapter flips side-pointing arrows in right-to-left pages. Thaana typing (`thaana`) is documented and tested for every adapter. Dhivehi lines that need a native speaker's review are listed in `docs/advanced-usage/dhivehi-and-rtl.md`. diff --git a/.changeset/shadcn.md b/.changeset/shadcn.md new file mode 100644 index 0000000..c7961d2 --- /dev/null +++ b/.changeset/shadcn.md @@ -0,0 +1,5 @@ +--- +"@javaabu/forms": minor +--- + +New `@javaabu/forms/shadcn`: `createShadcnAdapter({ Button, Input, Label, … })` makes an adapter from the app's own shadcn/ui components. Slots whose components are missing fall back to plain HTML. Searchable, multiple, tag and server-searched selects use the shadcn combobox (`Popover` + `Command`). Radios in a group now update together with any adapter, Radix's button radios included. diff --git a/.changeset/ui-agnostic-core.md b/.changeset/ui-agnostic-core.md new file mode 100644 index 0000000..2824fce --- /dev/null +++ b/.changeset/ui-agnostic-core.md @@ -0,0 +1,5 @@ +--- +"@javaabu/forms": minor +--- + +The core no longer depends on Onu. Components render through an adapter: `` with `onu` from `@javaabu/forms/onu`, and `components={{ Select: MySelect }}` replaces single components for the whole app or one form. Without an adapter, components render plain HTML (`@javaabu/forms/html`). `Wysiwyg` and `MapInput` are now in the main entry. Onu's editor and maps come from `@javaabu/forms/onu/editor` and `/onu/maps`, combined with `mergeAdapters(onu, onuEditor, onuMaps)`. The `./editor` and `./maps` entries are gone. Onu is now an optional peer dependency (`^0.2.1`). diff --git a/.changeset/wayfinder.md b/.changeset/wayfinder.md new file mode 100644 index 0000000..3be37a6 --- /dev/null +++ b/.changeset/wayfinder.md @@ -0,0 +1,5 @@ +--- +"@javaabu/forms": minor +--- + +`Form action` accepts Laravel Wayfinder routes: route objects (`store()`), form variants (`update.form(user)`) and route functions (`update`, called with the form's `model`). The route's method is used unless `method` is set, with `_method` spoofing for `PUT`, `PATCH` and `DELETE`. Nothing extra is needed to install. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7270e4b..ba01824 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,16 +2,24 @@ name: CI on: push: - branches: [main] + branches: [main, next-gen] pull_request: +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + jobs: check: + name: Lint, typecheck, test, build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22 cache: pnpm @@ -19,5 +27,51 @@ jobs: - run: pnpm lint - run: pnpm format:check - run: pnpm typecheck - - run: pnpm test + - name: Unit tests with coverage + run: pnpm test:coverage + - name: Upload coverage + uses: codecov/codecov-action@v5 + with: + files: coverage/lcov.info + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false - run: pnpm build + - name: Examples + run: pnpm examples:build + + git-install: + name: Install from Git (store under node_modules) + # Apps install `github:Javaabu/react-forms#next-gen`, so pnpm builds the package (`prepare`) in a temp + # folder inside its store. On GitHub Actions that store is under `node_modules`, which once broke + # declaration emit (TS2883, #20). Install this commit the same way, into a fresh app. + runs-on: ubuntu-latest + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository + steps: + - uses: actions/checkout@v5 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v5 + with: + node-version: 22 + - name: Install @javaabu/forms from Git into a new app + env: + SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + app="$RUNNER_TEMP/app" + mkdir -p "$app" && cd "$app" + cat > package.json < @@ -14,39 +17,58 @@ The API follows the Blade components. Blade attributes become camelCase props, a ``` ```tsx -import * as Forms from "@javaabu/react-forms" - -; - - - - Save - +import { Checkbox, Form, Select2, Submit, Text } from "@javaabu/forms" +import { update } from "@/actions/App/Http/Controllers/ArticlesController" + +; + + + + Save + ``` -## Documentation +## Features -- [Installation and setup](docs/installation-and-setup.md) -- [Migrating from Blade](docs/migrating-from-blade.md): every component, prop mapping and difference -- Basic usage: [model binding](docs/basic-usage/model-binding.md), [inputs and textarea](docs/basic-usage/inputs-and-textarea.md), [checkboxes and radios](docs/basic-usage/checkbox-and-radio.md), [selects](docs/basic-usage/select-elements.md), [Select2, ajax and cascading selects](docs/basic-usage/select2.md), [dates](docs/basic-usage/dates.md), [files and images](docs/basic-usage/file-inputs.md), [conditional wrapper](docs/basic-usage/conditional-wrapper.md), [forms, buttons and errors](docs/basic-usage/forms-and-buttons.md), [display components](docs/basic-usage/display.md), [listing pages](docs/basic-usage/listing.md), [WYSIWYG](docs/basic-usage/wysiwyg.md), [map input](docs/basic-usage/map-input.md) +- **The Blade API.** The same component names, camelCase attributes, generated ids and translated labels. +- **Laravel built in.** Model binding, old input, method spoofing, CSRF, relations, `sync_` fields, and validation errors under the right field, including error bags and `items.0.name` keys. +- **Any UI.** Adapters for Onu, shadcn/ui and plain HTML, a typed adapter contract with a conformance test kit, and per-component overrides. +- **Inertia and Wayfinder.** Forms submit as Inertia visits, or through `useForm()`, without glue code, and take Wayfinder routes as `action`. They also submit natively or with fetch. +- **Dhivehi and RTL.** English and Dhivehi strings with [@javaabu/i18n](https://github.com/Javaabu/i18n), RTL layouts and Thaana typing. + +## Installation -## What carries over from Blade +```bash +npm install @javaabu/forms +``` + +```tsx +import { FormsProvider } from "@javaabu/forms" +import { inertia } from "@javaabu/forms/inertia" +import { onu } from "@javaabu/forms/onu" -- **Model binding**: `Form model`, `` (Blade's `@model`), `model={false}`, nested models and relations. -- **Value precedence**: old input, then the bound value, then `default`. -- **Generated labels and ids**: `company_name` → "Company Name" / `company_name`, `address[city]` → `address-city`. Labels go through your translator. -- **Laravel requests**: `_method` spoofing, the `_token` field, `multipart/form-data`, `sync_` fields for multi-selects, `name[]` arrays. Validation errors appear under their fields: `Form` reads a 422 response, Inertia's `errors`, or your `errors` prop. -- **js-utilities behaviour, in React**: `conditionalDisplay`, select2 ajax search, cascading selects with fallback inputs, `fileUploadInput`, `selectAllCheckbox`, `sortable` headings, `animate-submit`, `urlInput`. -- **Strings**: the `en` and `dv` strings of `javaabu/forms`, following Onu's locale. +; + {children} + +``` + +See [Installation and setup](docs/installation-and-setup.md). + +## Documentation + +The full documentation is in [`docs/`](docs/intro.md), with an index for AI assistants in [`llms.txt`](llms.txt). Start with: + +- [Installation and setup](docs/installation-and-setup.md) +- [Forms and buttons](docs/basic-usage/forms-and-buttons.md), [model binding](docs/basic-usage/model-binding.md), [validation errors](docs/basic-usage/errors.md) +- [UI adapters](docs/advanced-usage/adapters.md), [shadcn/ui](docs/advanced-usage/shadcn.md), [Inertia](docs/advanced-usage/inertia.md), [Wayfinder](docs/advanced-usage/wayfinder.md) +- [Migrating from Blade](docs/migrating-from-blade.md), [upgrading from @javaabu/react-forms](docs/upgrading.md), [parity with javaabu/forms](docs/parity.md) ## Development ```bash pnpm install -pnpm dev # playground with every component (add ?dv for Dhivehi) -pnpm test -pnpm lint && pnpm typecheck && pnpm format:check -pnpm build +pnpm examples # one page per adapter (add ?dv for Dhivehi) +pnpm check # lint, format, typecheck, tests, build, examples ``` ## License diff --git a/docs/about-us.md b/docs/about-us.md new file mode 100644 index 0000000..4731c86 --- /dev/null +++ b/docs/about-us.md @@ -0,0 +1,10 @@ +--- +title: About Us +sidebar_position: 1.9 +--- + +[Javaabu](https://javaabu.com) is a webdesign agency based in Male', Maldives. + +As we dive into creating top-notch projects for our clients, we stumble upon nifty solutions that make life easier for developers like us. When we realize we've cracked the code to common problems, we tidy it up and share the love. + +Catch our open-source projects on [Github](https://github.com/Javaabu). diff --git a/docs/advanced-usage/_category_.json b/docs/advanced-usage/_category_.json new file mode 100644 index 0000000..88a4e51 --- /dev/null +++ b/docs/advanced-usage/_category_.json @@ -0,0 +1,11 @@ +{ + "position": 1.4, + "label": "Advanced Usage", + "collapsible": true, + "collapsed": false, + "link": { + "type": "generated-index", + "slug": "/forms/_categories/advanced-usage", + "description": "Adapters for UI frameworks, Inertia, Wayfinder, routers and custom submission" + } +} diff --git a/docs/advanced-usage/adapters.md b/docs/advanced-usage/adapters.md new file mode 100644 index 0000000..7862471 --- /dev/null +++ b/docs/advanced-usage/adapters.md @@ -0,0 +1,142 @@ +--- +title: UI adapters +sidebar_position: 1 +--- + +The components don't draw anything themselves. Each one works out its name, id, label, value, errors and ARIA attributes, then renders a **slot** of the active **adapter**. An adapter maps slots to a UI framework's components. + +| Adapter | Import | Notes | +| --------------- | ------------------------------------------- | ----------------------------------------------------------- | +| HTML (default) | `html` from `@javaabu/forms/html` | Plain, unstyled elements with `data-slot` attributes | +| Onu | `onu` from `@javaabu/forms/onu` | Javaabu's React UI library | +| Onu editor | `onuEditor` from `@javaabu/forms/onu/editor` | Onu's Tiptap editor for `Wysiwyg` | +| Onu maps | `onuMaps` from `@javaabu/forms/onu/maps` | Onu's Google Maps pickers for `MapInput` | +| shadcn/ui | `createShadcnAdapter()` from `@javaabu/forms/shadcn` | Made from your app's own shadcn components ([shadcn/ui](./shadcn)) | + +# Choosing an adapter + +```tsx +import { FormsProvider, mergeAdapters } from "@javaabu/forms" +import { onu } from "@javaabu/forms/onu" +import { onuEditor } from "@javaabu/forms/onu/editor" + +const ui = mergeAdapters(onu, onuEditor) + +export function App({ children }) { + return {children} +} +``` + +`mergeAdapters()` combines adapters slot by slot; later ones win. Define the merged adapter once, outside your components, so it keeps its identity. + +Any slot an adapter leaves out renders with the HTML adapter, so an adapter can be partial. + +# Replacing single components + +Pass `components` to a `FormsProvider` (for the app or a part of the page), or to one `Form`: + +```tsx +import { FormsProvider, type SelectSlotProps } from "@javaabu/forms" + +function CountrySelect(props: SelectSlotProps) { + // Your own select. It gets the resolved options, value, name, id and ARIA props. + return +} + +… + +
…
+``` + +The order is: `components` of the nearest provider or form, then the `ui` adapter, then the HTML adapter. + +# Writing an adapter + +An adapter is a plain object. Type it with `defineAdapter()`: + +```tsx +import { defineAdapter, type InputSlotProps, type FieldSlotProps } from "@javaabu/forms" +import { Input } from "my-ui" + +function MyInput({ prepend, append, thaana, thaanaToggle, ...props }: InputSlotProps) { + // `props` are native attributes: name, id, type, value, onChange, required, aria-*… + return +} + +function MyField({ id, label, hideLabel, help, helpId, errors, errorId, children }: FieldSlotProps) { + return ( +
+ {label ? ( + + ) : null} + {children} + {help ?

{help}

: null} + {errors.length ? ( +
+ {errors.map((e) =>

{e}

)} +
+ ) : null} +
+ ) +} + +export const myUi = defineAdapter({ + name: "my-ui", + components: { Input: MyInput, Field: MyField }, + // Optional: dialogs and notifications for file uploads. + confirm: (options) => myConfirm(options), + notify: (message, { type } = {}) => myToast(message, type), +}) +``` + +The source of the HTML adapter (`src/adapters/html/index.tsx`) is the reference: every slot, in plain elements. + +## Rules + +1. **Controlled values.** Value slots get `value` (or `checked`) and report changes with `onValueChange` / `onCheckedChange`. `Input` and `Textarea` get native props with a controlled `value` and an `onChange`: spread them onto the element. Don't keep your own copy of the value. +2. **Native submit works.** A control with a `name` puts its value into the form's `FormData` under that name, with a native input or a hidden one. A multiple `Select` gets a name that ends in `[]`; submit one entry per value. A `name` of `""` means "don't submit" (uploading file inputs). +3. **ARIA on the focusable element.** Put `id`, `aria-invalid` and `aria-describedby` on the element that takes focus, so labels, errors and "focus the first invalid field" work. +4. **The label is ready.** `Field`'s `label` already includes the required marker. Render it inside a `