tag:crieit.net,2005:https://crieit.net/tags/form/feed 「form」の記事 - Crieit Crieitでタグ「form」に投稿された最近の記事 2021-11-29T20:59:12+09:00 https://crieit.net/tags/form/feed tag:crieit.net,2005:PublicArticle/17795 2021-11-29T20:59:12+09:00 2021-11-29T20:59:12+09:00 https://crieit.net/posts/input-checkbox-double-bracket-post-failed-20211129 input type="checkbox" の二重ブラケットについて <p>WordPress の自作プラグインで挙動不審なところが見付かり、そういえば……と思って input type="checkbox" の二重ブラケットについて調べてみました。</p> <p>調査した結果としては、自作プラグインのコードの書き方が悪かったので修正しました。</p> <h2 id="経緯・現象"><a href="#%E7%B5%8C%E7%B7%AF%E3%83%BB%E7%8F%BE%E8%B1%A1">経緯・現象</a></h2> <p>WordPress の自作プラグインでカスタムタクソノミーをチェックボックスで選択できる設定画面を作ってあったのですが、そこにチェックを入れてもデータが保存されない現象に遭遇。</p> <h3 id="コード"><a href="#%E3%82%B3%E3%83%BC%E3%83%89">コード</a></h3> <pre><code class="php"><?php require_once( ABSPATH . '/wp-admin/includes/template.php' ); class MyCategoryChecklistWalker extends \Walker_Category_Checklist { /** * var */ protected $c; protected $hidden_ids; /** * コンストラクタ */ function __construct( $c, $arrayTaxonomies ) { $this->c = $c; $this->hidden_ids = $arrayTaxonomies['id']; } /** * Start the element output. * * @see Walker::start_el() * * @since 2.5.1 * * @param string $output Used to append additional content (passed by reference). * @param object $category The current term object. * @param int $depth Depth of the term in reference to parents. Default 0. * @param array $args An array of arguments. @see wp_terms_checklist() * @param int $id ID of the current term. */ function start_el( &$output, $category, $depth = 0, $args = Array(), $id = 0 ) { extract( $args ); if( empty( $taxonomy )) { $taxonomy = 'category'; } if( $taxonomy == 'category' ) { $name = 'post_category'; } else { $name = 'tax_input[' . $taxonomy . ']'; } $class = in_array( $category->term_id, $popular_cats ) ? ' class="popular-category"' : ''; // 処理 $id = esc_attr( $name ) . '___term---' . esc_attr( $category->term_id ); $nameAttr = "name=\"{$i( 'myplugin_checkboxes' )}[{$id}]\""; // 処理 } } </code></pre> <p>設定画面は WordPress の Walker_Category_Checklist を継承したクラスで、チェックボックスの設定画面を作成していました。</p> <p>特に今回気になったのは次の部分。</p> <pre><code class="php"> else { $name = 'tax_input[' . $taxonomy . ']'; } </code></pre> <p>もう一つ。</p> <pre><code class="php"> $id = esc_attr( $name ) . '___term---' . esc_attr( $category->term_id ); $nameAttr = "name=\"{$i( 'myplugin_checkboxes' )}[{$id}]\""; </code></pre> <p>デフォルトのカテゴリーならば <code>post_category</code> という文字列を付与していますが、カスタムタクソノミーの場合は <code>tax_input[TAXONOMY_NAME]</code> とブラケットを使ってオブジェクトの記述にしていました。これをチェックボックスの <code>name</code> 属性に使用しています。その際に、複数選択可能なチェックボックスなのでさらに外側にブラケットが付くようにしていました。</p> <pre><code>name="myplugin_checkboxes[tax_input[TAXONOMY_NAME]___term---TERMID]" </code></pre> <p>こんな感じですね。ところが、このチェックボックスにチェックを入れても反映されない、となったわけです。</p> <p>いかにも二重にブラケットで括っているのが挙動不審の原因になっていそうな気がしたので、調査しました。</p> <h2 id="調査"><a href="#%E8%AA%BF%E6%9F%BB">調査</a></h2> <pre><code>a:XX:{s:26:"post_category___term---154";i:1;s:26:"post_category___term---165";i:1;s:26: /* 略 */ i:1;s:32:"tax_input[TAXONOMY_NAME";i:1;} </code></pre> <p>データベースで該当するキーを確認すると……やはり、二重ブラケットになった値を入れるところでシリアライズされたデータのフォーマットが崩れています。</p> <h2 id="検証"><a href="#%E6%A4%9C%E8%A8%BC">検証</a></h2> <p>そこで簡単なコードを作成。</p> <pre><code class="html"><!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>input</title> </head> <body> <form action="./test.php" method="post"> <label for="input"> <input type="checkbox" name="input[hoge[fuga]]" id="input" value="1"> インプット </label> <button type="submit">送信</button> </form> </body> </html> </code></pre> <p>二重ブラケットのチェックボックスを用意して POST するだけの簡単なフォームです。</p> <pre><code class="php"><!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>result</title> </head> <body> <pre><code> <?php var_dump($_SERVER); ?> </code></pre> <pre><code> <?php var_dump($_REQUEST); ?> </code></pre> </body> </html> </code></pre> <p>テストなのでエスケープとか一切していないですが、これで出力を確認。</p> <pre><code>array (size=1) 'input' => array (size=1) 'hoge[fuga' => string '1' (length=1) </code></pre> <p>思った通り、二重ブラケットのキーが途中で壊れてしまっています。 DB の中でシリアライズが崩れたデータと同じ状態です。</p> <p>これで二重ブラケットがNGであることが分かったのでコードを修正します。</p> <h2 id="修正"><a href="#%E4%BF%AE%E6%AD%A3">修正</a></h2> <pre><code class="php"> else { // $name = 'tax_input[' . $taxonomy . ']'; $name = $taxonomy; } </code></pre> <p>今回は独自のパース処理でタームのチェックを判断しているので、 <code>tax_input</code> の配列名は不要でした。そこで、タクソノミー名をそのまま <code>$name</code> に渡すように修正を加えました。</p> <pre><code class="php"> $id = esc_attr( $name ) . '___term---' . esc_attr( $category->term_id ); $nameAttr = "name=\"{$i( 'myplugin_checkboxes' )}[{$id}]\""; </code></pre> <p>それを使ってチェックボックスを出力する部分や他の部分は一切触らず。これで動作OKを確認しました。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://developer.wordpress.org/reference/classes/walker_category_checklist/">Walker_Category_Checklist | Class | WordPress Developer Resources</a></li> </ul> arm-band tag:crieit.net,2005:PublicArticle/16725 2021-03-09T01:32:10+09:00 2021-03-09T01:32:10+09:00 https://crieit.net/posts/React-Hook-Form-Getform-io React Hook FormとGetform.ioを使って、お問い合わせフォームを作ろう! <p><a target="_blank" rel="nofollow noopener" href="https://react-hook-form.com/jp/">React Hook Form</a>が便利らしいと聞いたので使ってみることにしました。</p> <h2 id="React Hook Form"><a href="#React+Hook+Form">React Hook Form</a></h2> <p>皆さん、<a target="_blank" rel="nofollow noopener" href="https://react-hook-form.com/jp/">React Hook Form</a>を知ってますか?</p> <p>最近トレンドに乗っかってきた、<strong>Form</strong>を<strong>React Hooks</strong>で簡単に作ることのできる代物です。</p> <p><img src="https://i.imgur.com/2dqEW7L.png" alt="img" /></p> <p>特徴として、Hooksを使って簡単にFormが作れる、そして再レンダリングが最小限に抑えられているのでパフォーマンスも高い、らしいです。</p> <h2 id="Getform.io"><a href="#Getform.io">Getform.io</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://getform.io/">Getform.io</a>はフォームのバックエンドを提供するすばらしいサービスです。</p> <p>詳しくは<a target="_blank" rel="nofollow noopener" href="https://blog.tubone-project24.xyz/2021/02/13/netlify-github-action#getformio">こちらの過去記事</a>をご確認いただければと思います。</p> <h2 id="React Hook Form + Getform.io"><a href="#React+Hook+Form+%2B+Getform.io">React Hook Form + Getform.io</a></h2> <p><strong>合体!</strong></p> <p>だめ~となるかと思いましたがうまいことできました。</p> <p><img src="https://i.imgur.com/yYJBK98.jpg" alt="img" /></p> <p>今回はこちらの2技術を使って、お問い合わせフォームを作っていきます。</p> <h2 id="実コード"><a href="#%E5%AE%9F%E3%82%B3%E3%83%BC%E3%83%89">実コード</a></h2> <p>こんな感じのコンポーネントができました。</p> <pre><code class="typescript">import React, {useState} from "react"; import { useForm } from "react-hook-form"; import Button from "./button"; type Inputs = { name: string, email: string, subject: string, message: string, }; const ContactForm = (): JSX.Element => { const [serverState, setServerState] = useState({ submitting: false, status: {ok: false, msg: ""} }); const { register, handleSubmit, errors } = useForm<Inputs>(); const handleServerResponse = (ok: boolean, msg: string) => { setServerState({ submitting: true, status: { ok, msg } }); }; const onSubmit = (data: Inputs, e: any) => { const formData = new FormData(); formData.append("name", data.name) formData.append("email", data.email) formData.append("subject", data.subject) formData.append("message", data.message) fetch('https://getform.io/f/8xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', { method: 'POST', body: formData }) .then(() => { e.target.reset(); handleServerResponse(true, "Submitted!"); }) .catch((error) => { alert(error) console.error(error) handleServerResponse(false, error.toString()); }); } return ( <form onSubmit={handleSubmit(onSubmit)}> <p> <label>Your Name<br/> <input name="name" placeholder="Enter your name" type="text" ref={register({ required: true })} /> {errors.name && <span>This field is required</span>} </label> </p> <p> <label> Your email<br/> <input name="email" type="email" placeholder="Enter your email" ref={register({ pattern: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/i, required: true })} /> {errors.email && <span>This field is required and only email format</span>} </label> </p> <p> <label> Subject<br/> <input name="subject" type="text" maxLength={30} placeholder="Subject here..." ref={register({required: true })} /> {errors.subject && <span>This field is required</span>} </label> </p> <p> <label> Message<br /> <textarea name="message" placeholder="Something writing..." rows={6} cols={25} ref={register({required: true })}/> {errors.message && <span>This field is required</span>} </label> </p> <Button dark={serverState.submitting && serverState.status.ok} disabled={serverState.submitting && serverState.status.ok}> { serverState.submitting && serverState.status.ok ? serverState.status.msg: 'Submit'} </Button> </form> ); } export default ContactForm </code></pre> <h2 id="解説"><a href="#%E8%A7%A3%E8%AA%AC">解説</a></h2> <h3 id="準備"><a href="#%E6%BA%96%E5%82%99">準備</a></h3> <p>まず、フォームの項目に該当するTypeを作ります。</p> <pre><code class="typescript">type Inputs = { name: string, email: string, subject: string, message: string, }; </code></pre> <p>今回は名前、email、題名、メッセージを設定します。</p> <p>次に<strong>React Hook Form</strong>のuseFormを使って<strong>register</strong>などを作っていきます。正直これができれば基本的な機能は8割くらい完成です。</p> <pre><code class="typescript">const { register, handleSubmit, errors } = useForm<Inputs>(); </code></pre> <p>とりあえず用意するのは、formのrefに設定する<strong>register</strong>、onSubmitをコントロールできる<strong>handleSubmit</strong>、requireを検査できる<strong>errors</strong>です。</p> <p>ほかにも、form全体の項目検査のformState.isValidなども使うことができます。</p> <h3 id="Submit"><a href="#Submit">Submit</a></h3> <p>そして肝心な送信(Submit)部分ですがこちらは<a target="_blank" rel="nofollow noopener" href="https://blog.tubone-project24.xyz/2021/02/13/netlify-github-action#getformio">前記事</a>とほぼ同じように<strong>onSubmit</strong>に合わせて処理する関数を用意して、formの<strong>onsubmit属性</strong>に渡してあげればいいだけです。</p> <p>........いいだけですが一つ注意として、渡す際に<strong>handleSubmit</strong>で関数をラップしないと、form情報がうまく取れない、ということです。忘れずに設定してくださいませ。</p> <pre><code class="typescript"> <form onSubmit={handleSubmit(onSubmit)}> </form> </code></pre> <p>またGetform.ioへのPOSTはJSONではなく<strong>mulitpart/form-data</strong>で渡さないと行けないので、FormDataにappendする形でFormのデータを差し込みます。</p> <pre><code class="typescript"> const onSubmit = (data: Inputs, e: any) => { const formData = new FormData(); formData.append("name", data.name) formData.append("email", data.email) formData.append("subject", data.subject) formData.append("message", data.message) fetch('https://getform.io/f/8xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', { method: 'POST', body: formData }) .then(() => { e.target.reset(); handleServerResponse(true, "Submitted!"); }) .catch((error) => { alert(error) console.error(error) handleServerResponse(false, error.toString()); }); } </code></pre> <p>また、第二引数として、formのeventが取得できます。おそらくEventは<strong>React.BaseSyntheticEvent</strong>だとは思うのですがうまく型が通せなくて悩みだしてしまいましたのでとりあえずanyにしてしまいました。</p> <p>一応、<a target="_blank" rel="nofollow noopener" href="https://github.com/react-hook-form/react-hook-form/discussions/4376">こちら</a>で質問は投げてますが英語がへたくそで誰も答えてくれそうにありませんね。</p> <p>特にeventでデータを取る必要はなさそうですが、たとえば送信時にFormの内容をリセットするなどの処理を書きたいときは</p> <pre><code class="typescript"> e.target.reset(); handleServerResponse(true, "Submitted!"); </code></pre> <p>とやってあげればOKです。</p> <h3 id="Formを書く"><a href="#Form%E3%82%92%E6%9B%B8%E3%81%8F">Formを書く</a></h3> <p>さて、あとは普通のFormを作るようにJSXを書いていきます。</p> <p>唯一違うところはinputやtextareaのref属性にregisterをつけなければいけないですが、それだけで大丈夫です。</p> <pre><code class="typescript"> <textarea name="message" placeholder="Something writing..." rows={6} cols={25} ref={register({required: true })}/> </code></pre> <p>ちなみに、registerのパラメーターで、必須項目やパターンの検査もできます。</p> <pre><code class="typescript"> <input name="email" type="email" placeholder="Enter your email" ref={register({ pattern: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/i, required: true })} /> </code></pre> <p>また、検査が通っていないときに警告メッセージを出すのはerrorsをつかうことで実現できます。</p> <pre><code class="typescript"> {errors.subject && <span>This field is required</span>} </code></pre> <p>Form部分をすべて実装するとこんな感じです。</p> <pre><code class="typescript"> <form onSubmit={handleSubmit(onSubmit)}> <p> <label>Your Name<br/> <input name="name" placeholder="Enter your name" type="text" ref={register({ required: true })} /> {errors.name && <span>This field is required</span>} </label> </p> <p> <label> Your email<br/> <input name="email" type="email" placeholder="Enter your email" ref={register({ pattern: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/i, required: true })} /> {errors.email && <span>This field is required and only email format</span>} </label> </p> <p> <label> Subject<br/> <input name="subject" type="text" maxLength={30} placeholder="Subject here..." ref={register({required: true })} /> {errors.subject && <span>This field is required</span>} </label> </p> <p> <label> Message<br /> <textarea name="message" placeholder="Something writing..." rows={6} cols={25} ref={register({required: true })}/> {errors.message && <span>This field is required</span>} </label> </p> <Button dark={serverState.submitting && serverState.status.ok} disabled={serverState.submitting && serverState.status.ok}> { serverState.submitting && serverState.status.ok ? serverState.status.msg: 'Submit'} </Button> </form> </code></pre> <p>もう完成です。実に簡単ですね。</p> <p>React Hook Formを使わないと、<a target="_blank" rel="nofollow noopener" href="https://blog.tubone-project24.xyz/2021/02/13/netlify-github-action#getformio">前記事</a>のように、formのonChangeのたびに、setStateしなきゃいけないのですが、すっきり実装できました。</p> <p>React Hook Formを使わないと</p> <pre><code class="typescript"> handleChange(e) { this.setState({ [e.target.name]: e.target.value }); } (中略) <input type="text" name="name" className="form-control" maxLength="30" minLength="2" required placeholder="Enter your name" onChange={this.handleChange} /> </code></pre> <p>となります。</p> <p>出来上がりはただのFormですのでかっこいいCSSを当ててくださいね。</p> <p><img src="https://i.imgur.com/DsrFLOE.png" alt="img" /></p> <h2 id="結論"><a href="#%E7%B5%90%E8%AB%96">結論</a></h2> <p>楽に実装できたので余った時間は担当<strong>ウマ娘</strong>に捧げます。</p> tubone24