
Модальные окна в angularJS
В мире народного write-only джаваскрипта, не отягощенного фреймворками, жизнь легка и беззаботна, и вопросов, о том, как добавить в приложение, например, модальные окна ни у кого никогда не возникает - первая же библиотечка из гугла чаще всего решает все проблемы. Работая же с каким-либо фреймворком, мы соглашаемся на его правила игры, и любая "независимая" UI-билиотека скорее всего не впишется в выверенную структуру MV* приложения. Короче, рассмотрим решения, которые справляются с нашей проблемой по-энгуляровому, и поглядим на их преимущества и недостатки.
Сервисы модальных окон
Самый поверхтностный гугляж по этой теме моментально наводит на несколько рабочих и вполне себе окончательных решений:
- бустраповый модал https://angular-ui.github.io/bootstrap/#/modal
- вот такой https://github.com/dwmkerr/angular-modal-service
- и эдакий https://github.com/btford/angular-modal
Модуль из бутстрапового набора выглядит довольно тяжеловесно: в нужный контроллер пробрасывается сервис-конструктор, который с указанием настроек открывает модальное окно и ждет ответа в промисах. В контроллере самого модального окна мы используем уже другой сервис, представляющий экземпляр окна, у которого можно вызвать методы "закрыть" и "отозвать". Звучит довольно путано, не правда ли? Попробую прокомментировать пример с сайта проекта.
angular.module('ui.bootstrap.demo').controller('ModalDemoCtrl', function ($scope, $uibModal, $log) { $scope.items = ['item1', 'item2', 'item3']; $scope.open = function () { // настраивается подобно angular-route: указываются шаблон и контроллер var modalInstance = $uibModal.open({ templateUrl: 'myModalContent.html', controller: 'ModalInstanceCtrl', // у сервиса есть несчетное количество найстроек типа размера, анимации и т.п. size: 'sm', animation: true // пробрасываемые в контроллер данные. Определять сюда асинхронные $promise // будет хорошей идеей resolve: { items: function () { return $scope.items; } } }); // определяем два хендлера для обработки $uibModalInstance.close // и $uibModalInstance.dismiss modalInstance.result.then(function (selectedItem) { $scope.selected = selectedItem; }, function () { $log.info('Modal dismissed at: ' + new Date()); }); }; }); // описываем контроллер самого модального окна angular.module('ui.bootstrap.demo').controller('ModalInstanceCtrl', function ($scope, $uibModalInstance, items) { // вот они наши проброшенные айтемы $scope.items = items; $scope.selected = { item: $scope.items[0] }; // определяем методы, которые будут вызаваться // во вьюхе на кнопках "ok" и "cancel" $scope.ok = function () { $uibModalInstance.close($scope.selected.item); }; $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; });
Это пример использования сервиса в "полнофункциональном" режиме. Если задачи модалов в приложении ограничиваются простыми текстовыми оповещенями и диалоговыми окнами, то вся писанина сокращаеся до вызова функции open
из начала первого контроллера.
Обе другие из указанных библиотек работают примерно так же: конфигурируется какое-то окно по знакомому принципу: шаблон+контроллер, и вешаеся ждущий закрытия окна хендлер.
В чем проблема такого подхода? Многих тянет вываливать все эти кучи кода в одном месте, образуя непролазную свалку из промисов, закрывалок, открывалок и контроллеров. Даже если попытаться абстрагировать их в отдельные сервисы, код модалов со сложной логикой скорее всего окажется "потерянным". Вот пример из моего файла с окнами логина и регистрации: по сервису и по контроллеру на каждое. В итоге логика одного состояния оказалась в каждом разбита на два независимых блока, что вносило изрядную путаницу.
regModal = (btfModal) -> btfModal templateUrl: '/partials/partials/register-modal.html' controller: 'regModalCtrl as vm' class regModalCtrl @$inject: ['$scope', '$state', 'session', 'regModal', 'loginModal', 'User'] constructor: ($scope, $state, session, regModal, loginModal, User) -> $scope.newUser = new User $scope.close = regModal.deactivate $scope.openLoginModal = -> regModal.deactivate() loginModal.activate() $scope.register = -> User.register $scope.newUser, (user) -> session.init(user) regModal.deactivate() $state.go('app.profile.edit', slug: user.slug)
ui-router
Если создание модального окна при помощи этих библиотек так напоминает блоки нашего любимого роутера, почему бы просто не попробовать смастерить эти окна в виде состояний? Мысли в таком направлении приводят к элегантному решению без использования каких-либо сторонних сервисов, не лишенному хотя собственных минусов.
Начнем с того, что вспомним о системе именованных представлений ui-router'a (полностью о них можно почитать здесь). Любой вьюхе в приложении можно дать имя, а в описании состояния - применить к ней шаблон и контроллер по этому имени, указав до нее путь. Короче, у меня эта система выглядит примерно так.
В корневом шаблоне находятся две входных точки для представлений:
<body> <div ui-view></div> <!-- это главная вьюха нашего приложения "по умолчанию" --> <div ui-view="modal"></div> <!-- к этой вьюхе мы будем обращаться из состояний модальных окон --> </body>
В файле с роутами-состояниями добавлены состояния модальных окон, в которых стоит обратить внимания на блокviews
и абсолютный путь до вьюхи @modal
.
$stateProvider .state('pages' url: '/' abstract: true templateUrl: '/partials/pages/index.html' controller: 'MenuCtrl as index') .state('pages.home' url: '' templateUrl: '/partials/pages/home.html' controller: 'HomePageCtrl as vm') .state('pages.home.login' url: 'login' views: 'modal@': templateUrl: '/partials/partials/login-modal.html' controller: 'HomePageLoginCtrl as vm') .state('pages.home.register' url: 'register' views: 'modal@': templateUrl: '/partials/partials/register-modal.html' controller: 'HomePageRegisterCtrl as vm')
После этого я делаю, например, кнопку "регистрация" ссылкой на состояние.
<a ui-sref="pages.home.register">Регистрация</a>
Чем выгодно описание модалок прямо в роутере? Сложные модальные окна становятся полноценными экранами приложения, со своими url'ами. Появляется возможность пользоваться всем широким функционалом ui-router'а. И самое главное - код приобретает единообразность. Крупные контроллеры этих окон уже не теряются по непонятным файлам, а логично вписываются в архитектуру приложения, выстроенную вокруг скелета-роутера.
Теперь к минусам. Использовать роутер для создания множества модалов-алертов или небольших диалогов, мягко говоря, уже будет не так красиво: придется для каждого выделять по состоянию и скоро в файле роутера будет не продраться через все мелкие модалки. Потом, можно заметить, что определенный таким образом модал может "всплыть" только над родителем, определенном в пути состояния, и если оно определено как, например,pages.home.register
, "нижняя", основная вьюха всегда при переходе к регистрации будет переключаться на состояниеpages.home
.
Что ж в итоге лучше?
Для себя я решил остановиться на компромиссном варианте:
- для алертов и диалогов использовать сервис на основе этой крохотной библиотечки
- для сложных экранов, являющимися полноценными кусками приложения - ui-router