Модальное диаложество в AngularJS

Диалоговое окно — важный элемент в интерфейсах многих приложений. В этой заметке мы посмотрим, как в AngularJS вызвать модальный диалог, передать в него данные и получить результат (то есть действие пользователя) на примере бутстрапных диалогов Modal из ui.bootstrap (по ссылке есть примеры).

Для начала скачаем и подключим к странице скрипт Angular UI Bootstrap целиком, или кастомную сборку (не забудьте включить в нее Modal). Примерно вот так:

<script src="/js/libs/ui-bootstrap/ui-bootstrap-custom-tpls-0.12.1.js"></script>

Теперь подключим модуль к нашему приложению AngularJS:

var myAppWithDialogs = angular.module('myAppWithDialogs', ['ui.bootstrap']);

Чтобы использовать модальные диалоги в каком-нибудь контроллере, нужно внедрить зависимость от сервиса $modal:

myAppWithDialogs.controller('myRowsController', function ($scope, $modal) {
  //...
}

Сервис $modal позволяет нам заранее создавать темплейты для диалогов, а потом указывать их при вызове. Создадим темплейт для диалога удаления чего-нибудь:

<div class="modal-header">
  <h3 class="modal-title">Delete this item?</h3>
</div>
<div class="modal-body">
  Are you sure to delete this item?
  <div class="form-group" ng-show="isRecurrent">
    <label>
      <input type="radio" ng-model="deleteType" value="this">
      This occurence
    </label><br />
    <label>
      <input type="radio" ng-model="deleteType" value="future">
      Future occurences
    </label><br />
    <label>
      <input type="radio" ng-model="deleteType" value="all">
      All occurences
    </label><br />
  </div>
</div>
<div class="modal-footer">
  <button class="btn btn-danger" ng-click="ok()">Delete</button>
  <button class="btn btn-default" ng-click="cancel()">Cancel</button>
</div>

Сразу вставим в диалог радиокнопки, чтобы пользователь выбрал, как он хочет удалить item — этот экземпляр, этот и будущие или вообще все. Ну, как будто item — это рекуррентная календарная запись. На примере этих радиокнопок мы увидим, как передать в диалог какую-то информацию, и как забрать из диалога то, что пользователь в нем ввел или выбрал.
Обратите внимание на директивы ng-click у кнопок диалога — функции, указанные в этих директивах, должны быть реализованы в контроллере диалога. Именно они будут закрывать диалог и подтверждать или отменять удаление.
Сохраним этот темплейт где-нибудь, например в tpls/delete-dialog.html относительно корня приложения.

Теперь напишем контроллер:


myAppWithDialogs.controller('modalDialogController', function ($scope, $modalInstance, recurrence) {
  $scope.recurrence = recurrence;
  $scope.isRecurrent = (recurrence !== null);
  $scope.deleteType = 'this';
  $scope.ok = function () {
    if ($scope.isRecurrent) {
      $modalInstance.close($scope.deleteType);
    } else {
      $modalInstance.close('all');
    }
  };
  $scope.cancel = function () {
    $modalInstance.dismiss('cancel');
  };
});

В контроллер, помимо $scope, мы внедряем $modalInstance — экземпляр модального окна, с которым будет работать контроллер и передаем значение recurrence, которое указывает является ли наш элемент рекуррентным (т.е. увидит пользователь радиокнопки, или нет).
Затем мы определяем на $scope несколько значений, с которыми связаны элементы диалога (через директивы и ). Так, $scope.isRecurrent связан с тем, видимы наши радиокнопки или нет, а в $scope.deleteType попадет выбор пользователя из трех радиокнопок.

Функция ok(), связанная в темплейте диалога с соответствующей кнопкой, вызывает метод $modalInstance.close(). Этот метод закрывает диалог и позволяет вернуть какое-нибудь значение (в данном случае, выбранную радиокнопку).
Функция cancel() вызывает метод $modalInstance.dismiss(), также закрывающий диалоговое окно с возможностью указать причину.
Разница между close() и dismiss() заключается в том, что promise, возвращаемый при открытии модального окна, в случае вызова close() будет resolved, а в случае dismiss() — rejected.

Теперь допустим у нас есть где-то кнопка «удалить» напротив какой-то строки row (которая должна удалить эту строку). И есть функция deleteItem(row), которая вызывается по клику на эту кнопку. Откроем в ней наш новый диалог:

$scope.deleteItem = function (row) {
  var modalInstance = $modal.open({
    templateUrl: '/tpls/delete-dialog.html',
    controller: 'modalDialogController',
    size: 'sm',
    resolve: {
      recurrence: function () {
        return row.recurrence;
      }
    }
  });
}

Что тут происходит? Мы вызываем метод open() из сервиса $modal, который открывает наше диалоговое окно. В качестве аргумента мы передаем объект со следующими настройками:

  • templateUrl — путь к html-темплейту диалога;
  • controller — контроллер диалога;
  • size — размер окна; значение будет подставлено в класс с префиксом «modal-» — например, если size = sm, класс будет modal-sm;
  • resolve — тут перечисляются значения, которые будут определены и переданы модальному окну; в данном случае мы передаем значение recurrence, которое у нас является свойством объекта row.

Описание остальных параметров можно найти на странице компонента.
Глядя на параметры templateUrl и controller, нетрудно догадаться, что мы можем использовать разные темплейты с одним контроллером, или один контроллер с разными темплейтами. В общем, сервис $modal достаточно гибок в работе.

Наконец, теперь, когда окно открыто, осталось лишь получить выбор пользователя (т.е. подтверждение или отмену). Это нетрудно сделать в той же функции deleteItem() с помощью возвращенного нам объекта modalInstance:


$scope.deleteItem = function (row) {
    //show confirmation dialog
    var modalInstance = $modal.open({
      templateUrl: '/tpls/delete-dialog.html',
      controller: 'modalDialogController',
      size: 'sm',
      resolve: {
        recurrence: function () {
          return row.recurrence;
        }
      }
    });

    modalInstance.result.then(function (deleteType) {
      switch (deleteType) {
        case 'this':
          //удаляем только этот экземпляр
          break;
        case 'future':
          //удаляем этот и будущие экземпляры
          break;
        case 'all':
          //удаляем все экземпляры
          break;
      }
    });
  };

Суть в том, что свойство modalInstance.result — есть не что иное, как обычный promise. Соответственно, мы можем вызвать modalInstance.result.then() и передать 2 коллбэка — для resolved и для rejected. В данном случае мы используем только один коллбэк для resolved, поскольку нам не нужно ничего делать, если пользователь нажал Cancel. А в аргументе коллбэка будет то, что мы передали в контроллере диалога аргументом close(), то есть радиокнопка, выбранная пользователем.

Примерно так можно организовать модальные окна с помощью UI Bootstrap в AngularJS. На первый взгляд решение может показаться несколько громоздким. Но, в силу использования темплейтов и контроллеров, оно довольно гибкое и позволяет относительно компактно определить все нужные нашему приложению диалоги.

Комментарии