Convertendo modais de controllers legados para a nova API do componente DModal

:information_source: Se você estiver implementando um novo Modal, consulte a documentação principal aqui. Este tópico descreve como migrar um Modal baseado em controlador existente para a nova API baseada em Componentes.

No passado, o Discourse utilizava uma API baseada em Ember-Controller para renderizar modais. Para invocar o modal, você passava uma string com o nome do controlador para showModal(). Internamente, isso fazia uso da API Route#renderTemplate do Ember, que foi depreciada no Ember 3.x e será removida no Ember 4.x.

Para permitir que o Discourse seja atualizado para o Ember 4.x e versões posteriores, introduzimos uma nova API baseada em componentes para modais. Essa nova API adota os padrões de design “declarativos” do Ember e visa fornecer semântica limpa de DDAU (dados descendo, ações subindo).

Etapa 1: Mover Arquivos

Mova o arquivo JS do controlador e o arquivo de template para o diretório /components/modal. Isso os torna um “componente co-localizado”, que pode ser importado como qualquer outro módulo JS.

Etapa 2: Atualizar o arquivo JS

Em seguida, atualize a definição do componente JS para estender @ember/component em vez de @ember/controller [1]. Remova o mixin ModalFunctionality e atualize qualquer uso de suas funções de acordo com a tabela abaixo:

Antes Depois
flash() e clearFlash() Crie uma propriedade flash no seu componente e passe-a para o argumento @flash de <DModal>. Por padrão, o alerta será estilizado com a classe alert, que é uma cópia da classe error, mas pode ser substituída usando o argumento @flashType.
showModal() Importe a função showModal de discourse/lib/show-modal
ação closeModal Invoque o argumento closeModal que é passado automaticamente para o seu componente

Os Controladores de Modal do estilo antigo permaneceriam “para sempre”, o que significava que precisávamos limpar manualmente o estado. Com a nova API baseada em Componentes, o componente será criado e destruído quando o modal for exibido/oculto. Em muitos casos, isso significa que seus antigos hooks de ciclo de vida não são mais necessários.

Se você ainda precisar de alguma lógica baseada em ciclo de vida, use esta tabela:

Antes Depois
onShow() Use o ciclo de vida padrão do componente Ember (init() ou modificador Ember)
afterRender Use o ciclo de vida padrão do componente Ember (init() ou modificador Ember)
beforeClose() crie um wrapper em torno do argumento @closeModal que é passado para o seu componente. Passe uma referência para o seu wrapper de fechamento em DModal como <DModal @closeModal={{this.myCloseModalWrapper}}>
onClose() Use o ciclo de vida padrão do componente Ember (willDestroy() ou modificador Ember)

Etapa 3: Atualizar o Template

Substitua o wrapper <DModalBody> por <DModal>. Adicione alguns novos atributos:

  • Passe o novo argumento @closeModal
  • Adicione uma classe explícita. Para corresponder ao comportamento antigo, pegue o nome do arquivo do seu controlador e adicione -modal.

Por exemplo, se o seu controlador de modal se chamava close-topic.js, a nova invocação de <DModal> ficaria mais ou menos assim:

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

Se a invocação DModalBody incluir quaisquer outros argumentos, atualize-os com base na tabela abaixo:

Antes Depois
@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 Use a sintaxe de colchetes angulares com atributo HTML regular: <DModal class="blah">
@titleAriaElementId Use a sintaxe de colchetes angulares com atributo HTML regular: <DModal aria-labelledby="blah">
@dismissable, @submitOnEnter, @headerClass Inalterado

Se havia algum conteúdo de rodapé renderizado após o antigo componente <DModalBody>, use o novo bloco nomeado :footer para introduzi-lo dentro de <DModal>. Ao usar qualquer bloco nomeado, o conteúdo do corpo deve ser envolvido em :body e /:body. Por exemplo:

<DModal @closeModal={{@closeModal}}>
  :body
    Olá mundo, este é o conteúdo do modal
  /:body
  :footer
    Este é o conteúdo do rodapé. Um wrapper `.modal-footer` será adicionado
    automaticamente
  /:footer
</DModal>

Etapa 4: Atualizar os locais de chamada showModal

Anteriormente, os modais seriam renderizados usando a API showModal, que aceitava uma string (o nome do controlador) e vários opts. Ela retornava uma instância do controlador que podia ser manipulada:

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 novos Modais baseados em componentes, você deve injetar o serviço ‘modal’ (ou acessá-lo usando algo como getOwner(this).lookup("service:modal")) e chamar a função show().

show() aceita uma referência à nova classe de Componente como o primeiro argumento. A única opção ainda suportada é ‘model’, que pode ser usada para passar todos os dados/ações necessários para o seu Modal.

Nenhuma referência à instância do componente será retornada. Em vez disso, show() retorna uma promessa que será resolvida quando o modal for fechado. A promessa será resolvida com quaisquer dados que foram passados para @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, migre para a API declarativa descrita na documentação principal do DModal.

A funcionalidade das opções antigas pode ser replicada da seguinte forma:

Opção antiga showModal Solução
admin n/a para componente - remova-o
templateName n/a para componentes - remova-o
title mova para <DModal @title={{i18n "blah"}}>
titleTranslated mova para <DModal @title="blah">. Isso pode ser calculado com base em dados de model se necessário
modalClass mova para <DModal class="blah">
titleAriaElementId mova para <DModal aria-labelledby="blah">
panels Use o bloco nomeado :headerBelowTitle para implementar abas no seu componente (exemplo)
model inalterado

Etapa 5: Testes

Qualquer teste deve permanecer essencialmente o mesmo. Os problemas mais comuns são:

  • Modais não têm mais uma classe padrão baseada em seu nome. As classes devem ser especificadas explicitamente no template (veja o início da Etapa 3)

  • O wrapper d-modal não persiste mais no DOM quando o modal é fechado. Para verificar se todos os modais estão fechados, use uma verificação como assert.dom('.d-modal').doesNotExist()

Lucro!

Seu modal agora deve funcionar como antes. Para aproveitar ainda mais a nova API, você pode considerar substituir as chamadas showModal por uma estratégia declarativa, e converter seu Modal para ser um componente Glimmer.

Exemplos

Aqui estão alguns commits de exemplo que demonstram a conversão de alguns modais do núcleo do Discourse para a nova API:


Este documento está sob controle de versão - sugira alterações no github.


  1. Componentes Ember Clássicos são recomendados neste guia porque oferecem o caminho de migração mais fácil a partir dos Controladores Ember. No entanto, para modais simples, ou se você estiver disposto a gastar algum tempo refactorando, os componentes Glimmer modernos são a melhor escolha. ↩︎

20 curtidas

Isso parece ótimo. Me dá esperança de que eu possa converter meus modais para o Ember 4. Eu mal entendo o código Ember que escrevo, então escrever documentação que eu possa entender não é fácil. Muito obrigado por isso.

8 curtidas

Obrigado pelo tutorial! Olhar os exemplos foi muito útil. Consegui consertar meu modal de plugin personalizado que estava quebrado em uma hora.

4 curtidas

Estou trabalhando nesta conversão agora mesmo, mas estou encontrando um problema:

Anteriormente, nosso modal não tinha um controlador/definição JS correspondente, e conseguimos mostrar o modal através de showModal($HBS_FILE_NAME). Como o novo show() requer um componente para ser passado, preciso introduzir esta definição JS (esta é uma suposição correta?).

Adicionei algo como:

import Component from '@glimmer/component';

export default class SomeModal extends Component {

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

e tenho o arquivo .hbs anterior (com as alterações necessárias para DModal) ambos no diretório /components/modal com o mesmo nome de arquivo. Ao tentar renderizar o modal (via getOwner(this).lookup("service:modal").show(SomeModal)), vejo meu log do construtor impresso no console, mas o modal não é renderizado.

É necessária alguma outra configuração no controlador/definição JS para esta alteração? Qualquer orientação seria muito apreciada!

Você não precisa disso se não estiver adicionando nenhum código.

Você pode ter apenas o arquivo .hbs.

O discourse-templates, por exemplo, não tem um arquivo JS correspondente para o template handlebars do modal.

Você adaptou seu template handlebars seguindo as instruções?

Existem erros no console?

2 curtidas

Obrigado pelo feedback! Grande :facepalm: da minha parte, eu havia movido os arquivos para o diretório .../discourse/templates/components/modal, em vez de .../discourse/components/modal. As coisas estão funcionando como esperado agora (com ou sem o controlador .js), obrigado!

3 curtidas

Você poderia me mostrar como chamar showModal() de um script dentro de um head_tag.html, por favor? No meu caso, preciso usar

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

para capturar o evento de clique, verificar a condição e, em seguida, mostrar um modal personalizado.

1 curtida

Agradeço muito o esforço que você fez para documentar isso tão claramente, David!

Consegui eliminar quase todas as depreciações para a 3.2 em uma tarde em nosso maior plugin.

3 curtidas

Como acessar um modal existente no core para modificá-lo agora?

No passado, eu usava isto (que não funciona mais):
api.modifyClass("controller:poll-ui-builder", {

Neste caso específico, o nome dessa classe parece estar bem declarado e inalterado.

2 curtidas

Dependendo do que você precisa modificar, acho que a melhor solução seria usar um PluginOutlet para injetar seu código personalizado, ou um PluginOutlet Wrapper para substituir/mostrar condicionalmente a implementação principal. (Você pode enviar um PR para adicionar um outlet se ele não estiver disponível)

Se você realmente quiser usar modifyClass, ainda será possível, é apenas que o modal agora é um componente e está aninhado em components/modal, então você o acessaria assim:

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

   // insira código personalizado
});
4 curtidas