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

:information_source: Se você estiver implementando um novo Modal, confira 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 Controladores do Ember 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 está obsoleta no Ember 3.x e será removida no Ember 4.x.

Para permitir que o Discourse faça o upgrade para o Ember 4.x e versões posteriores, introduzimos uma nova API baseada em componentes para modais. Esta nova API abraça os padrões de design ‘declarativos’ do Ember e visa fornecer semânticas limpas de DDAU (data down actions up).

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 da mesma forma que 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() and 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 sobrescrito usando o argumento @flashType.
showModal() Importe a função showModal de discourse/lib/show-modal
ação closeModal Invocar o argumento closeModal, que é automaticamente passado para o seu componente

Os Controladores de Modal do estilo antigo viviam ‘para sempre’, o que significava que tínhamos que limpar o estado manualmente. Com a nova API baseada em Componentes, o componente será criado e destruído quando o modal for mostrado/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 de componentes Ember (init() ou modificador Ember)
afterRender Use o ciclo de vida padrão de componentes Ember (init() ou modificador Ember)
beforeClose() Crie um wrapper ao redor do argumento @closeModal que é passado para o seu componente. Passe uma referência para o seu wrapper de fechamento para o DModal como <DModal @closeModal={{this.myCloseModalWrapper}}>
onClose() Use o ciclo de vida padrão de componentes 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 fosse chamado close-topic.js, a nova invocação de <DModal> seria algo assim:

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

Se a invocação de 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 parênteses angulares com atributo html regular: <DModal class="blah">
@titleAriaElementId Use a sintaxe de parênteses angulares com atributo html regular: <DModal aria-labelledby="blah">
@dismissable, @submitOnEnter, @headerClass Inalterado

Se houvesse qualquer 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 envolto em <:body></:body>. Por exemplo:

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

Etapa 4: Atualizar os pontos de chamada do showModal

Anteriormente, os modais eram renderizados usando a API showModal, que aceitava uma string (o nome do controlador) e uma série de opções. 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 para a nova classe do Componente como 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 para a instância do componente será retornada. Em vez disso, show() retorna uma promise que será resolvida quando o modal for fechado. A promise 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 antigas opções 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

Quaisquer testes devem permanecer em grande parte os mesmos. O problema mais comum é:

  • Os Modais não têm mais uma classe padrão baseada em seus nomes. 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 deve agora funcionar como funcionava 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 exemplos de commits que demonstram a conversão de alguns dos modais do núcleo do Discourse para a nova API:


Este documento é controlado por versão - sugira alterações no github.


  1. Os Componentes Clássicos do Ember são recomendados neste guia porque forneceram o caminho de migração mais fácil a partir dos Controladores do Ember. Mas para modais simples, ou se você estiver disposto a gastar algum tempo refatorando, 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