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

:information_source: Se stai implementando un nuovo Modal, consulta la 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 Ember-Controller per il rendering dei modali. Per invocare il modal, si passava una stringa contenente il nome del controller a showModal(). Sotto il cofano, questo faceva uso dell’API Route#renderTemplate di Ember, che è deprecata in Ember 3.x e verrà rimossa in Ember 4.x.

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

Passo 1: Sposta i file

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

Passo 2: Aggiorna il file JS

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

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 utilizzando l’argomento @flashType.
showModal() Importa la funzione showModal da discourse/lib/show-modal
Azione closeModal Invoca l’argomento closeModal che viene automaticamente passato al tuo componente

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

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

Prima Dopo
onShow() Usa il ciclo di vita standard dei componenti Ember (init() o un modificatore Ember)
afterRender Usa il ciclo di vita standard dei componenti Ember (init() o un 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 un modificatore Ember)

Passo 3: Aggiorna il template

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

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

Ad esempio, se il tuo controller modal si chiamava close-topic.js, la nuova invocazione di <DModal> sarebbe simile a questa:

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

Se l’invocazione di DModalBody include altri argomenti, aggiornali in base alla tabella sottostante:

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

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

<DModal @closeModal={{@closeModal}}>
  <:body>
    Ciao mondo, questo è il contenuto del modal
  </:body>
  <:footer>
    Questo è il contenuto del footer. Un wrapper `.modal-footer` verrà aggiunto
    automaticamente
  </:footer>
</DModal>

Passo 4: Aggiorna i punti di chiamata di showModal

In precedenza, i modali venivano renderizzati utilizzando l’API showModal, che richiedeva una stringa (il nome del controller) e diversi parametri opzionali. Restituiva 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 utilizzando qualcosa come getOwner(this).lookup("service:modal")) e chiamare la funzione show().

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

Non verrà restituita alcuna riferimento all’istanza del componente. Invece, show() restituisce una promise che si risolve quando il modal viene chiuso. La promise si risolverà con i dati passati 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/a per i componenti - rimuovilo
templateName n/a per i componenti - rimuovilo
title sposta a <DModal @title={{i18n "blah"}}>
titleTranslated sposta a <DModal @title="blah">. Questo potrebbe essere calcolato in base ai dati di model se necessario
modalClass sposta a <DModal class="blah">
titleAriaElementId sposta a <DModal aria-labelledby="blah">
panels Usa il blocco denominato :headerBelowTitle per implementare le schede nel tuo componente (esempio)
model invariato

Passo 5: Test

Qualsiasi test dovrebbe rimanere sostanzialmente invariato. I problemi più comuni sono:

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

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

Profitto!

Il tuo modal dovrebbe ora funzionare come prima. Per sfruttare ulteriormente la nuova API, potresti voler considerare 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 modali del core di Discourse alla nuova API:


{small}Questo documento è sottoposto a controllo di versione - suggerisci modifiche su github.{/small}


  1. I Classic Ember Components sono raccomandati in questa guida perché offrono il percorso di migrazione più semplice dai Controller Ember. Tuttavia, per modali semplici, o se sei disposto a dedicare del tempo alla refattorizzazione, 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