Модальные окна в angularJS

Степан

Модальные окна в angularJS

12:48:31 25.10.2016 Степан

В мире народного write-only джаваскрипта, не отягощенного фреймворками, жизнь легка и беззаботна, и вопросов, о том, как добавить в приложение, например, модальные окна ни у кого никогда не возникает - первая же библиотечка из гугла чаще всего решает все проблемы. Работая же с каким-либо фреймворком, мы соглашаемся на его правила игры, и любая "независимая" UI-билиотека скорее всего не впишется в выверенную структуру MV* приложения. Короче, рассмотрим решения, которые справляются с нашей проблемой по-энгуляровому, и поглядим на их преимущества и недостатки.

Сервисы модальных окон

Самый поверхтностный гугляж по этой теме моментально наводит на несколько рабочих и вполне себе окончательных решений:

  1. бустраповый модал https://angular-ui.github.io/bootstrap/#/modal
  2. вот такой https://github.com/dwmkerr/angular-modal-service
  3. и эдакий 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
© 2002-2016 Креограф. Все права защищены законом РФ
 Русский /  English