Управление модальными окнами в Ember.JS

В своей практике я больше сталкиваюсь с Twitter Bootstrap, потому и решение для работы с модальными окнами будет заточено под него. В тоже время сама его идея может быть применена к любому другому фреймворку.

Ребятами из компании Kaliber5 был разработан и активно поддерживается аддон ember-bootstrap. В нем реализованы Компоненты Bootstrap в виде Компонентов EmberJS (и это не просто обертки, а полнофункциональные решения). Есть в их числе и Модальные Окна. Их использование в общем виде можно описать так:

1
2
3
4
5
{{#bs-modal open=modalIsOpened as |modal|}}
  {{modal.header}}
  {{modal.body}}
  {{modal.footer}}
{{/bs-modal}}

Тут стоит обратить внимание на параметр open. Он указывает на то, открыто или нет в данный момент Модальное Окно. Если на какой-то из страниц вашего приложения есть необходимость выводить несколько окон в разное время (например, подтверждение разных действий), то вам придется описать для каждого из них свой флаг и следить за его состоянием. То есть, вам нужны еще два обработчика событий на открытие и закрытие Окна. Как-то так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Controller from '@ember/controller';
import {set} from '@ember/object';
 
export default Controller.extend({
  modalIsOpened: false,
  actions: {
    openModal() {
      set(this, 'modalIsOpened', true);
      // any other stuff
    },
    closeModal() {
      set(this, 'modalIsOpened', false);
      // any other stuff
    }
  }
});

И чем больше у вас будет Окон, тем больше такого не очень «полезного» кода вам придется написать. Да и имена флагов и методов надо как-то согласовать, чтоб не возникало путаницы в будущем.

Есть путь, который позволяет избежать этого. Кратко его можно описать так: используем отдельный Сервис и Компонент-контейнер, которые отвечают за отрисовку Модальных Окон. Когда пользователь закрывает Модальное Окно, то это соответствует одному из двух состояний — пользователь решил выполнить какое-то действие или же отказался от него. Даже если ваше Окно выводит пользователю три кнопки, то все равно две из них будут для подверждения или отказа от действия (разве что в немного разных формах). Три принципиально разных действия трудно представить.

Это похоже на то, как ведет себя Promise. На момент открытия Окна можно инициализировать его экземпляр, а при закрытии — выполнять или отклонять его.

В коде Сервиса modals-manager это будет выглядеть так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// app/services/modals-manager.js
import {computed, get, set} from '@ember/object';
import {assert} from '@ember/debug';
import Service from '@ember/service';
import {defer} from 'rsvp';
 
export default Service.extend({
 
  modalIsOpened: false,
 
  modalDefer: null,
 
  options: computed(function () {
    return {};
  }),
 
  componentName: null,
 
  show(componentName, options) {
    assert('Only one modal may be opened in the same time!', !get(this, 'modalIsOpened'));
    set(this, 'modalIsOpened', true);
    set(this, 'options', options);
    set(this, 'componentName', componentName);
    const modalDefer = defer();
    set(this, 'modalDefer', modalDefer);
    return modalDefer.promise;
  },
 
  onConfirmClick(v) {
    set(this, 'modalIsOpened', false);
    get(this, 'modalDefer').resolve(v);
    set(this, 'options', {});
  },
 
  onDeclineClick(v) {
    set(this, 'modalIsOpened', false);
    get(this, 'modalDefer').reject(v);
    set(this, 'options', {});
  }
});

Свойство modalIsOpened отвечает за то открыто или закрыто Модальное Окно в данный момент. Одновременно только одно Окно может быть открыто (для этого есть проверка в методе show). Свойство options — это объект с какими-угодно данными, которые будут переданные в Компонент с Модальным Окном. componentName — это имя Компонента с Модальным Окном, который отрисован а данный момент.

Метод show — это единственный метод, который должен вызываться пользователем, если ему надо отрисовать то или другое Модальное Окно. Он принимает два параметра — имя Компонента с Модальным Окном и объект с допольнительными данными для этого Компонента. Их значения записываются в одноименные свойства Сервиса. Так же в show инициализируется свойство modalDefer (о котором ничего не было сказано в предыдущем абзаце). Раньше речь шла о Promise, а тут появился какой-то defer. Вообще, defer внутри себя содержит тот же Promise, но его resolve и reject доступны извне. Если посмотреть на его исходный код, это очень хорошо видно:

1
2
3
4
5
let deferred = {resolve: undefined, reject: undefined};
deferred.promise = new Promise((resolve, reject) => {
  deferred.resolve = resolve;
  deferred.reject = reject;
});

Метод show возвращает Promise, так что при его вызове можно будет написать код вида:

1
2
3
modalsManager.show('some-modal', {})
  .then(() => { /* ... */ })
  .catch(() => { /* ... */ });

У Сервиса остались еще два метода — onConfirmClick и onDeclineClick. Как следует из названий, они вызываются при закрытии Модального Окна. Первый используется, когда пользователь подтверждает какое-то действие, а второй — в любом другом случае закрытия Окна. Эти методы выполняют или отклоняют Promise, что приводит к срабатыванию функций обратного вызова в then или catch.

С Сервисом modals-manager все ясно. Теперь надо рассмотреть Компонент, в котором будут выводиться Модальные Окна. Назовем его modals-container. Он выглядит так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import Component from '@ember/component';
import {inject as service} from '@ember/service';
import {get} from '@ember/object';
import {readOnly} from '@ember/object/computed';
 
export default Component.extend({
  modalsManager: service(),
 
  options: readOnly('modalsManager.options'),
 
  modalIsOpened: readOnly('modalsManager.modalIsOpened'),
 
  componentName: readOnly('modalsManager.componentName'),
 
  actions: {
 
    confirm(v) {
      get(this, 'modalsManager').onConfirmClick(v);
    },
 
    decline(v) {
      get(this, 'modalsManager').onDeclineClick(v);
    }
  }
});

И его Шаблон:

1
2
3
4
5
6
7
8
9
{{#if modalIsOpened}}
  {{component
    componentName
    modalIsOpened=modalIsOpened
    options=options
    onConfirm=(action "confirm")
    onDecline=(action "decline")
  }}
{{/if}}

Как видим, этот Компонент служит обычным передатчиком между Сервисом modals-manager и реальными Компонентами Модальных Окон. Все, что с ним надо сделать, это разместить его внутри application.hbs:

1
2
3
{{! app/templates/application.hbs}}
{{outlet}}
{{modals-container}}

Пришло время описать один из таких Компонентов. В браузерах есть стандартное модальное окно confirm. Сделаем такое же, но уже средствами ember-bootstrap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// app/components/modal-confirm.js
import Component from '@ember/component';
import {get} from '@ember/object';
 
export default Component.extend({
  modalIsOpened: false,
  options: null,
  onConfirm: null,
  onDecline: null,
  actions: {
    confirm(v) {
      get(this, 'onConfirm')(v);
    },
    decline(v) {
      get(this, 'onDecline')(v);
    }
  }
});

И его Шаблон:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{{! app/templates/components/modal-confirm.hbs}}
{{#bs-modal
  open=modalIsOpened
  title=options.title
  onSubmit=(action "confirm")
  onHide=(action "decline")
as |modal|}}
  {{modal.header}}
  {{#modal.body}}
    {{options.body}}
  {{/modal.body}}
  {{#modal.footer}}
    {{#bs-button onClick=(action modal.close)}}{{options.decline}}{{/bs-button}}
    {{#bs-button type="primary" onClick=(action modal.submit)}}{{options.confirm}}{{/bs-button}}
  {{/modal.footer}}
{{/bs-modal}}

Пример его использования через modals-manager:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Controller from '@ember/controller';
import {set} from '@ember/object';
import {inject as service} from '@ember/service';
 
export default Controller.extend({
  modalsManager: service(),
  actions: {
    someAction() {
      get(this, 'modalsManager')
        .show('modal-confirm', {title: 'Some title', body: 'Some body'})
        .then(() => {
          // ...
        })
        .catch(() => {
          // ...
        });
    }
  }
});

По такому же принципу можно сделать и аналоги Модальных Окон alert и prompt. Да и практически любое Модальное Окно можно написать так, чтоб им можно было управлять через modals-manager.

Пару недель назад был написал небольшой (но достаточно функциональный) аддон ember-bootstrap-modals-manager, в котором реализован весь вышеописанный функционал и даже больше.

ember i ember-bootstrap-modals-manager

После этого не забываем добавить {{modals-container}} в Шаблон приложения.

Такие стандартные Модальные Окна как alert, confirm и prompt уже есть «из коробки». Их поведение точно такое же, как и оригинальных window.alert, window.confirm и window.prompt (примеры по ссылке):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import Controller from '@ember/controller';
import {set} from '@ember/object';
import {inject as service} from '@ember/service';
 
export default Controller.extend({
  modalsManager: service(),
  actions: {
    showAlert() {
      get(this, 'modalsManager')
        .alert({title: 'Some title', body: 'Some body'})
        .then(() => {})
        .catch(() => {});
    },
    showConfirm() {
      get(this, 'modalsManager')
        .confirm({title: 'Some title', body: 'Some body'})
        .then(() => {})
        .catch(() => {});
    },
    showPrompt() {
      get(this, 'modalsManager')
        .prompt({title: 'Some title', body: 'Some body'})
        .then(providedValue => {})
        .catch(() => {});
    }
  }
});

Модальное Окно confirm используется для подтверждения действия со стороны пользователя. В нем выводится обычное сообщение с вопросом и две кнопки «Да» и «Нет». Однако, очень часто пользователи закрывают такие окна бездумно и не вчитываясь, о чем там их спрашивают. Для таких случаев есть два других варианта подтверждения действия — check-confirm и prompt-confirm. В каждом из них кнопка «Да» по умолчанию заблокирована. В первом надо кликнуть по флажку в теле Модального Окна, а во втором случае надо ввести в поле ввода Модального Окна заранее определенное слово. Только тогда кнопка «Да» будет разблокирована и на нее можно будет нажать.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import Controller from '@ember/controller';
import {set} from '@ember/object';
import {inject as service} from '@ember/service';
 
export default Controller.extend({
  modalsManager: service(),
  actions: {
    showCheckConfirm() {
      get(this, 'modalsManager')
        .checkConfirm({title: 'Some title', body: 'Some body'})
        .then(() => {})
        .catch(() => {});
    },
    showPromptConfirm() {
      get(this, 'modalsManager')
        .confirm({
          title: 'Some title',
          body: 'Some body',
          promptValue: 'test' // <-- required
        })
        .then(() => {})
        .catch(() => {});
    }
  }
});

Методы alert, confirm, prompt, checkConfirm и promptConfirm — это обертки над методом show, который был описан выше. Соответствующие этим методам Компоненты Модальных Окон уже определены в самом аддоне. Метод show может быть использован и для отрисовки практически любых других Модальных Окон. Для этого они должны выполнять ряд условий.

Во-первых, ваш Компонент должен быть классом-наследником от Компонента modals-container/base:

1
2
3
4
// app/components/my-modal.js
import Base from './modals-container/base';
export default Base.extend({
});

Во-вторых, в Шаблоне Компонента должен быть вызов bs-modal, у которого параметр open привязан к свойству modalIsOpened, а обработчиками событий onSubmit и onHide должны быть confirm и decline соответственно:

1
2
3
4
5
6
7
{{! app/templates/components/my-modal.hbs}}
{{#bs-modal
  open=modalIsOpened
  onSubmit=(action "confirm")
  onHide=(action "decline")
as |modal|}}
{{/bs-modal}}

В-третьих, обработчиками нажатий кнопок «Да» и «Нет» должны быть modal.submit и modal.close соответственно:

1
2
3
4
5
6
7
{{! app/templates/components/my-modal.hbs}}
{{#bs-modal ... as |modal|}}
  {{#modal.footer}}
    {{#bs-button onClick=(action modal.close) type="default"}}No{{/bs-button}}
    {{#bs-button onClick=(action modal.submit) type="success"}}Yes{{/bs-button}}
  {{/modal.footer}}
{{/bs-modal}}

Вот и все. Выполнив эти условия, Вы сможете управлять новыми Модальными Окнами через modalsManager.show.

Еще раз ссылки на демку — https://git.io/vAt88 и на API-документацию — https://git.io/vAt80.

, , , ,

Оставить комментарий

Top ↑ | Main page | Back