バックエンドエンジニアとして、普段触ることのないフロントエンド。
とはいえ苦手意識をいつか克服したいと思い、VueやNuxtの勉強を始めていた矢先、業務でVueの修正を行う機会に恵まれました。
その際にぶつかった問題について、同僚のフロントエンドエンジニアの方に助けていただいたのでその内容を整理したいと思います。
ぶつかった問題
サブミット前に、あるフォームの値に応じて他のフォームに自動的に値をセットする、というような処理を書いていた時に、コンソールでは値が入っているように見えるのに送信先では受け取れない、といった問題が発生しました。
シナリオ
今回は「①好きな動物の名前と②飼っている動物の名前を送信してもらうためのフォームを作っている」という事にしましょう。
<body id="app" class="pl-10 pt-10 w-3/5">
<form @submit.prevent="submitForm" id="targetForm">
<div class="mb-10 flex justify-between">
<label
for="favorite-animal"
@click="console"
class="p-4 rounded-lg bg-purple-300 dark:bg-purple-800 dark:text-purple-400 w-1/2"
>好きな動物は?</label
>
<select
name="favorite-animal"
v-model="nameOfYourFavoriteAnimal"
class="rounded border-black border-2 w-1/3"
>
<option value="パンダ">パンダ</option>
<option value="うさぎ">うさぎ</option>
<option value="コアラ">コアラ</option>
</select>
</div>
<div class="w-50 mb-10 flex justify-between">
<label
for="favorite-animal"
class="p-4 rounded-lg bg-purple-300 dark:bg-purple-800 dark:text-purple-400 w-1/2"
>あなたが飼っている動物は?</label
>
<select
name="animal-you-have-is"
v-model="animalYouHaveIsSameAsYourFavorite"
class="rounded border-black border-2 w-1/3"
>
<option value="same-as-favorite">好きな動物と同じ</option>
<option value="different-animal">違う動物</option>
</select>
</div>
<div
v-show="animalYouHaveIsDifferent"
class="mt-5 mb-10 flex justify-between"
>
<p
class="p-4 rounded-lg bg-purple-300 dark:bg-purple-800 dark:text-purple-400 w-1/2"
>
あなたが飼っている動物の名前を入力してください。
</p>
<input
id="name-of-animal-you-have"
type="text"
name="animal-name"
v-model="nameOfAnimalYouHave"
class="rounded border-black border-2 w-1/3"
/>
</div>
<div class="w-50 mb-10 flex justify-center">
<input type="submit" class="rounded-lg bg-red-200 w-4/5" />
</div>
</form>
</body>
パターン1 : 好きな動物と飼っている動物が違う場合
「好きな動物は?」と「あなたが飼っている動物の名前を入力してください」の二つのフォームの値をそのまま使用すればよさそうですね。
パターン2 : 好きな動物と飼っている動物が同じ場合
「好きな動物は?」で答えた動物とおなじ動物を飼っていると答えた場合は、「あなたが飼っている動物の名前を入力してください」のフォームは表示せず、代わりに「好きな動物は?」の値をコピーしてフォームを送信してあげることにしましょう。
私がやっていたこと
フォームを送信したときに直前で呼ばれるsubmitForm()
メソッドの中で、好きな動物と飼っている動物が同じなら「nameOfAnimalYouHave (飼っている動物の名前)
」に好きな動物の名前を入れる処理を追加しました。
直後にnameOfAnimalYouHave (飼っている動物の名前)
をコンソールに出力してみると、きちんと「うさぎ」と出力されます。
const app = Vue.createApp({
data() {
return {
nameOfYourFavoriteAnimal: "",
animalYouHaveIsSameAsYourFavorite: "",
nameOfAnimalYouHave: "",
};
},
computed: {
animalYouHaveIsDifferent() {
return this.animalYouHaveIsSameAsYourFavorite === "different-animal";
},
},
methods: {
submitForm() {
// 好きな動物と飼っている動物が同じなら
if (!this.animalYouHaveIsDifferent) {
// 「飼っている動物の名前」に好きな動物の名前を入れる
this.nameOfAnimalYouHave = this.nameOfYourFavoriteAnimal;
}
// きちんとセットされていることを確認してみる
console.log(this.nameOfAnimalYouHave); // うさぎ
document.getElementById("targetForm").submit();
},
},
});
app.mount("#app");
これで完成。
と思いきや送信先で値が取れていない・・・?
何故・・・
同僚からの助言
そこでフロントエンドエンジニアの同僚に相談すると下記のようにアドバイスをいただきました。
Vueインスタンスの状態が変わっただけで、DOMに反映されていないかもしれない。
nextTick()を使うといいよ。
nextTick()
とは・・・?
nextTick()
nextTickについてはVueの公式ドキュメントにサンプルコード付きの詳しい解説がありました。
Vue でリアクティブな状態を変更したとき、その結果の DOM 更新は同期的に適用されません。
(中略)
状態を変更した直後に nextTick() を使用すると、DOM 更新が完了するのを待つことができます。
そういえばVue入門でもそんな話あったなぁ・・・(遠い目)
当然といえば当然でフォームのDOM要素をとってきて送信しているのですから、DOMに値がバウンドされないうちにフォームを送信してしまっていたのでvalueは空のままだったようです。
修正内容
nextTickを使用してお目当ての項目の値が更新されるようにした修正結果がこちらです。
無事送信先で値をとることができました。
methods: {
async submitForm() {
if (!this.animalYouHaveIsDifferent) {
this.nameOfAnimalYouHave = this.nameOfYourFavoriteAnimal;
}
// vueのリアクティブな値はきちんとセットされているが
console.log(this.nameOfAnimalYouHave); // うさぎ
// フォームの値は空
console.log(document.getElementById("name-of-animal-you-have").value);
await this.$nextTick();
// ここではうさぎに更新されている
console.log(document.getElementById("name-of-animal-you-have").value);
document.getElementById("targetForm").submit();
},
},
まとめ
フロントエンドは触りなれていないとついついDOMとJavascriptの状態の違いを忘れがちですが、大事なところなのでしっかり押さえておこうと反省。
参考文献