AngularJSでmousedown/touchstartが発火しない時に読む徹底ガイド
はじめに (対象読者・この記事でわかること)
本記事は、AngularJS(1.x)を用いたWebアプリケーションで、mousedownやtouchstartが想定通りに発火しない問題に悩むフロントエンド/フルスタックエンジニア、レガシープロダクトを保守する開発者を対象としています。読了後には、イベントが発火しない典型的な原因(イベント抑止、パッシブリスナー、要素の重なり、Angularタッチモジュールの仕様、CSS/ブラウザ挙動など)を切り分ける方法と、ストレスなく再現・修正する具体的な手順が理解できます。また、AngularJS特有のngTouchや$parse、$apply、directive内でのイベント取り扱いの落とし穴と、その代替やベストプラクティスも押さえられます。
開発環境・前提知識
この記事で紹介する内容は、以下の環境で動作確認をしています。
| カテゴリ | バージョン/情報 |
|---|---|
| OS | macOS Sonoma 14.x |
| 言語/FW | Node.js 20.x / AngularJS 1.8.x |
| ライブラリ | angular-touch 1.8.x, Hammer.js 2.0.x (任意) |
また、この記事を読み進める上で、以下の知識があるとスムーズです。 * DOMイベント伝播(キャプチャ/バブリング)、pointer/mouse/touchイベントの基本 * AngularJSディレクティブ(compile/link)、$scope/$apply/$timeoutの基礎
そもそもなぜ発火しないのか:概要・背景
AngularJS 1.xでは、ngTouch(angular-touch)を導入すると、click遅延回避・ゴーストクリック対策のために内部でtouch系イベントを扱い、場合によっては既定のイベント(mousedownやclick)を抑止・変換します。さらに、iOS Safariのタッチイベント優先処理や、CSSでのpointer-eventsやz-index、overflowのスクロール挙動、passive: trueが既定化されたブラウザの変更などが絡み合い、期待するタイミングでイベントが配信されないことが起こります。
また、AngularJSのディレクティブ内でjQuery liteや生のaddEventListenerを使う際、$scopeの変更を$applyで伝搬しない、あるいはイベントの設置フェーズ(compile/link/$(document).ready)が不適切でDOMが未構築など、フレームワークとのインテグレーションに起因する問題も多発します。本記事では、ブラウザ仕様・CSS・AngularJS/ライブラリの相互作用を切り分け、最短で原因に到達するためのチェックリストと修正パターンを解説します。
実践ガイド:原因の切り分けと実装パターン
ここが記事のメインパートです。端末・ブラウザ・AngularJS設定によって複数の要因が重なることが多いため、再現性ある最小構成での確認から進めます。
ステップ1:最小再現とイベント水位の確認
1) 最小HTMLを用意(AngularJSを読み込まずに確認)
<!doctype html>
<html>
<body>
<button id="btn" style="position:relative; z-index:1;">Tap/Click me</button>
<script>
const btn = document.getElementById('btn');
['touchstart','pointerdown','mousedown','click'].forEach(type => {
btn.addEventListener(type, e => console.log('fired:', type), {passive:false});
});
</script>
</body>
</html>
期待: 端末ごとにtouchstart/pointerdown/mousedown/clickの順または一部が発火。ここで発火するならブラウザ/DOMの問題は小さく、Angular統合層が疑わしい。
2) AngularJSを導入し、同じ要素にng-clickや自作ディレクティブを追加して再確認。発火が消える/変わるタイミングを特定します。
3) ブラウザDevToolsで以下を確認 - Elementsで要素の重なり: pointer-events:none、z-index、position、overflow/scrollが干渉していないか - Consoleでイベントログ順序 - Renderingパネルでヒットテスト(Layers/Hit-test)やPaintフラッシング - モバイルではTouch emulation、Issuesでパッシブリスナー警告
4) OS/ブラウザ差異 - iOS Safari: touchstart優先、hover非対応、click 300ms遅延(近年はviewport metaで回避可) - Chrome/Android: pointerイベントが標準、パッシブ既定のケースあり
ステップ2:AngularJS/周辺の代表的な落とし穴と対処
1) ngTouchによる抑止/変換 - 症状: touchstartが拾えない、mousedownが来ない、クリック相当の動作が二重/無発火。 - 対策: - ngTouchを一時的に外して再確認。 - ngClickの代わりにpointerdown/pointerupを自前ディレクティブで扱う。 - クリック遅延対策はで代替し、ngTouch依存を減らす。 - 必要箇所のみngTouchを適用する(画面単位モジュール分割)。
2) パッシブリスナー問題 - 症状: preventDefault()が効かず、スクロール優先でtouchstart相当が通らない。 - 対策: addEventListenerで{ passive:false }を明示。AngularJS/jqLiteはオプション未対応のため、rawなaddEventListenerを使うか、polyfillを導入。
3) 要素の重なり/ヒットテスト - 症状: 見えているのにクリック不可。別レイヤが覆っている。 - チェック: DevToolsで:hov強制、pointer-events、z-indexコンテキスト(transform/opacity/positionで新しいスタッキングコンテキストができる)を確認。 - 対策: pointer-events:auto、z-index/positionの見直し、透明オーバーレイの除去。スクロールコンテナにtouch-actionを適切に設定。
4) CSS touch-actionの未設定 - 症状: デフォルトジェスチャ(パン/ズーム)が優先され、pointerdown/mousedownが来ない。 - 対策: 操作したい要素にtouch-action: manipulation or none; を付与。例: ボタン系はmanipulation、カスタムジェスチャはnone。
5) ディレクティブ内スコープ更新漏れ - 症状: イベント自体は発火しているがUIが変わらない/状態が反映されない。 - 対策: 生イベントからスコープ更新時は$scope.$applyまたは$timeoutでダイジェストを起動。$apply中のエラーに注意。
6) ポインタイベントへの統一 - 推奨: mouse/touchの両対応をpointerイベントで一本化。古いiOS Safariでのフォールバックを用意。 - メリット: コードの分岐が減り、レースや二重発火を回避。
ステップ3:ディレクティブ実装の実例(安全なイベント取り扱い)
1) pointerイベントを優先、フォールバック付き
angular.module('app', [])
.directive('pressable', function($timeout) {
return {
restrict: 'A',
scope: { onPress: '&' },
link: function(scope, el) {
var node = el[0];
var down = function(e){
// 重要: スクロールを制御したい場合に限り
if (e.cancelable) e.preventDefault();
// Angularダイジェストを確実に
scope.$applyAsync(function(){ scope.onPress({ $event: e }); });
};
// pointer系があればそれを使う
if (window.PointerEvent) {
node.addEventListener('pointerdown', down, { passive:false });
} else {
// フォールバック: touch -> mouse の順に登録(重複抑制は必要に応じて追加)
node.addEventListener('touchstart', down, { passive:false });
node.addEventListener('mousedown', down, false);
}
scope.$on('$destroy', function(){
if (window.PointerEvent) {
node.removeEventListener('pointerdown', down, { passive:false });
} else {
node.removeEventListener('touchstart', down, { passive:false });
node.removeEventListener('mousedown', down, false);
}
});
}
};
});
- ポイント:
- passive:falseでpreventDefaultを有効化。
- $applyAsyncでダイジェストを安全にスケジュール。
- $destroyでリスナーを解除してリーク防止。
- フォーカス/アクセシビリティ対応が必要ならkeyイベントも併用。
2) ngTouchとの干渉を避ける - ng-clickを使う要素と、上記pressableを併存させない。 - クリック遅延対策はviewport meta/TurboTap等で代替。
3) スクロールコンテナと競合する場合 - スクロールが必要な親要素にはtouch-action:auto(既定)を保持し、インタラクティブ要素にのみmanipulation/noneを付与。 - ドラッグ操作を実装するときは、pointercancel/pointerupも監視して取りこぼしを防ぐ。
ステップ4:ブラウザ固有対策とチェックリスト
チェックリスト(上から順に確認) - DevToolsで当該要素にヒットしているか(hover状態、イベントログ) - CSS: pointer-eventsがnoneではない、z-indexで覆われていない、transformで新スタックを作っていないか - viewport metaが適切(幅固定、ズーム要件に応じて調整) - touch-actionの指定が要件と一致 - passiveリスナー警告が出ていないか - ngTouchを外すと改善するか - ディレクティブ内で$apply/$digestを呼んでいるか - 生のaddEventListenerを使うなら{ passive:false }で登録しているか - モバイルでの長押しメニュー/選択防止(-webkit-touch-callout: none; user-select: none;)が必要か - 代替としてpointerイベントへ統一できるか
ハマった点やエラー解決
エラー内容:
[Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/...
解決策: - addEventListener('touchstart', handler, { passive:false }) として再登録。 - AngularJSのjqLiteではオプションが渡せないため、el[0].addEventListenerを使う。 - preventDefaultが不要なら呼ばない(スクロールを阻害しない)。
エラー内容:
$apply already in progress
解決策: - scope.$applyではなく$scope.$applyAsync/$timeoutを使用。 - 既存のイベントハンドラから多重に$applyを呼んでいないか見直す。
症状: - 見えているのに反応しない(特にiOS) 解決策: - 要素または親にpointer-events:autoを付与。 - position/z-index/transformの見直し。 - iOSでclick発火を期待するなら、タップ対象をbutton/inputにしてCSSで見た目を整えるか、pointerdownベースで自前処理へ移行。
まとめ
本記事では、AngularJSでmousedown/touchstartが発火しない問題を、ブラウザ仕様、CSS、ngTouch、イベント登録方法の4レイヤに分けて整理し、再現→切り分け→修正の手順を提示しました。 - 発火しない主因は、ngTouchの抑止、passiveリスナー、要素のヒットテスト失敗、touch-action未設定に集約されやすい。 - 生のaddEventListenerで{ passive:false }、pointerイベントへの統一、$applyAsyncの活用が有効。 - DevToolsでの重なり確認と最小再現の比較により、原因特定が大幅に早まる。
これらにより、端末差やレガシー構成でも安定した入力処理を構築できます。今後は、アクセシビリティやキーボード操作の統合、ドラッグ/ジェスチャの抽象化(Pointer Events対応のユーティリティ化)についても掘り下げる予定です。
参考資料
- Pointer Events Level 2 仕様書: https://www.w3.org/TR/pointerevents2/
- MDN Web Docs: Passive event listeners https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener#parameters
- AngularJS ngTouch ドキュメント https://docs.angularjs.org/api/ngTouch
- Google Developers: Scrolling performance with passive listeners https://developers.google.com/web/updates/2016/06/passive-event-listeners
- MDN: touch-action https://developer.mozilla.org/docs/Web/CSS/touch-action