LivewireコンポーネントにWeb Speech APIを用いて音声入力機能を実装していたところ、ちょっとした表示の不具合に遭遇しました。具体的には、音声認識は正常に動作しているのに、マイクボタンのアイコンやスタイルが意図せず元に戻ってしまうという現象です。
直面した問題:音声認識はオンなのにボタンはオフ表示
実装していたのは、テキスト入力欄の横にあるマイクボタンです。
- マイクボタンをクリックするとWeb Speech APIで音声認識が開始。
- 音声認識中は、ボタンのアイコンが「マイクオン(
mic
)」に変わり、色もアクティブな状態(例:text-primary
クラスが付与)になる。 - 音声認識を停止すると、アイコンは「マイクオフ(
mic_off
)」に戻り、アクティブなスタイルも解除される。
ここまでは期待通りの動作です。しかし、Livewireコンポーネントが何らかの理由で再レンダリングされると、音声認識は継続しているにも関わらず、マイクボタンの見た目だけが「オフ」の状態に戻ってしまうのです。
試しに、Livewireを使わない素のHTML、CSS、JavaScriptで同じ機能を実装してみると、この問題は発生しませんでした。どうやらLivewireの挙動が関係していそうです。
原因の推測:LivewireのDOM再レンダリング
Livewireは、コンポーネントのPHPプロパティが変更されたり、イベントを受信したりすると、関連するBladeテンプレートの部分を効率的に再レンダリングします。この再レンダリングの際、JavaScriptによって直接変更されたDOMの属性(クラスやテキストコンテンツなど)が、Livewireのテンプレートに基づいた初期状態に上書きされてしまうことがあります。
今回のケースでは、JavaScriptでマイクボタンのアイコン(<span>
タグのテキストコンテント)やCSSクラスを変更していましたが、Livewireの再レンダリングによって、これらの変更がテンプレートの初期状態(アイコンが mic_off
で、text-primary
クラスなし)に戻されていたと考えられます。
解決策:wire:ignore
でLivewireの更新対象から除外
wire:ignore
をDOM要素に指定すると、Livewireはその要素(およびデフォルトではその子要素も)の更新をスキップします。これにより、JavaScriptによるDOM操作の結果を保護することができます。
具体的なコード修正
1. Bladeテンプレートの修正 (message-input.blade.php
)
音声認識のオン・オフを切り替えるボタン (id="toggle-speech-input"
) に wire:ignore
を追加します。
{{-- message-input.blade.php --}}
{{-- 修正前 --}}
{{--
<button type="button" id="toggle-speech-input" class="p-1 text-gray-400 hover:text-gray-600 rounded-full hover:bg-gray-100">
<span class="material-icons text-sm">mic_off</span>
</button>
--}}
{{-- 修正後 --}}
<button type="button" id="toggle-speech-input" wire:ignore class="p-1 text-gray-400 hover:text-gray-600 rounded-full hover:bg-gray-100">
<span class="material-icons text-sm">mic_off</span> {{-- 初期アイコンは mic_off --}}
</button>
ポイント:
-
wire:ignore
をボタン要素に適用しました。これにより、Livewireはこの<button>
要素とその内部の<span>
要素の更新を行わなくなります。 - 結果として、JavaScriptによるアイコンの変更(
textContent
)や色の変更(classList.add/remove
)が、Livewireの再レンダリングによって上書きされるのを防ぐことができます。
2. JavaScript側の処理 (変更なし)
JavaScript側では、引き続き id="toggle-speech-input"
を持つ要素とその子要素(アイコンの <span>
)を操作してスタイルを変更します。wire:ignore
を使用しても、JavaScriptからのDOM操作は通常通り機能します。
// message-input.blade.php の @script 内
// ...
const toggleSpeechInput = document.getElementById('toggle-speech-input');
// ...
recognition.onstart = function() {
state.isSpeechRecognitionActive = true;
// ボタンのアイコンとスタイルを「オン」状態に変更
toggleSpeechInput.querySelector('.material-icons').textContent = 'mic';
toggleSpeechInput.classList.add('text-primary');
speechStatus.classList.remove('hidden');
speechStatusText.textContent = '音声認識中...';
};
// recognition.onend や recognition.onerror で呼び出される stopSpeechRecognition 内で
// ボタンのアイコンとスタイルを「オフ」状態に戻す処理
function stopSpeechRecognition() {
if (state.speechRecognition) {
state.isSpeechRecognitionActive = false; // 状態を更新
// ボタンのアイコンとスタイルを「オフ」状態に戻す
toggleSpeechInput.querySelector('.material-icons').textContent = 'mic_off';
toggleSpeechInput.classList.remove('text-primary');
speechStatus.classList.add('hidden');
try {
state.speechRecognition.stop();
} catch (error) {
console.error('音声認識の停止に失敗しました:', error);
}
}
}
recognition.onend = function() {
// stopSpeechRecognition() 内でUIは更新済みのはずだが、念のためここでもスタイルをオフに設定
toggleSpeechInput.querySelector('.material-icons').textContent = 'mic_off';
toggleSpeechInput.classList.remove('text-primary');
speechStatus.classList.add('hidden');
}
};
wire:ignore
を設定したことで、上記のJavaScriptによるDOM変更がLivewireの更新サイクルから保護され、意図した通りにスタイルが維持されるようになりました。
wire:ignore
を使う際の注意点
-
Livewireディレクティブへの影響:
wire:ignore
を指定した要素の内部では、wire:model
やwire:click
といったLivewireのディレクティブは期待通りに機能しなくなる可能性があります。今回の音声認識ボタンは純粋にJavaScriptで制御しており、Livewireのアクションをトリガーするものではなかったため、この点は問題ありませんでした。 -
wire:ignore.self
: もし、要素自体はLivewireに更新させたくないが、その子要素はLivewireに更新させたい場合は、wire:ignore.self
を使用します。 - Alpine.jsとの連携: より複雑なクライアントサイドのインタラクションが必要な場合は、Livewireと親和性の高いAlpine.jsを導入し、状態管理をAlpine.jsに任せるのも良いと思います。
まとめ
Livewireコンポーネント内で、Web Speech APIのような外部JavaScriptライブラリや、素のJavaScriptでDOMを直接操作する場合、Livewireの再レンダリングによって意図しない表示の巻き戻りが発生することがあります。
このような状況では、wire:ignore
ディレクティブが非常にシンプルかつ効果的な解決策となり得ます。