Конвертация модальных окон из легаси-контроллеров в новый API компонента DModal

:information_source: Если вы реализуете новый Modal, ознакомьтесь с основной документацией здесь. В этой теме описывается процесс миграции существующего 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. Это сделает их «ко-локализованным компонентом», который можно импортировать так же, как и любой другой 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>
    Привет, мир, это контент модального окна
  </:body>
  <:footer>
    Это контент нижнего колонтитула. Обертка `.modal-footer` будет добавлена
    автоматически
  </: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 лайка