🍣
[Next.js13]郵便番号を入力したとき、対応する住所が複数あったらダイアログを表示してユーザーに選択させる
やること
↓これ
技術スタック
- Next.js13.4
- AppRouter
- ReactHookForm ver7.44.2
- zod ver3.21.4
- jotai ver2.1.0
ロジック
以下イメージです。
アドレスフォーム
- 郵便番号を入力する
- 7文字に達する
- APIを叩いて住所リストを取得する
- 使用したAPIは→
https://zipcloud.ibsnet.co.jp/api/search
- 使用したAPIは→
- 候補が1つならフォームにセット
- 終了
- 候補が2つ以上ならダイアログを表示
- ユーザーが候補から1つを選ぶ
- 候補をフォームにセット
- 終了
next-template/index.tsx at develop · ampersand-github/next-template · GitHub
"use client";
export const AddressForm = () => {
// ダイアログの制御
const [_, setIsOpen] = useAtom(isAddressSelectDialogOpenAtom);
// 住所の候補リスト(ダイアログに表示するため)
const [candidate, setCandidate] = useAtom(candidateAddressAtom);
// ダイアログからユーザーが選択した住所
const [selectedAddress] = useAtom(selectedAddressAtom);
const [isLoading, setIsLoading] = React.useState(false);
// formの定義
const form = useForm<z.infer<typeof addressFormSchema>>({
resolver: zodResolver(addressFormSchema),
defaultValues: initialAddress,
});
// ユーザーが住所を選択したとき、選択した値をフォームにセットする
useEffect(() => {
form.setValue("prefecture", selectedAddress?.prefecture || "");
form.setValue("city", selectedAddress?.city || "");
form.setValue("town", selectedAddress?.town || "");
}, [form, selectedAddress]);
}
// 郵便番号から住所を取得する
// 郵便番号と住所が1:1の場合、取得した住所をフォームにセットする
// 郵便番号が1:Nの場合、ダイアログを開いてユーザーに住所を選択させる
const autofillFromZipcode = async (postalCode: string) => {
setIsLoading(true);
const results = await fetchCandidateAddress(postalCode);
if (results.length === 1) {
form.setValue("prefecture", results[0].prefecture);
form.setValue("city", results[0].city);
form.setValue("town", results[0].town);
}
//
if (results.length >= 2) {
setCandidate([results[0], ...results.slice(1)]);
setIsOpen(true);
// ここでダイアログが開く
// 選択すると自動でフォームに入力される
}
setIsLoading(false);
};
// 送信
const onSubmit = (values: z.infer<typeof addressFormSchema>) => {
console.log(values);
toast({ description: "送信しました", variant: "success" });
};
return (
<Form {...form}>
{isLoading ? (
<div className="flex justify-center">
<Loader2 className="r-2 h-8 w-8 animate-spin " />
</div>
) : (
<>
<AddressSelectDialog items={candidate} />
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="postalCode"
render={({ field }) => (
<FormItem>
<FormLabel>郵便番号</FormLabel>
<FormControl>
<Input
type="text"
autoComplete="postal-code"
inputMode="numeric"
placeholder="1234567"
maxLength={7}
{...field}
value={field.value || ""}
onChange={async (
e: React.ChangeEvent<HTMLInputElement>
) => {
if (isNumeric(e.target.value)) {
// 数値のみ反映させる
field.onChange(e.target.value);
// 7字になったら郵便番号を検索する
if (e.target.value.length === 7)
await autofillFromZipcode(e.target.value);
}
}}
/>
</FormControl>
<FormDescription>郵便番号の入力は必須です</FormDescription>
<FormMessage />
</FormItem>
)}
/>
// 都道府県(略)
// 市区町村(略)
// 町名以下(略)
// 建物名・階層・部屋情報(略)
<Button type="submit">Submit</Button>
</form>
</>
)}
</Form>
);
住所選択ダイアログ
- 引数で住所リストを受け取ってラジオボックス形式で表示
- 選択したらJotaiで値を保存する
useAtom(selectedAddressAtom)
next-template/index.tsx at develop · ampersand-github/next-template · GitHub
"use client";
type props = {
items: [IAddress, ...IAddress[]];
};
export const AddressSelectDialog = ({ items }: props) => {
const [isOpen, setIsOpen] = useAtom(isAddressSelectDialogOpenAtom);
const [_, setSelected] = useAtom(selectedAddressAtom);
const handleChange = async (data: any) => {
console.log(data);
setSelected(items[Number(data)]);
setIsOpen(false);
};
return (
<Dialog modal={true} open={isOpen} onOpenChange={() => setIsOpen(!isOpen)}>
<DialogContent className="mt-8 max-h-[calc(100%_-_64px)] max-w-[calc(100%_-_64px)] overflow-y-auto">
<DialogHeader>
<DialogTitle>住所選択</DialogTitle>
<DialogDescription>
以下のリストから住所を選択してください。選択した住所は変更できます。
</DialogDescription>
</DialogHeader>
<RadioGroup
onValueChange={handleChange}
className="flex flex-col space-y-1"
>
{items.map((item, index) => {
const value = `${item.prefecture} ${item.city} ${item.town}`;
return (
<div className="flex items-center space-x-2" key={index}>
<RadioGroupItem value={String(index)} id={String(index)} />
<Label htmlFor={String(index)}>{value}</Label>
</div>
);
})}
</RadioGroup>
</DialogContent>
</Dialog>
);
};
ポイント
フォームと住所選択ダイアログを分離しています。
状態管理を色々試していましたが、なんやかんやでjotaiに落ち着きました。
AppRouterで使用できる状態管理ツールかつ、軽いもので選定しました。
参考:【jotai】App Routerでの状態管理
(なぜjotaiなら動くのかわかってないので誰か教えてください)
Discussion