Conversión de modales de controladores heredados a la nueva API de componentes DModal

:information_source: Si estás implementando un nuevo Modal, consulta la documentación principal aquí. Este tema describe cómo migrar un Modal basado en controladores existente a la nueva API basada en componentes.

En el pasado, Discourse utilizaba una API basada en Ember-Controller para renderizar modales. Para invocar el modal, pasabas una cadena con el nombre del controlador a showModal(). Internamente, esto hacía uso de la API Route#renderTemplate de Ember, que está obsoleta en Ember 3.x y será eliminada en Ember 4.x.

Para permitir que Discourse se actualice a Ember 4.x y versiones posteriores, hemos introducido una nueva API basada en componentes para los modales. Esta nueva API adopta los patrones de diseño ‘declarativos’ de Ember y tiene como objetivo proporcionar una semántica limpia de DDAU (datos hacia abajo, acciones hacia arriba).

Paso 1: Mover archivos

Mueve el archivo JS del controlador y el archivo de plantilla al directorio /components/modal. Esto los convierte en un ‘componente co-ubicado’, que se puede importar como cualquier otro módulo JS.

Paso 2: Actualizar el archivo JS

Luego, actualiza la definición del componente JS para que extienda de @ember/component en lugar de @ember/controller [1]. Elimina el mixin ModalFunctionality y actualiza cualquier uso de sus funciones según la tabla siguiente:

Antes Después
flash() y clearFlash() Crea una propiedad flash en tu componente y pásala al argumento @flash de <DModal>. Por defecto, la alerta se estilizará con la clase alert, que es una copia de la clase ‘error’, pero puede sobrescribirse usando el argumento @flashType.
showModal() Importa la función showModal desde discourse/lib/show-modal
acción closeModal Invoca el argumento closeModal, que se pasa automáticamente a tu componente

Los controladores de Modal de estilo antiguo vivían ‘para siempre’, lo que significaba que teníamos que limpiar manualmente el estado. Con la nueva API basada en componentes, el componente se crea y destruye cuando el modal se muestra/oculta. En muchos casos, esto significa que ya no se requieren tus hooks de ciclo de vida antiguos.

Si aún necesitas alguna lógica basada en el ciclo de vida, usa esta tabla:

Antes Después
onShow() Usa el ciclo de vida estándar de los componentes de Ember (init() o modificador de Ember)
afterRender Usa el ciclo de vida estándar de los componentes de Ember (init() o modificador de Ember)
beforeClose() Crea un envoltorio alrededor del argumento @closeModal que se pasa a tu componente. Pasa una referencia a tu envoltorio de cierre a DModal como <DModal @closeModal={{this.myCloseModalWrapper}}>
onClose() Usa el ciclo de vida estándar de los componentes de Ember (willDestroy() o modificador de Ember)

Paso 3: Actualizar la plantilla

Reemplaza el envoltorio <DModalBody> con <DModal>. Añade algunos atributos nuevos:

  • Pasa el nuevo argumento @closeModal
  • Añade una clase explícita. Para igualar el comportamiento anterior, toma el nombre de archivo de tu controlador y añade -modal.

Por ejemplo, si tu controlador de modal se llamaba close-topic.js, la nueva invocación de <DModal> se vería algo así:

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

Si la invocación de DModalBody incluye otros argumentos, actualízalos según la tabla siguiente:

Antes Después
@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 sintaxis de corchetes angulares con un atributo HTML regular: <DModal class="blah">
@titleAriaElementId Usa la sintaxis de corchetes angulares con un atributo HTML regular: <DModal aria-labelledby="blah">
@dismissable, @submitOnEnter, @headerClass Sin cambios

Si había algún contenido de pie de página renderizado después del antiguo componente <DModalBody>, usa el nuevo bloque nombrado :footer para introducirlo dentro de <DModal>. Al usar cualquier bloque nombrado, el contenido del cuerpo debe estar envuelto en <:body></:body>. Por ejemplo:

<DModal @closeModal={{@closeModal}}>
  <:body>
    Hola mundo, este es el contenido del modal
  </:body>
  <:footer>
    Este es el contenido del pie de página. Se añadirá automáticamente un envoltorio `.modal-footer`
  </:footer>
</DModal>

Paso 4: Actualizar los sitios de llamada a showModal

Anteriormente, los modales se renderizaban usando la API showModal, que aceptaba una cadena (el nombre del controlador) y varias opciones. Devolvía una instancia del controlador que podía manipularse:

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

Para renderizar nuevos Modales basados en componentes, debes inyectar el servicio ‘modal’ (o acceder a él usando algo como getOwner(this).lookup("service:modal")) y llamar a la función show().

show() toma una referencia a la nueva clase de componente como primer argumento. La única opción aún soportada es ‘model’, que se puede usar para pasar todos los datos/acciones requeridos para tu Modal.

No se devolverá ninguna referencia a la instancia del componente. En su lugar, show() devuelve una promesa que se resolverá cuando el modal se cierre. La promesa se resolverá con cualquier dato que se haya pasado 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 },
    });
  });
}

Alternativamente, migra a la API declarativa descrita en la documentación principal de DModal.

La funcionalidad de las opciones antiguas puede replicarse de la siguiente manera:

Opción antigua de showModal Solución
admin n/a para componentes - elimínala
templateName n/a para componentes - elimínala
title muévelo a <DModal @title={{i18n "blah"}}>
titleTranslated muévelo a <DModal @title="blah">. Esto podría calcularse basándose en datos de model si es necesario
modalClass muévelo a <DModal class="blah">
titleAriaElementId muévelo a <DModal aria-labelledby="blah">
panels Usa el bloque nombrado :headerBelowTitle para implementar pestañas en tu componente (ejemplo)
model sin cambios

Paso 5: Pruebas

Cualquier prueba debería permanecer en gran medida igual. Los problemas más comunes son:

  • Los modales ya no tienen una clase predeterminada basada en su nombre. Las clases deben especificarse explícitamente en la plantilla (ver inicio del Paso 3)

  • El envoltorio d-modal ya no persiste en el DOM cuando el modal se cierra. Para verificar que todos los modales están cerrados, usa una comprobación como assert.dom('.d-modal').doesNotExist()

¡Éxito!

Tu modal debería funcionar ahora como antes. Para aprovechar aún más la nueva API, es posible que quieras considerar reemplazar las llamadas a showModal con una estrategia declarativa, y convertir tu Modal en un componente de Glimmer.

Ejemplos

Aquí tienes algunos commits de ejemplo que demuestran la conversión de algunos modales del núcleo de Discourse a la nueva API:


Este documento está bajo control de versiones: sugiere cambios en github.


  1. Los componentes clásicos de Ember se recomiendan en esta guía porque ofrecen la ruta de migración más sencilla desde los controladores de Ember. Pero para modales simples, o si estás dispuesto a dedicar tiempo a la refactorización, los componentes modernos de Glimmer son la mejor opción. ↩︎

20 Me gusta

Esto se ve muy bien. Me da esperanza de que pueda convertir mis modales a Ember 4. Apenas entiendo el código de Ember que escribo, así que escribir documentación que pueda entender no es fácil. Muchas gracias por esto.

8 Me gusta

¡Gracias por el tutorial! Ver los ejemplos fue muy útil. Pude arreglar mi modal de plugin personalizado roto en una hora.

4 Me gusta

Estoy trabajando en esta conversión ahora mismo, pero me encuentro con un problema:

Anteriormente, nuestro modal no tenía un controlador/definición de JS correspondiente, y podíamos mostrar el modal a través de showModal($HBS_FILE_NAME). Dado que el nuevo show() requiere que se pase un componente, necesito introducir esta definición de JS (¿es esta una suposición correcta?).

Agregué algo como:

import Component from '@glimmer/component';

export default class SomeModal extends Component {

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

y tengo el archivo .hbs anterior (con los cambios necesarios en DModal) tanto en el directorio /components/modal con el mismo nombre de archivo. Al intentar renderizar el modal (a través de getOwner(this).lookup("service:modal").show(SomeModal)), veo mi registro del constructor impreso en la consola, pero el modal no se renderiza.

¿Se necesita alguna otra configuración en el controlador/definición de JS para este cambio? ¡Cualquier orientación sería muy apreciada!

No lo necesitas si no vas a añadir ningún código.

Puedes tener solo el archivo .hbs.

discourse-templates, por ejemplo, no tiene un archivo JS correspondiente para la plantilla handlebars del modal.

¿Adaptaste tu plantilla handlebars siguiendo las instrucciones?

¿Hay algún error en la consola?

2 Me gusta

¡Gracias por tus comentarios! Un enorme :facepalm: de mi parte, había movido los archivos al directorio .../discourse/templates/components/modal, en lugar de .../discourse/components/modal. Ahora todo funciona como se esperaba (con o sin el controlador .js), ¡gracias!

3 Me gusta

¿Podrías mostrarme cómo llamar a showModal() desde un script dentro de un archivo head_tag.html? En mi caso, necesito usar

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

para capturar el evento de clic, verificar la condición y luego mostrar un modal personalizado.

1 me gusta

¡Realmente aprecio el esfuerzo que hiciste aquí para documentar esto tan claramente, David!

He logrado eliminar casi todas las deprecaciones para 3.2 en una tarde en nuestro plugin más grande.

3 Me gusta

¿Cómo se accede ahora a una ventana modal existente en core para modificarla?

En el pasado, usé esto (que ya no funciona):
api.modifyClass("controller:poll-ui-builder", {

En este caso particular, el nombre de la clase parece estar bien declarado y sin cambios.

2 Me gusta

Dependiendo de lo que necesites modificar, creo que la mejor solución sería usar un PluginOutlet para inyectar tu código personalizado, o un PluginOutlet Wrapper para reemplazar/mostrar condicionalmente la implementación principal. (Puedes enviar una PR para añadir un outlet si no está disponible).

Si realmente quieres usar modifyClass, todavía debería ser posible, es solo que el modal es ahora un componente y está anidado en components/modal, por lo que accederías a él así:

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

   // inserta código personalizado
});
4 Me gusta