Conversione dei modali dai controller legacy alla nuova API del componente DModal

:information_source: Se stai implementando un nuovo Modal, dai un’occhiata alla documentazione principale qui. Questo argomento descrive come migrare un Modal basato su controller esistente alla nuova API basata su Componenti.

In passato, Discourse utilizzava un’API basata su Controller Ember per il rendering dei modali. Per richiamare il modale, si passava una stringa con il nome del controller a showModal(). Sotto la superficie, questo faceva uso dell’API Route#renderTemplate di Ember, che è deprecata in Ember 3.x e verrà rimossa in Ember 4.x.

Per consentire a Discourse di aggiornarsi a Ember 4.x e oltre, abbiamo introdotto una nuova API basata su componenti per i modali. Questa nuova API abbraccia i pattern di progettazione ‘dichiarativi’ di Ember e mira a fornire una semantica pulita DDAU (data down actions up).

Passaggio 1: Spostare i file

Sposta il file JS del controller e il file del template nella directory /components/modal. Questo li rende un ‘componente co-locato’ che può essere importato come qualsiasi altro modulo JS.

Passaggio 2: Aggiornare il file JS

Quindi, aggiorna la definizione JS del componente per estendere @ember/component invece di @ember/controller [1]. Rimuovi il mixin ModalFunctionality e aggiorna qualsiasi utilizzo delle sue funzioni secondo la tabella seguente:

Prima Dopo
flash() e clearFlash() Crea una proprietà flash nel tuo componente e passala all’argomento @flash di <DModal>. Di default l’avviso verrà stilizzato con la classe alert che è una copia della classe ‘error’, ma può essere sovrascritta usando l’argomento @flashType.
showModal() Importa la funzione showModal da discourse/lib/show-modal
azione closeModal Richiama l’argomento closeModal che viene passato automaticamente al tuo componente

I Controller Modal di vecchio stile vivevano ‘per sempre’, il che significava che dovevamo pulire manualmente lo stato. Con la nuova API basata su Componenti, il componente verrà creato e distrutto quando il modale viene mostrato/nascosto. In molti casi ciò significa che i vecchi hook di ciclo di vita non sono più necessari.

Se hai ancora bisogno di logica basata sul ciclo di vita, usa questa tabella:

Prima Dopo
onShow() Usa il ciclo di vita standard dei componenti Ember (init() o modificatore Ember)
afterRender Usa il ciclo di vita standard dei componenti Ember (init() o modificatore Ember)
beforeClose() Crea un wrapper attorno all’argomento @closeModal che viene passato al tuo componente. Passa un riferimento al tuo wrapper di chiusura a DModal come <DModal @closeModal={{this.myCloseModalWrapper}}>
onClose() Usa il ciclo di vita standard dei componenti Ember (willDestroy() o modificatore Ember)

Passaggio 3: Aggiornare il Template

Sostituisci il wrapper <DModalBody> con <DModal>. Aggiungi alcuni nuovi attributi:

  • Passa attraverso l’argomento @closeModal nuovo
  • Aggiungi una classe esplicita. Per corrispondere al vecchio comportamento, prendi il nome del file del controller e aggiungi -modal.

Ad esempio, se il tuo controller modale si chiamava close-topic.js, la nuova chiamata a <DModal> avrebbe un aspetto simile a questo:

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

Se la chiamata a DModalBody include altri argomenti, aggiornali in base alla tabella seguente:

Prima Dopo
@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 Usa la sintassi con parentesi angolari con attributo html regolare: <DModal class="blah">
@titleAriaElementId Usa la sintassi con parentesi angolari con attributo html regolare: <DModal aria-labelledby="blah">
@dismissable, @submitOnEnter, @headerClass Invariato

Se c’era contenuto footer renderizzato dopo il vecchio componente <DModalBody>, usa il nuovo blocco nominato <:footer> per introdurlo all’interno di <DModal>. Quando si usano blocchi nominati, il contenuto del corpo deve essere avvolto in <:body></:body>. Ad esempio:

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

Passaggio 4: Aggiornare i punti di chiamata di showModal

In precedenza, i modali venivano renderizzati usando l’API showModal, che prendeva una stringa (il nome del controller) e una serie di opzioni. Ritornava un’istanza del controller che poteva essere manipolata:

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

Per renderizzare i nuovi Modali basati su componenti, dovresti iniettare il servizio ‘modal’ (o accedervi usando qualcosa come getOwner(this).lookup("service:modal")) e chiamare la funzione show().

show() prende un riferimento alla nuova classe Component come primo argomento. L’unica opzione ancora supportata è ‘model’, che può essere usata per passare tutti i dati/azioni richiesti per il tuo Modal.

Non verrà restituito alcun riferimento all’istanza del componente. Invece, show() restituisce una promise che verrà risolta quando il modale viene chiuso. La promise verrà risolta con qualsiasi dato che è stato passato a @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 },
    });
  });
}

In alternativa, migra all’API dichiarativa descritta nella documentazione principale di DModal.

La funzionalità delle vecchie opzioni può essere replicata come segue:

Vecchia opzione showModal Soluzione
admin n/d per componente - rimuovilo
templateName n/d per componenti - rimuovilo
title sposta in <DModal @title={{i18n "blah"}}>
titleTranslated sposta in <DModal @title="blah">. Questo potrebbe essere calcolato in base ai dati da model se necessario
modalClass sposta in <DModal class="blah">
titleAriaElementId sposta in <DModal aria-labelledby="blah">
panels Usa il blocco nominato <:headerBelowTitle> per implementare le tab nel tuo componente (esempio)
model invariato

Passaggio 5: Test

I test dovrebbero in gran parte rimanere gli stessi. Il problema più comune è:

  • I Modali non hanno più una classe predefinita basata sul loro nome. Le classi devono essere specificate esplicitamente nel template (vedi inizio del Passaggio 3)

  • Il wrapper d-modal non persiste più nel DOM quando il modale viene chiuso. Per verificare che tutti i modali siano chiusi, usa un controllo come assert.dom('.d-modal').doesNotExist()

Profitto!

Il tuo modale dovrebbe ora funzionare come prima. Per sfruttare ulteriormente la nuova API, potresti voler considerare di sostituire le chiamate a showModal con una strategia dichiarativa, e convertire il tuo Modal in un componente Glimmer.

Esempi

Ecco alcuni commit di esempio che dimostrano la conversione di alcuni dei modali core di Discourse alla nuova API:


Questo documento è sotto controllo di versione - suggerisci modifiche su github.


  1. I Classic Ember Components sono raccomandati in questa guida perché hanno fornito il percorso di migrazione più semplice dai Controller Ember. Ma per modali semplici, o se sei disposto a dedicare del tempo al refactoring, i moderni componenti Glimmer sono la scelta migliore. ↩︎

20 Mi Piace

Sembra davvero fantastico. Mi dà speranza che io possa convertire le mie modali in ember 4. Capisco a malapena il codice ember che scrivo, quindi scrivere documentazione che posso capire non è facile. Grazie mille per questo.

8 Mi Piace

Grazie per il tutorial! Guardare gli esempi è stato molto utile. Sono riuscito a riparare la modale del mio plugin personalizzato che era rotta in un’ora.

4 Mi Piace

Ci sto lavorando in questo momento, ma ho riscontrato un problema:

In precedenza, la nostra modale non aveva un controller/definizione JS corrispondente e siamo stati in grado di mostrare la modale tramite showModal($HBS_FILE_NAME). Poiché il nuovo show() richiede il passaggio di un componente, devo introdurre questa definizione JS (questa è un’ipotesi corretta?).

Ho aggiunto qualcosa di simile:

import Component from '@glimmer/component';

export default class SomeModal extends Component {

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

e ho il file .hbs precedente (con le modifiche richieste a DModal) entrambi nella directory /components/modal con lo stesso nome file. Quando tento di renderizzare la modale (tramite getOwner(this).lookup("service:modal").show(SomeModal)), vedo il mio log del costruttore stampato nella console, ma la modale non viene renderizzata.

È necessaria qualche altra configurazione nel controller/definizione JS per questa modifica? Qualsiasi indicazione sarebbe molto apprezzata!

Non ne hai bisogno se non stai aggiungendo codice.

Puoi avere solo il file .hbs.

Ad esempio, discourse-templates non ha un file JS corrispondente per il template handlebars della modale.

Hai adattato il tuo template handlebars seguendo le istruzioni?

Ci sono errori nella console?

2 Mi Piace

Grazie per il feedback! Enorme :facepalm: da parte mia, avevo spostato i file nella directory .../discourse/templates/components/modal, invece che in .../discourse/components/modal. Ora funziona come previsto (con o senza il controller .js), grazie!

3 Mi Piace

Potresti mostrarmi come chiamare showModal() da uno script all’interno di head_tag.html? Nel mio caso, ho bisogno di usare

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

per catturare l’evento click, controllare la condizione e quindi mostrare una modale personalizzata.

1 Mi Piace

Apprezzo molto l’impegno che hai dedicato a documentare questo in modo così chiaro, David!

Sono quasi riuscito a eliminare le deprecazioni per la 3.2 in un pomeriggio sul nostro plugin più grande.

3 Mi Piace

Come si accede ora a una modale esistente nel core per modificarla?

In passato ho usato questo (che non funziona più):
api.modifyClass("controller:poll-ui-builder", {

In questo caso particolare, il nome della classe sembra essere dichiarato correttamente e non è cambiato.

2 Mi Piace

A seconda di ciò che devi modificare, penso che la soluzione migliore sarebbe utilizzare un PluginOutlet per inserire il tuo codice personalizzato, o un PluginOutlet Wrapper per sostituire/mostrare condizionalmente l’implementazione principale. (Puoi inviare una PR per aggiungere un outlet se non è disponibile)

Se vuoi davvero usare modifyClass, dovrebbe essere ancora possibile, è solo che la modale è ora un componente ed è nidificata in components/modal, quindi vi accederesti come:

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

   // inserisci codice personalizzato
});
4 Mi Piace