AddressForm
Add/edit an address on the evolve-feature-form pattern: a RAC Form whose
validationErrors drive display, clean until the first submit then live, and a
submit button that is never disabled.
import { AddressForm } from "@evolve-storefront/commerce-ui/address-form";Renders inside AddressConfigProvider.
Design notes
Country-aware: the country select comes first because it drives both the field
layout and the schema (a US address needs a region; a Dutch one is
postal-code-first). Configure the country list and the field set through
AddressConfigProvider; with no provider the form behaves as it always has.
It performs no write. onValidSubmit receives the validated values and the
consumer runs its own mutation — the address book runs useAddressForm from
feature-account, checkout runs a cart update. That is what lets one form serve
every surface without commerce-ui depending on a feature.
Compound: this root owns the <form> and its state and seeds
AddressFormContext, while AddressFormFields and AddressFormSubmit read
it from there. That is what lets AddressDialog put cancel and submit in a
DialogFooter without this form importing anything dialog-specific.
Preview
Usage
A new address starts in the store's country, from AddressConfigProvider's
defaultCountry, so the layout and the rules are the right ones before anyone
touches the select. preferredCountries puts it at the top of the list; the rest
follow by name.
<AddressConfigProvider countries={EU_COUNTRY_CODES} defaultCountry="NL" preferredCountries={["NL"]}> <AddressForm onValidSubmit={save}>…</AddressForm></AddressConfigProvider>;Props
| Prop | Type | Description |
|---|---|---|
onValidSubmit | (values: { givenName: string; familyName: string; streetName: string; streetNumber: string; postalCode: string; city: string; country: string; phone: string | u… | Runs with the validated values on a valid submit — where the mutation goes. |
children | ReactNode | The composed pieces — typically |
validationBehavior? | "aria" | "native" | Default |
defaultValues? | Partial<{ givenName: string; familyName: string; streetName: string; streetNumber: string; postalCode: string; city: string; country: string; phone: string | un… | |
onValuesChange? | ((values: Partial<{ givenName: string; familyName: string; streetName: string; streetNumber: string; postalCode: string; city: string; country: string; phone: s… | Every change, typed or filled by a lookup, unvalidated. For a caller that keeps
unsaved input when the form unmounts and passes it back as |
supplied? | Partial<{ givenName: string; familyName: string; streetName: string; streetNumber: string; postalCode: string; city: string; country: string; phone: string | un… | Values the caller already holds, such as a name asked for earlier in a checkout. Their fields are not rendered; the values are validated and submitted as given. |
isPending? | boolean | |
error? | string | null | Form-level error (e.g. a failed mutation), shown above the actions. |
serverErrors? | FormValidationErrors | Server-provided field errors, merged into the same display path. |
Any other props go to the root element, like on a plain HTML element.
Change the layout
Pass children to AddressForm to arrange its parts yourself. Every part is exported from @evolve-storefront/commerce-ui/address-form.
AddressFormContextThe context AddressForm fills.
No props of its own.
AddressFormFieldsThe country-aware address fields, seeded from AddressForm's context.
The form-level error sits at the end of this part rather than in the footer, so it reads above the actions wherever the caller places them.
No props of its own.
AddressFormSubmitThe form's submit button — never disabled (per the evolve-feature-form pattern: submission is gated by validity in the form's onSubmit, not by a disabled control).
| Prop | Type | Description |
|---|---|---|
pendingLabel | ReactNode | Shown while a submit is in flight. |
isPending? | boolean | The action is running. The button keeps its label, so its width and
accessible name hold, shows a spinner and is |
variant? | ButtonVariant | |
tone? | ButtonTone | |
isIconOnly? | boolean | The button has no visible label, so it is square. Structural rather than a size: it says the button has nothing to be wide for, and says nothing about how big — the brand's control tokens still decide that. MoreNot inferred from the children. A kebab menu is as often a glyph or an SVG
as an |
asChild? | boolean | |
data-component? | undefined | A button's identity is not a caller's to rename — see |
Any other props go to the root element, like on a plain HTML element.
CountrySelectCountry picker whose value is the 2-letter ISO code and whose labels are localized region names from Intl.DisplayNames, sorted with Intl.Collator because alphabetical order differs per locale; preferredCountries come first.
| Prop | Type | Description |
|---|---|---|
defaultValue? | string | |
required? | boolean | |
autoComplete? | string | The autofill hint for the hidden native select, such as |
name? | string | |
disabled? | boolean | |
value? | string | |
data-component? | undefined | Not a caller's to rename — see |
onValueChange? | ((value: string) => void) | Hands over the chosen value. Not |
invalid? | boolean | |
description? | ReactNode | |
labelClassName? | string | |
placement? | SelectPlacement | |
countries? | readonly string[] | The selectable ISO-3166-1 alpha-2 codes. Defaults to the list from
|
preferredCountries? | readonly string[] | Listed first, in this order, such as the store's country; the rest follow by
name. Defaults to |
label? | ReactNode | |
placeholder? | string |
Any other props go to the root element, like on a plain HTML element.
Examples
EditSeeded (edit), NL — postal code first, no region.
// `defaultValues` is what makes edit mode work; the country in it picks the// layout, so a Dutch address renders postal-code-first with no region field.<AddressForm defaultValues={toDefaults(address)} onValidSubmit={save}> {/* … */}</AddressForm>;United StatesUS — the region field appears and is required; street name comes first.
// Nothing is configured for this: `COUNTRY_FIELD_RULES` makes `region`// required for US/CA/AU/IN/BR, and the layout map puts the number after the// street name. Picking the country is the whole API.<AddressForm defaultValues={{ country: "US" }} onValidSubmit={save}> {/* … */}</AddressForm>;Minimal Field SetA client shipping four countries with salutation, company and email off.
// Wrap the app once. `false` switches a field off entirely rather than hiding// it, so the schema drops it too — a hidden-but-validated field is an// unfixable form.<AddressConfigProvider countries={["NL", "BE", "DE", "FR"]} fields={{ company: false, email: false, salutation: false }}> <AddressForm onValidSubmit={save}>{/* … */}</AddressForm></AddressConfigProvider>;Custom LayoutA custom layout for one country, supplied by the consumer.
// One country's row order, overridden. Unlisted countries keep the shipped// layout, so this is an override rather than a replacement.<AddressConfigProvider countries={["NL", "US"]} layout={{ NL: [["city"], ["streetName", "streetNumber"], ["postalCode"]], }}> <AddressForm onValidSubmit={save}>{/* … */}</AddressForm></AddressConfigProvider>;Supplied NameA name the caller already holds: its fields are left out, and the submitted values carry it.
<AddressForm supplied={{ givenName, familyName }} onValuesChange={setDraft} onValidSubmit={save}>…</AddressForm>;Company On RequestfieldsOnRequest keeps an optional field behind an "Add …" button until the shopper asks for it.
// Most shoppers have no company; the few who do ask for the field.<AddressConfigProvider fieldsOnRequest={["company"]}> <AddressForm onValidSubmit={save}>{/* … */}</AddressForm></AddressConfigProvider>;
