-
+
{children}
}
-const SelectCountry: React.FC<{ namePrefix: string }> = ({ namePrefix }) => {
+// The two selects take their field paths already resolved, rather than a prefix
+// to reapply: a path is then built in exactly one place, and no string inside
+// these components can quietly miss the namespace the way `state_code` once did.
+const SelectCountry: React.FC<{ countryCodeName: string }> = ({
+ countryCodeName,
+}) => {
const [forceTextInput, setForceTextInput] = useState(false)
const { countries, isLoading, error } = useCountryList()
@@ -239,7 +247,7 @@ const SelectCountry: React.FC<{ namePrefix: string }> = ({ namePrefix }) => {
if (forceTextInput) {
return (
)
@@ -247,7 +255,7 @@ const SelectCountry: React.FC<{ namePrefix: string }> = ({ namePrefix }) => {
return (
= ({ namePrefix }) => {
)
}
-const SelectStates: React.FC<{ namePrefix: string }> = ({ namePrefix }) => {
+const SelectStates: React.FC<{
+ stateCodeName: string
+ countryCodeName: string
+}> = ({ stateCodeName, countryCodeName }) => {
const [states, setStates] = useState()
- const { watch, setValue } =
- useFormContext<
- z.infer>
- >()
+ const { watch, setValue, getValues } = useFormContext()
const [forceTextInput, setForceTextInput] = useState(false)
- const countryCode = watch("country_code")
- const stateCode = watch("state_code")
+ const countryCode: string | undefined = watch(countryCodeName)
+ const stateCode: string | undefined = watch(stateCodeName)
const countryWithStates = ["US", "IT"]
+ // The country the form opened on: a state that isn't in its list is a custom
+ // one the address already carries, not a leftover to be wiped.
+ const initialCountryCode = useRef(countryCode)
useEffect(() => {
if (countryCode != null && countryWithStates.includes(countryCode)) {
@@ -277,9 +288,15 @@ const SelectStates: React.FC<{ namePrefix: string }> = ({ namePrefix }) => {
.then(async (res) => await res.json())
.then((data) => {
setStates(data)
- if (data.find(({ value }) => value === stateCode) == null) {
- // reset state_code if not found in the list
- setValue("state_code", "")
+ // read at response time: the effect's closure predates whatever the
+ // user may have typed while the list was in flight
+ const currentStateCode: string | undefined = getValues(stateCodeName)
+ if (
+ countryCode !== initialCountryCode.current &&
+ data.find(({ value }) => value === currentStateCode) == null
+ ) {
+ // the country changed under it, so the previous state no longer applies
+ setValue(stateCodeName, "")
}
})
.catch(() => {
@@ -290,25 +307,37 @@ const SelectStates: React.FC<{ namePrefix: string }> = ({ namePrefix }) => {
}, [countryCode])
if (
+ countryCode == null ||
!countryWithStates.includes(countryCode) ||
states?.length === 0 ||
forceTextInput
) {
return (
)
}
+ // A custom state keeps its place in the list, so it shows as the selected
+ // value and stays selectable after the user browses the other options.
+ const options =
+ states != null &&
+ stateCode != null &&
+ stateCode !== "" &&
+ states.find(({ value }) => value === stateCode) == null
+ ? [{ value: stateCode, label: stateCode }, ...states]
+ : (states ?? [])
+
return (
)
diff --git a/packages/docs/src/stories/resources/ResourceAddress.stories.tsx b/packages/docs/src/stories/resources/ResourceAddress.stories.tsx
index 8cbaabb3d..b6603aa55 100644
--- a/packages/docs/src/stories/resources/ResourceAddress.stories.tsx
+++ b/packages/docs/src/stories/resources/ResourceAddress.stories.tsx
@@ -141,6 +141,27 @@ export const ApiError: StoryFn = () => {
)
}
+/**
+ * An address can carry a state the official list doesn't know — one typed by
+ * hand, or imported from elsewhere. Editing it keeps that value as the selected
+ * one, and the select stays creatable: pick a province from the list, or type
+ * another custom value and keep it.
+ */
+export const CustomState: StoryFn = () => {
+ return (
+
+ {
+ console.log("updated address", updatedAddress)
+ }}
+ />
+
+ )
+}
+
export const HookedModal: StoryFn = () => {
const [address, setAddress] = useState(presetAddresses.withName)