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

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

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

Discourse が Ember 4.x 以降にアップグレードできるようにするため、Modal 用の新しいコンポーネントベースの API を導入しました。この新しい API は Ember の「宣言的」な設計パターンを採用し、クリーンな DDAU(Data Down, Actions Up)セマンティクスを提供することを目的としています。

ステップ 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 引数を呼び出します

旧スタイルの Modal コントローラーは「永遠に」存在し続けたため、状態の手動クリーンアップが必要でした。新しいコンポーネントベースの API では、コンポーネントは Modal の表示/非表示時に作成・破棄されます。多くの場合、これは従来のライフサイクルフックが不要になることを意味します。

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

変更前 変更後
onShow() 標準的な Ember コンポーネントのライフサイクル(init() または Ember モディファイア)を使用します
afterRender 標準的な Ember コンポーネントのライフサイクル(init() または Ember モディファイア)を使用します
beforeClose() コンポーネントに渡される @closeModal 引数のラッパーを作成します。クローズラッパーの参照を DModal に渡します。例: <DModal @closeModal={{this.myCloseModalWrapper}}>
onClose() 標準的な Ember コンポーネントのライフサイクル(willDestroy() または Ember モディファイア)を使用します

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

<DModalBody> ラッパーを <DModal> に置き換えます。いくつかの新しい属性を追加します:

  • 新しい @closeModal 引数をそのまま渡します
  • 明示的なクラスを追加します。旧来の動作と一致させるには、コントローラーのファイル名に -modal を追加します。

例えば、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 の呼び出し箇所を更新

以前は、Modal は 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 をレンダリングするには、「modal」サービス(または getOwner(this).lookup("service:modal") のような方法でアクセスする)をインジェクションし、show() 関数を呼び出す必要があります。

show() は、新しいコンポーネントクラスの参照を最初の引数として受け取ります。サポートされる唯一のオプションは ‘model’ で、Modal に必要なすべてのデータ/アクションを渡すために使用できます。

コンポーネントインスタンスの参照は返されません。代わりに、show() は Modal がクローズされたときに解決されるプロミスを返します。プロミスは、@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: テスト

テストは基本的に同じままで大丈夫です。最も一般的な問題は以下の通りです:

  • Modal はもはや名前に基づくデフォルトのクラスを持たなくなりました。クラスはテンプレート内で明示的に指定する必要があります(ステップ 3 の冒頭参照)

  • Modal がクローズされたとき、d-modal ラッパーはもはや DOM に残らなくなりました。すべての Modal がクローズされていることを確認するには、assert.dom('.d-modal').doesNotExist() のようなチェックを使用してください

完了!

これにより、Modal は以前と同じように動作するはずです。新しい API の利点をさらに活かすには、showModal の呼び出しを宣言的戦略に置き換える こと、および Modal を Glimmer コンポーネントに変換することを検討してください。

以下は、Discourse コアの一部の Modal を新しい API に変換するデモとなるコミットの例です:


このドキュメントはバージョン管理されています - 変更提案は GitHub でお願いします。


  1. このガイドでは、Ember コントローラーからの最も簡単な移行パスを提供するため、Classic Ember Components(クラシック Ember コンポーネント)が推奨されています。ただし、シンプルな Modal の場合、またはリファクタリングに時間を費やすことに抵抗がない場合は、モダンな 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