Skip to content

Conversation

logaretm
Copy link
Member

@logaretm logaretm commented Aug 19, 2025

What

This PR breaks up the field composables into smaller, more granular composables for use in UI libraries that follow the field and control pattern.

How

A form-field can be defined as:

  • A control that the user interacts with.
  • A group of labels/descriptions, and error messages.

This PR models this relationship by breaking a field composable into:

  • use___Control: Handles all the logic related to the control (e.g: Interactions and props).
  • useFormField: Handles the state and labeling/descriptions/error message relationships with the control.

Usage

There are a couple of configurations to put all three composables together. A same-component approach and injectable approach.

Let's take a look at a TextField implementation.

Same component approach

Similar to how Formwerk will expose the full field composables, where all three are composed in the same composable. In this case, the labeling is colocated with the control.

// TextField.vue
// Build the state
 const field = useFormField<string | undefined>({
  label: props.label,
  description: props.description,
  path: props.name,
  initialValue: toValue(props.modelValue) ?? toValue(props.value),
  disabled: props.disabled,
  schema: props.schema,
});

// Build the control
const { inputEl, inputProps } = useTextControl(props, { field });

Injection Approach

This suits UI libraries more as they usually maintain their separate FormField component.

At a FormField component implementation:

// FormField.vue
const field = useFormField<string | undefined>({
  label: props.label,
  description: props.description,
  path: props.name,
  initialValue: toValue(props.modelValue) ?? toValue(props.value),
  disabled: props.disabled,
  schema: props.schema,
});

Then, at the control level, like an <Input > component, you only need to call the useTextControl composable:

// Input.vue
const { inputEl, inputProps } = useTextControl(props);

Under the hood the control composable will find the form field context and hook itself up.

Misc

supports genu/ui#1

Copy link

changeset-bot bot commented Aug 19, 2025

🦋 Changeset detected

Latest commit: 5c28e28

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@formwerk/core Minor
@formwerk/devtools Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@logaretm logaretm changed the title feat: initial API draft feat: granular field composables Aug 19, 2025
Copy link

pkg-pr-new bot commented Aug 19, 2025

Open in StackBlitz

npm i https://pkg.pr.new/formwerkjs/formwerk/@formwerk/core@205
npm i https://pkg.pr.new/formwerkjs/formwerk/@formwerk/devtools@205

commit: 5cec028

@genu
Copy link
Contributor

genu commented Aug 21, 2025

@logaretm Say in the Injectable Approach we want to use some value from the state.

Would we use useFieldStateInjection() alongside useTextControl?

So, we would do something like this?

const { errorMessage } = useFieldStateInjection()!
const { inputEl, inputProps } = useTextControl({  name })

@logaretm
Copy link
Member Author

const { errorMessage } = useFieldStateInjection()!
const { inputEl, inputProps } = useTextControl({  name })

You can do that if you want to, as useFieldStateInjection doesn't create the state object it just injects it. Is there a limitation you are trying to bypass?

@logaretm logaretm force-pushed the feat/granular-controls branch 2 times, most recently from 95d0361 to 1342a64 Compare August 21, 2025 08:52
@logaretm
Copy link
Member Author

logaretm commented Aug 21, 2025

@genu Thinking about combining the useFieldState and useFormField because its too confusing to have two required pieces detached like that. What do you think?

EDIT: I just pushed this, feels much cleaner and will make migrating other fields easier.

@genu
Copy link
Contributor

genu commented Aug 21, 2025

@genu Thinking about combining the useFieldState and useFormField because its too confusing to have two required pieces detached like that. What do you think?

EDIT: I just pushed this, feels much cleaner and will make migrating other fields easier.

Just pulled the latest. It definitely feels more elegant 🚀

@logaretm logaretm force-pushed the feat/granular-controls branch 3 times, most recently from 3a85049 to 6705570 Compare August 26, 2025 13:52
@genu
Copy link
Contributor

genu commented Aug 27, 2025

@logaretm Should we export the inputId from use__Control.ts or useFormField() or both? This can be useful when adding extra elements with ids.

For example, in NuxtUi, a FormField has a hint and a help. Currently, I'm doing this:

const { labelProps  } = useFormField(...)
<span :id="`${labelProps?.id}-hint`">...</span>
<div :id="`${labelProps?.id}-help`">...</div>      

@logaretm logaretm force-pushed the feat/granular-controls branch from 12abf4d to 8d838e0 Compare September 8, 2025 19:13
@logaretm logaretm force-pushed the feat/granular-controls branch from 48ac966 to e732324 Compare September 18, 2025 09:34
logaretm and others added 24 commits September 27, 2025 19:25
* chore: register with devtools in `useFormField`

* refactor: update registerField to accept Ref type and use

* feat: introduce BuiltInControlTypes for consistent control type

* chore: fix typo

* fix: types and type reactivity in devtools

---------

Co-authored-by: Abdelrahman Awad <[email protected]>
* feat: add `blurred` state

* chore: touch fields `onInput` and `onChange`

* chore: add ability to blur a field

* feat: track blurred fields

* test: update blur test to check both touched and blurred states

* test: update input test to check touched state and value updates

* feat: add clearBlurred method to reset blurred fields

* test: add tests for handling blurred state in form

* test: enhance field state tests for blurred states

* refactor: remove redundant onChange handler in useTextField

* feat: add blurred state to form transactions and field management

* chore: rebase branch

* feat: implement onBlur handler to set blurred state in useSwitchControl

* feat: manage blurred state in useSlider

* feat: enhance useSelectControl to manage touched and blurred states

* feat: update useRadio to manage blurred state in onBlur handler

* feat: manage touched and blurred states in fillSlots and onBlur handlers

* feat: update useNumberControl to manage blurred state in onBlur handler

* feat: update useFileControl to manage touched state in various handlers

* feat: manage blurred state in date and time controls

* feat: manage touched state in useComboBoxControl

* feat: manage blurred state in useCheckbox and useCheckboxGroup

* feat: manage blurred state in useCalendarControl

* test: fix failing tests and added test cases for touched

---------

Co-authored-by: Abdelrahman Awad <[email protected]>
@logaretm logaretm force-pushed the feat/granular-controls branch from aaf4a4f to 5cec028 Compare September 27, 2025 16:25
@logaretm logaretm marked this pull request as ready for review September 27, 2025 18:02
@logaretm logaretm merged commit 9a959c1 into main Sep 27, 2025
3 checks passed
@logaretm logaretm deleted the feat/granular-controls branch September 27, 2025 18:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants