レガシーコントローラーから新しいDModalコンポーネントAPIへのモーダルの変換

:information_source: 新しいモーダルを実装する場合は、メインのドキュメントをこちらでご覧ください。このトピックでは、既存のコントローラーベースのモーダルを新しいコンポーネントベースの API に移行する方法について説明しています。

過去、Discourse ではモーダルをレンダリングするために Ember-Controller ベースの API を使用していました。モーダルを呼び出すには、コントローラーの名前を含む文字列を showModal() に渡す必要がありました。内部では、これは Ember の Route#renderTemplate API を使用しており、Ember 3.x で非推奨となり、Ember 4.x で削除されます。

Discourse が Ember 4.x 以降にアップグレードできるようにするため、モーダル用の新しいコンポーネントベースの API を導入しました。この新しい API は Ember の「宣言的」デザインパターンを採用し、クリーンな DDAU(データは下から、アクションは上へ)セマンティクスを提供することを目指しています。

ステップ 1: ファイルの移動

コントローラーの JS ファイルとテンプレートファイルを /components/modal ディレクトリに移動します。これにより、これらは「コロケートされたコンポーネント」となり、他の JS モジュールと同様にインポートできるようになります。

ステップ 2: JS ファイルの更新

次に、コンポーネントの JS 定義を @ember/controller ではなく @ember/component から拡張するように更新します [1]ModalFunctionality ミックスインを削除し、その関数の使用を以下の表に従って更新します。

以前
flash() および clearFlash() コンポーネント内に flash プロパティを作成し、<DModal>@flash 引数に渡します。デフォルトではアラートは alert クラス(error クラスのコピー)でスタイルされますが、@flashType 引数を使用して上書きできます。
showModal() discourse/lib/show-modal から showModal 関数をインポートします
closeModal アクション コンポーネントに自動的に渡される closeModal 引数を呼び出します

旧式のモーダルコントローラーは「永久」に存在するため、状態のクリーンアップを手動で行う必要がありました。新しいコンポーネントベースの API では、モーダルが表示/非表示になるときにコンポーネントが作成され、破棄されます。多くの場合、これは古いライフサイクルフックが不要になったことを意味します。

まだライフサイクルベースのロジックが必要な場合は、以下の表を使用してください。

以前
onShow() 標準的な Ember コンポーネントのライフサイクル(init() または Ember 修飾子)を使用します
afterRender 標準的な Ember コンポーネントのライフサイクル(init() または Ember 修飾子)を使用します
beforeClose() コンポーネントに渡される @closeModal 引数を取り巻くラッパーを作成します。クローズラッパーへの参照を DModal\u003cDModal @closeModal={{this.myCloseModalWrapper}}\u003e のように渡します。
onClose() 標準的な Ember コンポーネントのライフサイクル(willDestroy() または Ember 修飾子)を使用します

ステップ 3: テンプレートの更新

\u003cDModalBody\u003e ラッパーを \u003cDModal\u003e に置き換えます。いくつかの新しい属性を追加します。

  • 新しい @closeModal 引数を渡す
  • 明示的なクラスを追加する。以前の動作に合わせるには、コントローラーのファイル名に -modal を追加します。

例えば、モーダルコントローラーが close-topic.js と呼ばれていた場合、新しい \u003cDModal\u003e の呼び出しは以下のようになります。

\u003cDModal @closeModal={{@closeModal}} class="close-topic-modal"\u003e

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 属性で角括ら構文を使用:\u003cDModal class="blah"\u003e
@titleAriaElementId 通常の HTML 属性で角括ら構文を使用:\u003cDModal aria-labelledby="blah"\u003e
@dismissable, @submitOnEnter, @headerClass 変更なし

旧式の \u003cDModalBody\u003e コンポーネントの後にフッターコンテンツがレンダリングされていた場合は、新しい \u003c:footer\u003e 名前付きブロックを使用して、\u003cDModal\u003e 内で導入します。名前付きブロックを使用する場合は、ボディコンテンツを \u003c:body\u003e\u003c/:body\u003e で囲む必要があります。例:

\u003cDModal @closeModal={{@closeModal}}\u003e
  \u003c:body\u003e
    こんにちは、これがモーダルのコンテンツです
  \u003c/:body\u003e
  \u003c:footer\u003e
    これがフッターコンテンツです。`.modal-footer` ラッパーが自動的に追加されます
  \u003c/:footer\u003e
\u003c/DModal\u003e

ステップ 4: showModal 呼び出し箇所の更新

以前、モーダルは showModal API を使用してレンダリングされていました。この API は文字列(コントローラー名)と複数の opts を受け取り、コントローラーのインスタンスを返して操作できました。

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() は最初の引数として新しいコンポーネントクラスへの参照を受け取ります。現在サポートされている opt は 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 },
    });
  });
}

または、メインの DModal ドキュメント で説明されている宣言的 API に移行することもできます。

古いオプションの機能は以下のように再現できます。

古い showModal opt 解決策
admin コンポーネントでは該当なし - 削除
templateName コンポーネントでは該当なし - 削除
title \u003cDModal @title={{i18n "blah"}}\u003e に移動
titleTranslated \u003cDModal @title="blah"\u003e に移動。必要に応じて model から取得したデータに基づいて計算することもできます
modalClass \u003cDModal class="blah"\u003e に移動
titleAriaElementId \u003cDModal aria-labelledby="blah"\u003e に移動
panels \u003c:headerBelowTitle\u003e 名前付きブロックを使用して、コンポーネント内でタブを実装する(
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

チュートリアルありがとうございます!例を見るのは非常に役立ちました。カスタムプラグインのモーダルが壊れていたのを1時間で修正できました。

「いいね!」 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 には、モーダル ハンドルバー テンプレートに対応する JavaScript ファイルがありません。

指示に従ってハンドルバー テンプレートを適応させましたか?

コンソールにエラーはありますか?

「いいね!」 2

フィードバックありがとうございます!私の大きな:facepalm:、ファイルを .../discourse/components/modal ディレクトリの代わりに .../discourse/templates/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