将模态框从旧版控制器迁移到新的 DModal 组件 API

:information_source: 如果你正在实现一个新的 Modal,请查看此处的主文档。本主题描述了如何将现有的基于控制器(Controller)的 Modal 迁移到新的基于组件(Component)的 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 目录。这使得它们成为一个“共置组件”(colocated component),可以像任何其他 JS 模块一样被导入。

步骤 2:更新 JS 文件

然后,更新组件的 JS 定义,使其继承自 @ember/component 而不是 @ember/controller [1]。移除 ModalFunctionality mixin,并根据下表更新其函数的使用方式:

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

旧式的模态框控制器会“永远”存在,这意味着我们必须手动清理状态。使用新的基于组件的 API 后,组件将在模态框显示/隐藏时创建和销毁。在许多情况下,这意味着你旧的生命周期钩子不再需要。

如果你仍然需要一些基于生命周期的逻辑,请使用此表:

之前 之后
onShow() 使用标准的 Ember 组件生命周期(init() 或 Ember modifier)
afterRender 使用标准的 Ember 组件生命周期(init() 或 Ember modifier)
beforeClose() 创建一个包装器,包裹传递到你的组件中的 @closeModal 参数。将你的关闭包装器的引用传递给 DModal,例如 <DModal @closeModal={{this.myCloseModalWrapper}}>
onClose() 使用标准的 Ember 组件生命周期(willDestroy() 或 Ember modifier)

步骤 3:更新模板

<DModal> 替换 <DModalBody> 包装器。添加一些新的属性:

  • 传递新的 @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 调用点

以前,模态框是使用 showModal 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’ 服务(或者使用类似 getOwner(this).lookup("service:modal") 的方式访问它),然后调用 show() 函数。

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

不会返回组件实例的引用。相反,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 调用,并将你的模态框转换为 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 个赞