Перевод модальных окон из устаревших контроллеров на новый API компонента DModal

:information_source: Если вы реализуете новый Modal, ознакомьтесь с основной документацией здесь. В этой теме описывается, как перенести существующий модальный компонент, основанный на контроллере, на новый компонентный API.

В прошлом Discourse использовал API на базе Ember-Controller для отрисовки модальных окон. Для вызова модального окна вы передавали строку с именем контроллера в showModal(). Под капотом это использовало API Route#renderTemplate из Ember, которое устарело в Ember 3.x и будет удалено в Ember 4.x.

Чтобы позволить Discourse обновиться до Ember 4.x и выше, мы представили новый компонентный API для модальных окон. Этот новый API следует декларативным паттернам проектирования Ember и стремится обеспечить чистую семантику DDAU (data down actions up — данные вниз, действия вверх).

Шаг 1: Перемещение файлов

Переместите JS-файл контроллера и файл шаблона в каталог /components/modal. Это сделает их «колокатируемым компонентом» (colocated component), который можно импортировать так же, как любой другой JS-модуль.

Шаг 2: Обновление JS-файла

Затем обновите определение компонента в JS, чтобы он наследовался от @ember/component, а не от @ember/controller [1]. Удалите миксин ModalFunctionality и обновите все использования его функций согласно таблице ниже:

Было Стало
flash() и clearFlash() Создайте свойство flash в вашем компоненте и передайте его в аргумент @flash компонента <DModal>. По умолчанию предупреждение будет стилизовано с помощью класса alert, который является копией класса ‘error’, но это можно переопределить, используя аргумент @flashType.
showModal() Импортируйте функцию showModal из discourse/lib/show-modal
действие closeModal Вызовите аргумент closeModal, который автоматически передается в ваш компонент

Контроллеры модальных окон старого стиля существовали «вечно», что означало, что нам приходилось вручную очищать состояние. С новым компонентным API компонент будет создан и уничтожен при показе/скрытии модального окна. Во многих случаях это означает, что ваши старые хуки жизненного цикла больше не нужны.

Если вам все еще нужна логика, основанная на жизненном цикле, используйте эту таблицу:

Было Стало
onShow() Используйте стандартный жизненный цикл компонента Ember (init() или модификатор Ember)
afterRender Используйте стандартный жизненный цикл компонента Ember (init() или модификатор Ember)
beforeClose() Создайте обертку вокруг аргумента @closeModal, который передается в ваш компонент. Передайте ссылку на вашу обертку закрытия в DModal, например <DModal @closeModal={{this.myCloseModalWrapper}}>
onClose() Используйте стандартный жизненный цикл компонента Ember (willDestroy() или модификатор Ember)

Шаг 3: Обновление шаблона

Замените обертку <DModalBody> на <DModal>. Добавьте несколько новых атрибутов:

  • Передайте новый аргумент @closeModal
  • Добавьте явный класс. Чтобы соответствовать старому поведению, возьмите имя файла вашего контроллера и добавьте -modal.

Например, если ваш контроллер модального окна назывался close-topic.js, новый вызов <DModal> будет выглядеть примерно так:

<DModal @closeModal={{@closeModal}} class="close-topic-modal">

Если в вызове DModalBody есть другие аргументы, обновите их на основе таблицы ниже:

Было Стало
@title="title_key" @title={{i18n "title_key"}}
@rawTitle="translated title" @title="translated title"
@subtitle="subtitle_key" @subtitle={{i18n "subtitle_key"}}
@rawSubtitle="translated subtitle" @subtitle="translated subtitle"
@class @bodyClass
@modalClass Используйте синтаксис угловых скобок с обычным html-атрибутом: <DModal class="blah">
@titleAriaElementId Используйте синтаксис угловых скобок с обычным html-атрибутом: <DModal aria-labelledby="blah">
@dismissable, @submitOnEnter, @headerClass Без изменений

Если после старого компонента <DModalBody> рендерился какой-либо контент футера, используйте новый именованный блок <:footer>, чтобы ввести его внутри <DModal>. При использовании именованных блоков контент тела должен быть обернут в <:body></:body>. Например:

<DModal @closeModal={{@closeModal}}>
  <:body>
    Hello world, this is the content of the modal
  </:body>
  <:footer>
    This is the footer content. A `.modal-footer` wrapper will be added
    automatically
  </:footer>
</DModal>

Шаг 4: Обновление мест вызова showModal

Ранее модальные окна рендерились с помощью API showModal, который принимал строку (имя контроллера) и ряд опций. Он возвращал экземпляр контроллера, с которым можно было манипулировать:

import showModal from "discourse/lib/show-modal";

export default class extends Component {
  showMyModal() {
    const controller = showModal("my-modal", {
      title: "My Modal Title",
      modalClass: "my-modal-class",
      model: { topic: this.topic },
    });

    controller.set("updateTopic", this.updateTopic);
  });
}

Чтобы отрисовать новые компонентные модальные окна, вам следует внедрить сервис ‘modal’ (или получить к нему доступ с помощью чего-то вроде getOwner(this).lookup("service:modal")) и вызвать функцию show().

show() принимает ссылку на новый класс компонента в качестве первого аргумента. Единственной поддерживаемой опцией по-прежнему является ‘model’, которую можно использовать для передачи всех данных/действий, необходимых для вашего модального окна.

Ссылка на экземпляр компонента возвращаться не будет. Вместо этого show() возвращает промис, который будет разрешен при закрытии модального окна. Промис будет разрешен с любыми данными, которые были переданы в @closeModal.

import MyModal from "discourse/components/my-modal";
import { service } from "@ember/service";

export default class extends Component {
  @service modal;

  showMyModal() {
    this.modal.show(MyModal, {
      model: { topic: this.topic, updateTopic: this.updateTopic },
    });
  });
}

В качестве альтернативы, перейдите на декларативный API, описанный в основной документации DModal.

Функциональность старых опций можно воспроизвести следующим образом:

Старая опция showModal Решение
admin Не применимо для компонента — удалите
templateName Не применимо для компонентов — удалите
title Перенесите в <DModal @title={{i18n "blah"}}>
titleTranslated Перенесите в <DModal @title="blah">. При необходимости это можно вычислить на основе данных из model
modalClass Перенесите в <DModal class="blah">
titleAriaElementId Перенесите в <DModal aria-labelledby="blah">
panels Используйте именованный блок <:headerBelowTitle>, чтобы реализовать вкладки в вашем компоненте (пример)
model Без изменений

Шаг 5: Тесты

Большинство тестов должны остаться практически без изменений. Самая распространенная проблема:

  • Модальные окна больше не имеют класса по умолчанию, основанного на их имени. Классы должны быть указаны явно в шаблоне (см. начало Шага 3)

  • Обертка d-modal больше не сохраняется в DOM при закрытии модального окна. Чтобы проверить, что все модальные окна закрыты, используйте проверку, такую как assert.dom('.d-modal').doesNotExist()

Готово!

Ваше модальное окно должно работать так же, как и раньше. Чтобы в полной мере воспользоваться новым API, вы можете рассмотреть возможность замены вызовов showModal декларативной стратегией и преобразования вашего модального окна в компонент Glimmer.

Примеры

Вот несколько примеров коммитов, демонстрирующих преобразование некоторых модальных окон ядра Discourse на новый API:


Этот документ находится под контролем версий — предлагайте изменения на github.


  1. В этом руководстве рекомендуются классические компоненты Ember, так как они обеспечивают самый простой путь миграции из контроллеров Ember. Но для простых модальных окон, или если вы готовы потратить время на рефакторинг, современные компоненты Glimmer — лучший выбор. ↩︎

20 лайков

Выглядит действительно отлично. Это вселяет надежду, что я смогу конвертировать свои модальные окна в Ember 4. Я с трудом понимаю код Ember, который пишу, поэтому писать документацию, которую я смогу понять, непросто. Большое спасибо за это.

8 лайков

Спасибо за туториал! Просмотр примеров оказался очень полезным. Мне удалось за час исправить сломанное модальное окно моего кастомного плагина.

4 лайка

Я сейчас работаю над этим преобразованием, но столкнулся с проблемой:

Ранее у нашего модального окна не было соответствующего контроллера/определения JS, и мы могли показать модальное окно через showModal($HBS_FILE_NAME). Поскольку новый метод show() требует передачи компонента, мне нужно добавить это определение JS (правильно ли я предположил?).

Я добавил что-то вроде:

import Component from '@glimmer/component';

export default class SomeModal extends Component {

  constructor() {
    super(...arguments);
    console.log('Modal constructor')
  }
}

и поместил предыдущий файл .hbs (с необходимыми изменениями для DModal) в директорию /components/modal с тем же именем файла. При попытке отрисовать модальное окно (через getOwner(this).lookup("service:modal").show(SomeModal)), я вижу вывод конструктора в консоли, но модальное окно не отображается.

Нужна ли какая-либо другая конфигурация в определении контроллера/JS для этого изменения? Любые рекомендации будут очень кстати!

Вам это не нужно, если вы не добавляете код.

Достаточно иметь только файл .hbs.

Например, discourse-templates не имеет соответствующего JS-файла для модального шаблона Handlebars.

Вы адаптировали свой шаблон Handlebars в соответствии с инструкциями?

Есть ли какие-либо ошибки в консоли?

2 лайка

Спасибо за обратную связь! У меня большой :facepalm: — я переместил файлы в директорию .../discourse/templates/components/modal вместо .../discourse/components/modal. Всё работает как ожидалось (как с контроллером .js, так и без него), спасибо!

3 лайка

Не могли бы вы показать, как вызвать showModal() из скрипта внутри head_tag.html? В моём случае мне нужно использовать

document.querySelector(".actions .double-button .toggle-like");

чтобы перехватить событие клика, проверить условие и затем отобразить модальное окно.

1 лайк

Действительно, спасибо за усилия, которые вы приложили, чтобы так четко это задокументировать, Дэвид!

Мне почти удалось устранить устаревания для версии 3.2 за один день в нашем самом большом плагине.

3 лайка

Как теперь получить доступ к существующему модальному окну в ядре для его изменения?

Раньше я использовал этот способ (который больше не работает):
api.modifyClass("controller:poll-ui-builder", {

В данном конкретном случае имя этого класса, похоже, объявлено корректно и не изменилось.

2 лайка

В зависимости от того, что вам нужно изменить, я считаю, что лучшим решением будет использование PluginOutlet для внедрения вашего собственного кода или PluginOutlet Wrapper для замены или условного отображения базовой реализации. (Вы можете отправить PR для добавления outlet, если его нет.)

Если вы действительно хотите использовать modifyClass, это всё ещё возможно. Просто теперь модальное окно является компонентом и вложено в components/modal, поэтому обращаться к нему нужно так:

api.modifyClass("component:modal/poll-ui-builder", {
   pluginId: "your-custom-plugin-id",

   // вставьте свой код
});
4 лайка