tl;dr
- React Final Form には、format機能があります。
- フィールドの表示内容に対して、全角英数→半角英数 のような変換処理が行えます。
- しかし、format機能を使うと、変換処理が走るたびにIMEが閉じてしまうので、IMEが使い物になりません。
- そこで、
formatOnBlur
prop を指定しましょう。- blur (つまりフォーカスが外れた)の瞬間にしか format が走らないようになり、
- 変換処理のたびにIMEが閉じてしまうのを抑制できます。
- しかし、そうすると、フォームをリセットした時にフィールドの状態がおかしくなります。
- → 結論:
value={input.value || ""}
で 強制的に空文字列で上書きしてしまいましょう。
環境・コード
以下のライブラリを使用しています。
ライブラリ | ver |
---|---|
Next.js | 10.1.3 |
React.js | 16.13.0 |
MUI | 5.3.0 |
React Final Form | 6.5.3 |
Final Form | 4.20.2 |
TypeScript | 4.0 |
この記事では、簡単のため、Next.js, TypeScript, MUI を用いています
ソースコード全文を先に置いておきます。
(Container
, Stack
はレイアウト用のコンポーネントなので気にしないで下さい。)
import { NextPage } from "next";
import { Button, Container, Stack, TextField } from "@mui/material";
import { Field, Form, FormSpy } from "react-final-form";
// 住所に含まれる全角数字と長音記号を、半角数字・半角マイナス記号に変換
const normalizeAddress = (s: string | undefined): string => {
// NOTE: (1) undefined -> "" (空文字列) のデフォルトの変換を再現
if (!s) return "";
// https://www.yoheim.net/blog.php?q=20191101
return s
.replace(/[−]/g, "-")
.replace(/[0-9]/g, (s) => String.fromCharCode(s.charCodeAt(0) - 0xfee0));
};
const ReactFinalFormFormatPage: NextPage = () => {
return (
<Container>
<Form onSubmit={() => {}}>
{({ handleSubmit, form }) => (
<form onSubmit={handleSubmit}>
<Stack py={4} spacing={2}>
{/* NOTE: (2) IMEとの兼ね合いでformatOnBlurを使ってる */}
<Field name="address" format={normalizeAddress} formatOnBlur>
{({ input }) => (
<TextField
{...input}
// NOTE: (3) formatOnBlurを使っているので、実はここでも再び undefined を弾かないといけない
value={input.value || ""}
label="address"
/>
)}
</Field>
<Stack direction="row">
<Button variant="contained" onClick={() => form.restart()}>
<code>form.restart()</code>
</Button>
</Stack>
{/* フォームの値をJSON文字列の形で表示 */}
<FormSpy>
{({ values }) => (
<pre>
<code>{JSON.stringify(values, null, 2)}</code>
</pre>
)}
</FormSpy>
</Stack>
</form>
)}
</Form>
</Container>
);
};
export default ReactFinalFormFormatPage;
React Final Form での空欄の扱い
まず、React Final Form が空のフィールドを保持しているとき、
- Form は内部的に
undefined
として格納し、 - Field コンポーネントがこれを空文字列
""
に変換したうえでinput要素 (ここではTextField
を使用)に渡します。
React Final Form は Controlled Component として各要素を管理しますが、value が undefined
になってしまうと、 Uncontrolled Component になってしまい、React Final Form による value の制御から離れてしまうため、と考えられます。
format
prop
format
prop には、(Form内での値) => (Fieldに渡すvalue)
の変換関数を渡すことが出来ますが、(s: string | undefind) => string
の関数を渡すことで、文字列フォーマットのみが行われます。(数値への変換などは行わない)
ただし、format
を指定したときには、デフォルトで行われていた undefined -> ""
の変換が自動的に行われなくなる 1 ため、変換関数の中で明示的に if (!s) return "";
と書いています。[NOTE (1)]
// 住所に含まれる全角数字と長音記号を、半角数字・半角マイナス記号に変換
const normalizeAddress = (s: string | undefined): string => {
// NOTE: (1) undefined -> "" (空文字列) のデフォルトの変換を再現
if (!s) return "";
// https://www.yoheim.net/blog.php?q=20191101
return s
.replace(/[−]/g, "-")
.replace(/[0-9]/g, (s) => String.fromCharCode(s.charCodeAt(0) - 0xfee0));
};
<Field
format={normalizeAddress}
>...
</Field>
formatOnBlur
prop
しかし、そのままでは問題があります。
IMEで入力している場合には、文字列の変換処理が走るたびにIMEが中断されてしまうのです。
(関数の入力と出力が同じであれば中断されませんが、谷町「4(全角)」 → 谷町「4(半角)」
のように、差分が生じた瞬間に中断されます)
そこで、formatOnBlur
propに true
を渡すことで、上記の変換を行うタイミングを「フィールドからフォーカスが外れた瞬間」のみに限定することが出来ます。
IMEを邪魔しないためには、このpropを設定する必要があります。[NOTE (2)]
formatOnBlur がある時 |
formatOnBlur がない時 |
---|---|
<Field
format={normalizeAddress}
+ formatOnBlur
>...
</Field>
formatOnBlur
による問題点
この「変換を行うタイミングを限定する」動作が裏目に出てしまう瞬間があります。
それはフォームがリセットされた瞬間です。
フォームがリセットされて、該当するフィールドが undefined
になった瞬間、formatの変換処理が実行されないので、Controlled Component にその undefined
がそのまま渡されてしまいます。
ここに意図せぬ Uncontrolled Component が誕生しました。
内部的には undefined
にリセットされているのに、<TextField>
ではリセットする前の文字列が表示されたままになってしまいます。
そのためには、format
の機能と別の方向で undefined -> ""
の変換をしなければなりません。
そのために、<TextField>
に渡す value
を上書きして、明示的に ""
でフォールバックした訳です。2 [NOTE (3)]
<Field name="address" format={normalizeAddress} formatOnBlur>
{({ input }) => (
<TextField
{...input}
+ value={input.value || ""}
label="address"
/>
)}
</Field>
参考記事
-
ちょっと違うけど、FieldProps#format のドキュメントより引用(太字強調は筆者による) ―― Note: If you would like to disable the default behavior of converting undefined to '', you can pass an identity function, v => v, to format. If you do this, making sure your inputs are "controlled" is up to you. ↩
-
initialValues
propあるいはreset(initialValues)
,restart(initialValues)
などの引数に明示的に""
を渡す方法もありますが、フォームが管理する値(ビジネスロジックに直結)とフィールドの状態管理を分離できたほうが気が楽だと思ったので、それらの方法は取りませんでした。 ↩