将模态框从旧版控制器转换为新的 DModal 组件 API

:information_source: 如果您正在实现一个新的 Modal,请查看主文档 此处。该主题介绍了如何将现有的基于控制器的 Modal 迁移到新的基于组件的 API。

过去,Discourse 使用基于 Ember 控制器的 API 来渲染模态框。要调用模态框,您需要将包含控制器名称的字符串传递给 showModal()。在底层,这使用了 Ember 的 Route#renderTemplate API,该 API 在 Ember 3.x 中已被弃用,并将在 Ember 4.x 中被移除。

为了让 Discourse 能够升级到 Ember 4.x 及更高版本,我们引入了一个新的基于组件的模态框 API。这个新 API 采用了 Ember 的“声明式”设计模式,旨在提供清晰的 DDAU(数据向下,操作向上)语义。

步骤 1:移动文件

将控制器 JS 文件和模板文件移动到 /components/modal 目录。这将使它们成为“共置组件”,可以像任何其他 JS 模块一样被导入。

步骤 2:更新 JS 文件

然后,更新组件 JS 定义,使其从 @ember/component 而不是 @ember/controller 扩展 [1]。移除 ModalFunctionality 混入,并根据下表更新其函数的任何使用:

之前 之后
flash()clearFlash() 在您的组件中创建一个 flash 属性,并将其传递给 <DModal>@flash 参数。默认情况下,警报将使用 alert 类进行样式设置,该类是 ‘error’ 类的副本,但可以通过 @flashType 参数进行覆盖。
showModal() discourse/lib/show-modal 导入 showModal 函数
closeModal 操作 调用自动传递给您的组件的 closeModal 参数

旧式 Modal 控制器会“永久”存在,这意味着我们必须手动清理状态。使用新的基于组件的 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 调用位置

以前,模态框会使用 showModal API 进行渲染,该 API 接受一个字符串(控制器名称)和多个选项。它会返回一个控制器实例,可以对其进行操作:

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,您应该注入 ‘modal’ 服务(或使用类似 getOwner(this).lookup("service:modal") 的方式访问它),并调用 show() 函数。

show() 将新组件类的引用作为第一个参数。唯一仍然支持的选项是 ‘model’,可用于传递您的 Modal 所需的所有数据/操作。

不会返回对组件实例的引用。相反,show() 返回一个 Promise,该 Promise 将在模态框关闭时解析。Promise 将解析为传递给 @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 },
    });
  });
}

或者,迁移到 主 DModal 文档 中描述的声明式 API。

旧选项的功能可以按如下方式复制:

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 调用,并将您的 Modal 转换为 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 个赞

能否请您演示一下如何在 head_tag.html 文件中的脚本内调用 showModal()?在我的例子中,我需要使用

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

来捕获点击事件,检查条件,然后显示一个自定义模态框。

1 个赞

David,非常感谢你如此清晰地记录了这一切!

在一下午的时间里,我就已经基本清除了我们最大插件的 3.2 版本弃用项。

3 个赞

如何在新版核心中访问现有模态以对其进行修改?

过去我曾使用过(但现在已失效)此方法:
api.modifyClass("controller:poll-ui-builder", {

在这种特定情况下,该类名似乎已正确声明且未更改。

2 个赞

根据您需要修改的内容,我认为最好的解决方案是使用 PluginOutlet 来注入您的自定义代码,或者使用 PluginOutlet Wrapper 来替换/有条件地显示核心实现。(如果插座不可用,您可以提交 PR 来添加它)

如果您确实想使用 modifyClass,它仍然是可能的,只是模态框现在是一个组件,并且嵌套在 components/modal 中,因此您可以通过以下方式访问它:

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

  // 插入自定义代码
});
4 个赞