Modals von Legacy-Controllern auf die neue DModal-Komponenten-API umstellen

:information_source: Wenn du ein neues Modal implementierst, findest du die Hauptdokumentation hier. Dieses Thema beschreibt, wie man ein bestehendes, auf Controllern basierendes Modal auf die neue, auf Komponenten basierende API migriert.

In der Vergangenheit verwendete Discourse eine auf Ember-Controllern basierende API zum Rendern von Modals. Um das Modal aufzurufen, wurde eine Zeichenkette mit dem Namen des Controllers an showModal() übergeben. Im Hintergrund nutzte dies die Route#renderTemplate-API von Ember, die in Ember 3.x veraltet ist und in Ember 4.x entfernt wird.

Um es Discourse zu ermöglichen, auf Ember 4.x und darüber hinaus zu aktualisieren, haben wir eine neue, auf Komponenten basierende API für Modals eingeführt. Diese neue API nutzt die „deklarativen“ Designmuster von Ember und zielt darauf ab, saubere DDAU-Semantiken (Data Down, Actions Up) bereitzustellen.

Schritt 1: Dateien verschieben

Verschiebe die Controller-JS-Datei und die Template-Datei in das Verzeichnis /components/modal. Dadurch werden sie zu einer „colocated component“ (zusammengefügten Komponente), die wie jedes andere JS-Modul importiert werden kann.

Schritt 2: Die JS-Datei aktualisieren

Aktualisiere dann die JS-Definition der Komponente, so dass sie von @ember/component statt von @ember/controller erbt [1]. Entferne das ModalFunctionality-Mixin und aktualisiere die Verwendung seiner Funktionen gemäß der folgenden Tabelle:

Vorher Nachher
flash() und clearFlash() Erstelle eine flash-Eigenschaft in deiner Komponente und übergebe sie an das @flash-Argument von <DModal>. Standardmäßig wird die Warnung mit der alert-Klasse gestylt, die eine Kopie der 'error'-Klasse ist, aber sie kann über das @flashType-Argument überschrieben werden.
showModal() Importiere die showModal-Funktion aus discourse/lib/show-modal
closeModal-Aktion Rufe das closeModal-Argument auf, das automatisch in deine Komponente übergeben wird

Altmodische Modal-Controller lebten „ewig“, was bedeutete, dass wir den Zustand manuell aufräumen mussten. Mit der neuen, auf Komponenten basierenden API wird die Komponente erstellt und zerstört, wenn das Modal ein-/ausgeblendet wird. In vielen Fällen bedeutet das, dass deine alten Lifecycle-Hooks nicht mehr erforderlich sind.

Falls du immer noch auf Lifecycle-basierte Logik angewiesen bist, verwende diese Tabelle:

Vorher Nachher
onShow() Verwende den Standard-Ember-Komponenten-Lifecycle (init() oder Ember-Modifier)
afterRender Verwende den Standard-Ember-Komponenten-Lifecycle (init() oder Ember-Modifier)
beforeClose() Erstelle eine Hülle (Wrapper) um das @closeModal-Argument, das in deine Komponente übergeben wird. Übergebe eine Referenz zu deiner Close-Hülle an DModal wie <DModal @closeModal={{this.myCloseModalWrapper}}>
onClose() Verwende den Standard-Ember-Komponenten-Lifecycle (willDestroy() oder Ember-Modifier)

Schritt 3: Das Template aktualisieren

Ersetze den <DModalBody>-Wrapper durch <DModal>. Füge einige neue Attribute hinzu:

  • Gib das neue @closeModal-Argument weiter
  • Füge eine explizite Klasse hinzu. Um das alte Verhalten zu entsprechen, nimm deinen Controller-Dateinamen und füge -modal hinzu.

Zum Beispiel, wenn dein Modal-Controller close-topic.js hieß, würde der neue <DModal>-Aufruf ungefähr so aussehen:

<DModal @closeModal={{@closeModal}} class="close-topic-modal">

Wenn der DModalBody-Aufruf andere Argumente enthält, aktualisiere sie basierend auf der folgenden Tabelle:

Vorher Nachher
@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 Verwende die Winkelklammer-Syntax mit regulärem HTML-Attribut: <DModal class="blah">
@titleAriaElementId Verwende die Winkelklammer-Syntax mit regulärem HTML-Attribut: <DModal aria-labelledby="blah">
@dismissable, @submitOnEnter, @headerClass Unverändert

Falls nach dem alten <DModalBody>-Komponenten Footer-Inhalte gerendert wurden, verwende den neuen <:footer>-Named Block, um sie innerhalb von <DModal> einzuführen. Bei der Verwendung von Named Blocks sollte der Body-Inhalt in <:body></:body> gekapselt werden. Zum Beispiel:

<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>

Schritt 4: Die showModal-Aufrufstellen aktualisieren

Früher wurden Modals mit der showModal-API gerendert, die eine Zeichenkette (den Controller-Namen) und eine Reihe von Optionen annahm. Sie gab eine Instanz des Controllers zurück, die manipuliert werden konnte:

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);
  });
}

Um neue, auf Komponenten basierende Modals zu rendern, solltest du den „modal“-Service injizieren (oder darauf mit etwas wie getOwner(this).lookup("service:modal") zugreifen) und die show()-Funktion aufrufen.

show() nimmt eine Referenz auf die neue Klassenkomponente als erstes Argument entgegen. Die einzige noch unterstützte Option ist „model“, die verwendet werden kann, um alle für dein Modal erforderlichen Daten/Aktionen zu übergeben.

Es wird keine Referenz auf die Komponenteninstanz zurückgegeben. Stattdessen gibt show() eine Promise zurück, die aufgelöst wird, wenn das Modal geschlossen ist. Die Promise wird mit allen Daten aufgelöst, die an @closeModal übergeben wurden.

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 },
    });
  });
}

Alternativ kannst du auf die deklarativen API migrieren, die in der Haupt-Dokumentation zu DModal beschrieben ist.

Die Funktionalität der alten Optionen kann wie folgt repliziert werden:

Alte showModal-Option Lösung
admin Nicht anwendbar für Komponenten – entferne sie
templateName Nicht anwendbar für Komponenten – entferne sie
title Verschiebe zu <DModal @title={{i18n "blah"}}>
titleTranslated Verschiebe zu <DModal @title="blah">. Dies kann bei Bedarf basierend auf Daten aus model berechnet werden
modalClass Verschiebe zu <DModal class="blah">
titleAriaElementId Verschiebe zu <DModal aria-labelledby="blah">
panels Verwende den <:headerBelowTitle>-Named Block, um Tabs in deiner Komponente zu implementieren (Beispiel)
model Unverändert

Schritt 5: Tests

Die meisten Tests sollten weitgehend unverändert bleiben. Die häufigsten Probleme sind:

  • Modals haben keine Standardklasse mehr, die auf ihrem Namen basiert. Klassen müssen im Template explizit angegeben werden (siehe Anfang von Schritt 3)

  • Der d-modal-Wrapper bleibt nicht mehr im DOM erhalten, wenn das Modal geschlossen ist. Um zu prüfen, ob alle Modals geschlossen sind, verwende eine Prüfung wie assert.dom('.d-modal').doesNotExist()

Profit!

Dein Modal sollte jetzt so funktionieren wie zuvor. Um den neuen API noch weiter zu nutzen, solltest du in Betracht ziehen, showModal-Aufrufe durch eine deklarative Strategie zu ersetzen und dein Modal in eine Glimmer-Komponente umzuwandeln.

Beispiele

Hier sind einige Beispiel-Commits, die die Konvertierung einiger der Discourse-Core-Modals auf die neue API demonstrieren:


Dieses Dokument wird versioniert verwaltet – schlage Änderungen auf GitHub vor.


  1. In diesem Leitfaden werden klassische Ember-Komponenten empfohlen, da sie den einfachsten Migrationspfad von Ember-Controllern boten. Für einfache Modals oder wenn du bereit bist, etwas Zeit für Refactoring aufzuwenden, sind moderne Glimmer-Komponenten jedoch die bessere Wahl. ↩︎

20 „Gefällt mir“

Das sieht wirklich großartig aus. Es gibt mir Hoffnung, dass ich meine Modals auf Ember 4 umstellen kann. Ich verstehe den Ember-Code, den ich schreibe, nur kaum, daher ist es nicht einfach, Dokumentationen zu schreiben, die ich verstehen kann. Vielen Dank dafür.

8 „Gefällt mir“

Danke für das Tutorial! Die Beispiele waren sehr hilfreich. Ich konnte mein benutzerdefiniertes Plugin-Modal in einer Stunde reparieren.

4 „Gefällt mir“

Ich arbeite gerade an dieser Konvertierung, stoße aber auf ein Problem:

Zuvor hatte unser Modal keine entsprechende Controller-/JS-Definition und wir konnten das Modal über showModal($HBS_FILE_NAME) anzeigen. Da das neue show() eine Komponente benötigt, muss ich diese JS-Definition einführen (ist das eine korrekte Annahme?).

Ich habe etwas hinzugefügt wie:

import Component from '@glimmer/component';

export default class SomeModal extends Component {

  constructor() {
    super(...arguments);
    console.log('Modal constructor')
  }
}

und die vorherige .hbs-Datei (mit erforderlichen Änderungen an DModal) sowohl im Verzeichnis /components/modal als auch mit demselben Dateinamen. Wenn ich versuche, das Modal zu rendern (über getOwner(this).lookup("service:modal").show(SomeModal)), sehe ich, dass mein Konstruktor-Log in der Konsole ausgegeben wird, aber das Modal wird nicht gerendert.

Ist eine weitere Konfiguration im Controller/in der JS-Definition für diese Änderung erforderlich? Jeder Hinweis wäre sehr willkommen!

Sie benötigen sie nicht, wenn Sie keinen Code hinzufügen.

Sie können einfach die .hbs-Datei haben.

discourse-templates zum Beispiel hat keine entsprechende JS-Datei für die Modal-Handlebars-Vorlage.

Haben Sie Ihre Handlebars-Vorlage gemäß den Anweisungen angepasst?

Gibt es Fehler in der Konsole?

2 „Gefällt mir“

Danke für das Feedback! Riesiges :facepalm: von meiner Seite, ich hatte die Dateien in das Verzeichnis .../discourse/templates/components/modal verschoben, anstatt nach .../discourse/components/modal. Jetzt funktioniert alles wie erwartet (mit oder ohne den .js-Controller), danke!

3 „Gefällt mir“

Könnten Sie mir zeigen, wie ich showModal() aus einem Skript in einer head_tag.html-Datei aufrufen kann? In meinem Fall muss ich verwenden

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

um das Klickereignis abzufangen, die Bedingung zu prüfen und dann ein benutzerdefiniertes Modal anzuzeigen.

1 „Gefällt mir“

Ich weiß die Mühe, die du dir hier gemacht hast, um das so klar zu dokumentieren, David, wirklich zu schätzen!

Ich habe es fast geschafft, die Deprecations für 3.2 an einem Nachmittag für unser größtes Plugin zu beheben.

3 „Gefällt mir“

Wie greift man jetzt auf ein vorhandenes Modal in Core zu, um es zu ändern?

In der Vergangenheit habe ich dies verwendet (was nicht mehr funktioniert):
api.modifyClass("controller:poll-ui-builder", {

In diesem speziellen Fall scheint der Klassenname gut deklariert und unverändert zu sein.

2 „Gefällt mir“

Je nachdem, was Sie ändern müssen, denke ich, dass die beste Lösung darin besteht, ein PluginOutlet zu verwenden, um Ihren benutzerdefinierten Code einzufügen, oder einen PluginOutlet Wrapper, um die Kernimplementierung zu ersetzen/bedingt anzuzeigen. (Sie können einen PR erstellen, um ein Outlet hinzuzufügen, wenn es nicht verfügbar ist)

Wenn Sie modifyClass wirklich verwenden möchten, sollte dies immer noch möglich sein. Es ist nur so, dass das Modal jetzt eine Komponente ist und in components/modal verschachtelt ist, sodass Sie darauf wie folgt zugreifen würden:

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

   // benutzerdefinierten Code einfügen
});
4 „Gefällt mir“