Skip to content

refactor(web): network connection form UX and composition improvements (part 2) #3338

Open
dgdavid wants to merge 16 commits intoenhance-network-connection-formfrom
network-tanstack-form-part2
Open

refactor(web): network connection form UX and composition improvements (part 2) #3338
dgdavid wants to merge 16 commits intoenhance-network-connection-formfrom
network-tanstack-form-part2

Conversation

@dgdavid
Copy link
Copy Markdown
Contributor

@dgdavid dgdavid commented Mar 27, 2026

TL;DR: Refine connection form for cleaner device and IP settings interface, with clearer option labels and a more logical field order.


This PR continues the reimplementation of the network connection form, built on top of the TanStack Form foundation introduced in part 1. It focuses on two areas: refining the IP settings and device binding UX, and improving the form architecture to align with TanStack Form's composition model.

What is included

IP settings redesign

The IP settings block is extracted into a standalone IpSettings component, rendered once per protocol. The settings selector offers three options: Automatic, Manual, and Advanced. Manual and Advanced reveal address and gateway fields while Automatic keeps the form clean. Option descriptions tries to use no user-targeting language as well as no implementation-detail jargon.

Device binding redesign

The interface binding section is extracted into BindingModeSelector and DeviceSelector components. Options use plain language (Any, Chosen by name, Chosen by MAC) rather than technical terms as in the current (to be drop) BindingSettingsForm. DeviceSelector's label is visually hidden since the binding selector already provides visual context.

TanStack Form composition

These components were first implemented making use of useFormContext(), which is deliberately untyped since it is designed for generic leaf field components where the form type is not known at definition time. But later rewritten to use withForm() instead, since it gives each component a typed form instance, removes all "as any" casts, and catches field name errors at compile time.


Two example of the form in action. The form has several other states worth exploring it by yourself. The easiest way is to check out the branch and play with it.

Keep in mind this is an intermediate step. Some things are intentionally simplified for now, such as the textarea inputs that will eventually become a more fluid multi-value experience.

Unbound, fully automatic connection Bind by name, manual IPv4 settings
localhost_8080_ (20) localhost_8080_ (21)

For the reasoning behind field visibility and label choices, see conventions.md.


What is deferred to follow-up PRs

Same as part 1, still pending:

Notes for reviewers

This PR took longer than expected due to four iterations of the form layout (still not fully finalized) before finding a version that balances an easy-to-understand workflow with the ability to configure complex connections.

Although it has almost the same pending items as the previous commit, it was opened as a separate PR rather than keep adding more and more changes.

The intermediate commits from the UI iterations have been preserved in the history. It does not hurt to have them there and may be useful if any past decisions need to be revisited. Therefore, it is recommended to focus on reviewing the final result rather than each commit individually. For details on the final layout reasoning, see the notes in #3337.

Finally, TSDoc blocks and code can still be improved or shortened in some places. However, documenting the first TanStack Form components thoroughly provides helpful context, as these components are new to the project and the documentation migth aid understanding.

Changelog entry postponed for the final PR from feature branch against master.


Related to https://trello.com/c/rUEeqOkf (protected link)

dgdavid added 14 commits March 24, 2026 10:08
Introduces `ChoiceField`, a generic TanStack Form-aware select component
for mode/behavior selection. Registered in `fieldComponents` so it is
accessed as `field.ChoiceField` inside `form.AppField` render props.

Splits form contexts into `hooks/form-contexts.tsx` to avoid the
circular import that would arise from field components importing
`useFieldContext` while `hooks/form.tsx` imports those same components
for registration.
Adds Pattern 5 (choice selector) and renumbers the former patterns 2–5
to make room for it. Reorders patterns from least to most intrusive and
rewrites the "Combining patterns" and "Choosing the right pattern"
sections accordingly.
Renders a protocol-specific IP configuration block using a three-level
structure: mode selector (Default/Custom), method selector
(Automatic/Manual), and the corresponding fields. Manual mode shows
required addresses plus optional gateway and DNS. Automatic mode offers
an opt-in checkbox for extra static settings on top of DHCP.
Uses `useFormContext` following the TanStack Form composition guide, so
it can be dropped into any `useAppForm`-backed form without
prop-drilling. Field names are passed explicitly via `fieldNames`.
Replace the flat IPv4/IPv6 method selectors, the combined IP Addresses
textarea, and the top-level "Use custom DNS servers" checkbox with two
IpSettings components, one per protocol. Each manages its own mode
(Default/Custom), method (Automatic/Manual), addresses, gateway,
nameservers, and the opt-in "With extra IPv4/IPv6 settings" toggle.

Wrap the form body in form.AppForm so child components can access the
form context via useFormContext. The submit handler merges per-protocol
addresses and nameservers into the flat arrays expected by the
Connection constructor, including values only from protocols with active
custom configuration.

All IpSettings field labels are prefixed with the protocol name (e.g.
"IPv4 Gateway" instead of "Gateway") so each label is self-sufficient
when announced by a screen reader navigating outside the visual
grouping. Sighted users benefit too: the prefix removes any ambiguity
when both protocols are visible at once. See WCAG 2.4.6 (Headings and
Labels):

https://www.w3.org/WAI/WCAG21/Understanding/headings-and-labels.html

The gateway field in DHCP+extra mode carries the suffix "(optional,
ignored without a static IP)" to clarify that a gateway alone has no
effect without at least one static address alongside it.
Replace the two-level Default/Custom mode + Auto/Manual method + "With
extra settings" checkbox structure with a single three-option selector:
Default, Automatic, and Manual.

  - IpSettings now takes only three field names: mode, addresses, gateway
  - Addresses are optional in Automatic mode and required in Manual mode
  - Gateway is shown for both non-default modes; omitted at submit time
    unless at least one address is present
  - DNS servers moved back to the top level in ConnectionForm as a shared
    checkbox+textarea
Most users choosing Automatic (DHCP) do not need to set static
addresses or a gateway alongside it. Showing those fields upfront
adds visual noise and may confuse users who are not aware that
combining DHCP with static settings is even a valid configuration.

A "Show advanced settings" toggle now appears alongside the mode
selector in Automatic mode, hiding the extra fields until explicitly
requested. Manual mode is unaffected and always shows addresses and
gateway, since those are the point of choosing it.

The submit handler only collects addresses and gateway for Automatic
mode when the advanced toggle is enabled.
…pproach 2)

Previous iterations either showed static address fields for all
Automatic users (too much upfront complexity) or hid them behind a
checkbox toggle alongside the selector (two controls where one should
suffice). Both violated the principle of progressive disclosure.

This approach keeps a single selector but adds a fourth option, Mixed,
which makes the uncommon DHCP+static combination an explicit choice
rather than a hidden extra. Users who just want DHCP pick Automatic and
see nothing else. Users who need static addresses on top of DHCP pick
Mixed and get exactly that. The right complexity is revealed only when
the user actively asks for it.

Descriptions do the heavy lifting: instead of hiding complexity behind
a toggle, they guide the user toward the right choice, including a
"not needed for most setups" nudge on Mixed.

Each option carries a protocol-aware description where relevant:
Automatic shows DHCP for IPv4 and SLAAC or DHCPv6 for IPv6, reflecting
how NetworkManager's auto method behaves differently per protocol.
Replace the plain FormSelect-based interface field with two dedicated
ChoiceField components for interface binding UI:

- BindingModeSelector: wraps the binding mode selection (Unbound / By
  device name / By MAC address) with descriptions that clarify each
option's effect on the connection.

- DeviceSelector: picks a network device by interface name or MAC
  address; the non-selected identifier is shown as a styled description
for context.

Both components use useFormContext internally and are laid out
side-by-side via Flex/FlexItem in ConnectionForm, with DeviceSelector
appearing only when the mode is not Unbound (via form.Subscribe).
The Default option is dropped from IP settings. It represented a
NetworkManager implementation detail (no method written to the profile)
that most users would not understand. Automatic now covers that common
case, with NetworkManager deciding how to configure the interface.
Mixed is renamed to Advanced DHCP (IPv4) or Advanced Automatic (IPv6),
making it clear it extends the automatic case rather than a separate
alternative.

Option descriptions are rewritten to be concise and neutral: no trailing
periods, no user-targeting language, no protocol jargon.

"Interface binding" renamed to "Device" and its options drop technical
vocabulary in favor of plain language: Any, Chosen by name, and Chosen
by MAC. The device picker that appears next to the binding selector gets
a visually hidden label, since the selector already provides enough
context.
The IPv4 and IPv6 mode selectors used different labels for the same
option: "Advanced DHCP" and "Advanced Automatic". The distinction was
technically accurate: IPv4 automatic addressing uses DHCP, while IPv6
uses SLAAC, making "DHCP" wrong for IPv6. Hence "Advanced Automatic".

However, such asymmetry could raise the exact question it tried to
avoid: why does one selector say DHCP and the other say Automatic? Both
words describe the underlying mechanism, not the outcome, conflicting
with the plain-language description style adopted for all other options.
This commit changes both labels to "Advanced". The description still
conveys the relationship to Automatic without encoding protocol-specific
implementation details in the label.
…-components

IpSettings, BindingModeSelector, and DeviceSelector previously used
useFormContext(), which is designed for generic leaf field components
and is deliberately untyped. Since these components render slices of a known
form, withForm() is the correct fit. The mismatch forced "as any" casts on
every field name and subscribe selector, silently bypassing TypeScript's
checks and leaving field renames undetected at compile time.

Rewriting the three components with withForm() removes all "as any"
casts and restores full type safety. The form options were extracted from
ConnectionForm using formOptions() and exported to allow sub-components
spread it in their withForm() definition for type inference.

Also, a convenience mergeFormDefaults helper has been added for handling
the case where some defaults depend on runtime hook values and cannot be
declared statically.
And use <Text srOnly> directly instead. HiddenLabel implied a <label>
element but just rendered <Text srOnly>.
FlexItem with no props adds no value. Flex lays out its direct children
as flex items regardless.
@dgdavid dgdavid requested review from ancorgs and teclator March 27, 2026 03:19
*/
import { createFormHookContexts } from "@tanstack/react-form";

export const { fieldContext, formContext, useFieldContext, useFormContext } =
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useFormContext is exported but currently unused. It was originally the mechanism used by IpSettings, BindingModeSelector, and DeviceSelector to access the form instance, but was later replaced by withForm(). It is kept because it is the intended API for components registered in formComponents (e.g. a submit button that reacts to form state), which are not yet present but expected as the form layer grows.

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.

1 participant