はじめに (対象読者・この記事でわかること)

この記事は、AngularJSとui-routerを使用したWebアプリケーション開発を経験した開発者、特に複数の画面でモーダルダイアログを再利用したいと考えている方を対象としています。

この記事を読むことで、ui-routerの抽象状態を活用して、Bootstrapベースのモーダルをアプリケーション全体で共通化する実装方法を習得できます。また、モーダルと親コンポーネント間のデータ連携方法や、状態管理との統合手法についても理解を深めることができます。

AngularJSプロジェクトでモーダルを実装する際に、画面ごとに重複したコードを書いてしまうという課題を抱えている方には、特に参考になる内容となっています。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • AngularJSの基本的な知識(ディレクティブ、コントローラー、サービスなど)
  • ui-routerの基本的な使い方(状態定義、パラメータなど)
  • Bootstrapの基本的な知識(特にモーダルコンポーネント)
  • JavaScriptの基本的な知識(非同期処理、Promiseなど)

ui-routerと共通モーダルの必要性

AngularJSでモーダルを実装する場合、単純にBootstrapのモーダルHTMLをAngularJSのディレクティブとしてラップする方法が考えられます。しかし、この方法では各画面ごとにモーダルの実装が必要となり、コードの重複や保守性の問題が生じます。

そこで本記事では、ui-routerの抽象状態(abstract state)を活用したモーダル実装手法を紹介します。この方法の利点は以下の通りです。

  • モーダルの表示状態をui-routerの状態管理と連携できる
  • 複数の画面から同一のモーダルコンポーネントを呼び出せる
  • モーダルと親コンテキスト間のデータ連携が容易になる
  • モーダルの表示/非表示をURLパラメータで制御できる

特に、SPA(シングルページアプリケーション)で複雑な状態遷移を扱う場合、ui-routerの状態管理とモーダルを統合することで、UXの向上とコードの保守性の両立が可能になります。

共通モーダルの具体的な実装方法

ここからは、実際にAngularJSとui-routerを組み合わせた共通モーダルの実装手順を解説します。

ステップ1:モーダル用のテンプレートの作成

まずはBootstrapモーダルのHTMLテンプレートを作成します。このテンプレートは、AngularJSのディレクティブとして機能するように設計します。

Html
<!-- common-modal.html --> <div class="modal fade" id="commonModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal-dialog" ng-class="::modalSize"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">&times;</span> </button> <h4 class="modal-title" ng-bind="::title"></h4> </div> <div class="modal-body" ng-bind="::content"> <!-- 動的なコンテンツはtranscludeで挿入 --> <div ng-transclude></div> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">キャンセル</button> <button type="button" class="btn btn-primary" ng-click="ok()">OK</button> </div> </div> </div> </div>

次に、このテンプレートをAngularJSディレクティブとして定義します。

Javascript
// common-modal.directive.js angular.module('app.common').directive('commonModal', function() { return { restrict: 'E', templateUrl: 'templates/common/common-modal.html', transclude: true, scope: { title: '@', content: '@', modalSize: '@', onOk: '&' }, link: function(scope, element, attrs) { scope.ok = function() { if (scope.onOk) { scope.onOk(); } $(element).modal('hide'); }; } }; });

ステップ2:ui-routerの状態定義

次に、ui-routerの状態定義にモーダル用の抽象状態を追加します。

Javascript
// app.config.js angular.module('app').config(function($stateProvider) { $stateProvider // アプリケーションのメイン状態 .state('app', { url: '', abstract: true, template: '<ui-view></ui-view>' }) // 共通モーダルの抽象状態 .state('app.modal', { url: '/modal/:modalId', abstract: true, template: '<common-modal></common-modal>', onEnter: ['$stateParams', '$uibModal', function($stateParams, $uibModal) { $uibModal.open({ template: '<div ui-view></div>', size: $stateParams.size || 'md' }); }] }); // 具体的なモーダルの状態(例:確認ダイアログ) .state('app.modal.confirm', { url: '/confirm', controller: 'ModalConfirmController', controllerAs: 'vm', params: { title: { value: '確認', squash: true }, message: { value: '', squash: true }, onOk: null } }); });

ステップ3:モーダルサービスの実装

モーダルを簡単に呼び出せるように、サービスを定義します。

Javascript
// modal.service.js angular.module('app.common').service('ModalService', function($state, $uibModal) { this.showConfirm = function(options) { var defaultOptions = { title: '確認', message: 'この操作を実行しますか?' }; var finalOptions = angular.extend(defaultOptions, options); return $state.go('app.modal.confirm', { title: finalOptions.title, message: finalOptions.message, onOk: finalOptions.onOk }); }; // その他のモーダルタイプを追加可能 this.showAlert = function(options) { // アラートダイアログの実装 }; });

ステップ4:モーダルコントローラーの実装

モーダルの動作を制御するコントローラーを実装します。

Javascript
// modal-confirm.controller.js angular.module('app.common').controller('ModalConfirmController', function($scope, $stateParams, $uibModalInstance) { var vm = this; vm.title = $stateParams.title; vm.message = $stateParams.message; vm.ok = function() { if ($stateParams.onOk) { $stateParams.onOk(); } $uibModalInstance.close(); }; vm.cancel = function() { $uibModalInstance.dismiss('cancel'); }; });

ステップ5:アプリケーションへの統合

最後に、作成したモーダルをアプリケーションに統合します。

Html
<!-- index.html --> <div ng-app="app"> <div ui-view="app"></div> </div>
Html
<!-- main.html (app状態のテンプレート) --> <div class="container"> <h1>メイン画面</h1> <button class="btn btn-primary" ng-click="showConfirm()">確認ダイアログを表示</button> </div>
Javascript
// main.controller.js angular.module('app').controller('MainController', function(ModalService) { var vm = this; vm.showConfirm = function() { ModalService.showConfirm({ title: '削除確認', message: 'このアイテムを削除してもよろしいですか?', onOk: function() { // 削除処理を実行 console.log('アイテムを削除しました'); } }); }; });

ハマった点やエラー解決

問題1:モーダルが表示されない

現象:モーダルの状態に遷移しても、モーダルが表示されない。

原因:BootstrapモーダルはjQueryに依存しており、AngularJS単体では正しく動作しない。

解決策:Bootstrapモーダルの表示/非表示をAngularJSの$timeoutと組み合わせて制御します。

Javascript
// common-modal.directive.jsのlink関数を修正 link: function(scope, element, attrs) { scope.ok = function() { if (scope.onOk) { scope.onOk(); } $(element).modal('hide'); }; // モーダルの表示を遅延実行 $timeout(function() { $(element).modal('show'); }); }

問題2:モーダル内のスコープが親と共有されない

現象:モーダル内で親コントローラーのスコープにアクセスできない。

原因:ui-routerの状態が新しいスコープを作成するため、親スコープに直接アクセスできない。

解決策:resolveプロパティを使ってデータを渡すか、$rootScopeを介してデータを共有します。

Javascript
// 状態定義にresolveを追加 .state('app.modal.confirm', { url: '/confirm', controller: 'ModalConfirmController', controllerAs: 'vm', resolve: { modalData: function($stateParams) { return { title: $stateParams.title, message: $stateParams.message, onOk: $stateParams.onOk }; } } }); // コントローラーでresolveデータを受け取る .controller('ModalConfirmController', function(modalData) { var vm = this; vm.title = modalData.title; vm.message = modalData.message; vm.ok = function() { if (modalData.onOk) { modalData.onOk(); } // モーダルを閉じる処理 }; });

問題3:モーダルが閉じた後に状態が残ってしまう

現象:モーダルを閉じた後も、URLにパラメータが残ってしまう。

原因:ui-routerの状態を手動で変更しないため、モーダル状態が残ったままになる。

解決策:モーダルを閉じる際に、親状態に戻る処理を追加します。

Javascript
// モーダルコントローラーに戻る処理を追加 vm.ok = function() { if (modalData.onOk) { modalData.onOk(); } $state.go('app.home'); // 親状態に戻る }; vm.cancel = function() { $state.go('app.home'); // 親状態に戻る };

まとめ

本記事では、AngularJSとui-routerを組み合わせた共通モーダルコンポーネントの実装方法について解説しました。

  • ui-routerの抽象状態を活用することで、モーダルの表示状態をURLと連携できる
  • 共通モーダルサービスを作成することで、各画面から簡単にモーダルを呼び出せる
  • resolveプロパティを利用することで、モーダルと親コンポーネント間のデータ連携が容易になる
  • モーダルのライフサイクルを適切に管理することで、状態の残存問題を防げる

この実装方法を取り入れることで、AngularJSアプリケーションにおけるモーダルの再利用性が向上し、保守性の高いコードを実現できます。今後は、モーダルのアニメーションカスタマイズや、非同期処理との連携など、さらに発展的な実装についても記事にする予定です。

参考資料