# Converting modals from legacy controllers to new DModal component API

**URL:** https://meta.discourse.org/t/converting-modals-from-legacy-controllers-to-new-dmodal-component-api/268057
**Category:** Developer Guides
**Tags:** code
**Created:** [July 3, 2023, 9:52am UTC](https://meta.discourse.org/t/converting-modals-from-legacy-controllers-to-new-dmodal-component-api/268057 "2023-07-03T09:52:14Z")
**Posts on this page:** 10
**Page:** 1

<div class="post-metadata">

### Author: ![Discourse](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/discourse/32/148734_2.png) [@Discourse](https://meta.discourse.org/u/Discourse)
#### Post date: [July 3, 2023, 9:52am UTC](https://meta.discourse.org/t/converting-modals-from-legacy-controllers-to-new-dmodal-component-api/268057/1 "2023-07-03T09:52:14Z")

</div>

> ℹ If you’re implementing a new Modal, check out the main docs [here](https://meta.discourse.org/t/268304). This topic describes how to migrate an existing controller-based Modal to the new Component-based API.

In the past, Discourse used an Ember-Controller-based API for rendering modals. To invoke the modal, you would pass a string with the name of the controller to `showModal()`. Under the covers, this made use of Ember’s `Route#renderTemplate` API, which is deprecated in Ember 3.x and will be removed in Ember 4.x.

To allow Discourse to upgrade to Ember 4.x and beyond, we’ve introduced a new component-based API for modals. This new API embraces Ember’s ‘declarative’ design patterns, and aims to provide clean DDAU (data down actions up) semantics.

## Step 1: Move Files

Move the controller JS file and the template file to the `/components/modal` directory. This makes them a ‘colocated component’ which can be imported just like any other JS module.

## Step 2: Update the JS file

Then, update the component JS definition to extend from `@ember/component` instead of `@ember/controller` \[1\]. Remove the `ModalFunctionality` mixin and update any uses of its functions according to the table below:

| Before | After |
| --- | --- |
| `flash()` and `clearFlash()` | Create a `flash` property in your component and pass it to the `@flash` argument of `<DModal>`. By default the alert will be styled with the `alert` class which is a copy of the ‘`error`’ class, but it can be overridden using the `@flashType` argument. |
| `showModal()` | Import the `showModal` function from `discourse/lib/show-modal` |
| `closeModal` action | Invoke the `closeModal` argument which is automatically passed into your component |

Old-style Modal Controllers would live ‘forever’, which meant we had to manually cleanup state. With the new Component-based API, the component will be created and destroyed when the modal is shown/hidden. In many cases that means your old lifecycle hooks are no longer required.

If you still need some lifecycle-based logic, use this table:

| Before | After |
| --- | --- |
| `onShow()` | Use standard Ember component lifecycle (`init()` or Ember modifier) |
| `afterRender` | Use standard Ember component lifecycle (`init()` or Ember modifier) |
| `beforeClose()` | create a wrapper around the `@closeModal` argument which is passed into your component. Pass a reference to your close wrapper into `DModal` like `<DModal @closeModal={{this.myCloseModalWrapper}}`\> |
| `onClose()` | Use standard Ember component lifecycle (`willDestroy()` or Ember modifier) |

## Step 3: Update the Template

Replace the `<DModalBody>` wrapper with `<DModal>`. Add some new attributes:

- Pass through the new `@closeModal` argument
- Add an explicit class. To match the old behavior, take your controller filename and add `-modal`.

For example, if your modal controller was called `close-topic.js`, the new `<DModal>` invocation would look something like this:

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

```

If the `DModalBody` invocation includes any other arguments, update them based on the table below:

| Before | After |
| --- | --- |
| `@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 angle-bracket syntax with regular html attribute: `<DModal class="blah">` |
| `@titleAriaElementId` | Use angle-bracket syntax with regular html attribute: `<DModal aria-labelledby="blah">` |
| `@dismissable`, `@submitOnEnter`, `@headerClass` | Unchanged |

If there was any footer content rendered after the old `<DModalBody>` component, use the new `<:footer>` named block to introduce it inside `<DModal>`. When using any named blocks, the body content should be wrapped in `<:body></:body>`. For example:

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

```

### Step 4: Update the showModal call sites

Previously, modals would be rendered using the `showModal` API, which would take a string (the controller name) and a number of opts. It would return an instance of the controller which could be manipulated:

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

```

To render new component-based Modals you should inject the ‘modal’ service (or access it using something like `getOwner(this).lookup("service:modal")`) and call the `show()` function.

`show()` takes a reference to the new Component class as the first argument. The only opt still supported is ‘model’, which can be used to pass all data/actions required for your Modal.

No reference to the component instance will be returned. Instead, `show()` returns a promise which will resolve when the modal is closed. The promise will resolve with any data which was passed to `@closeModal`.

```js
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 },
    });
  });
}

```

Alternatively, migrate to the declarative API described in [the main DModal documentation](https://meta.discourse.org/t/using-the-dmodal-api-to-render-modal-windows-aka-popups-dialogs-in-discourse/268304).

The functionality of the old options can be replicated as follows:

| Old `showModal` opt | Solution |
| --- | --- |
| `admin` | n/a for component - remove it |
| `templateName` | n/a for components - remove it |
| `title` | move to `<DModal @title={{i18n "blah"}}>` |
| `titleTranslated` | move to `<DModal @title="blah">`. This could be computed based on data from `model` if needed |
| `modalClass` | move to `<DModal class="blah">` |
| `titleAriaElementId` | move to `<DModal aria-labelledby="blah">` |
| `panels` | Use the `<:headerBelowTitle>` named block to implement tabs in your component ([example](https://github.com/discourse/discourse/pull/22164)) |
| `model` | unchanged |

## Step 5: Tests

Any tests should largely remain the same. The most common issue are:

- Modals no longer have a default class based on their name. Classes must be specified explicitly in the template (see beginning of Step 3)

- The `d-modal` wrapper no longer persists in the DOM when the modal is closed. To check all modals are closed, use a check like `assert.dom('.d-modal').doesNotExist()`

## Profit!

Your modal should now work as it did before. To take further advantage of the new API, you may want to consider replacing `showModal` calls with a declarative strategy, and converting your Modal to be a Glimmer component.

## Examples

Here are some example commits which demonstrate converting some of Discourse core’s modals to the new API:

- [DEV: Convert share-topic modal to new component-based API - Pull Request #22154 - discourse/discourse - GitHub](https://github.com/discourse/discourse/pull/22154)

- [DEV: Convert poll modals to new component-based API - Pull Request #22164 - discourse/discourse - GitHub](https://github.com/discourse/discourse/pull/22164)

* * *

This document is version controlled - suggest changes [on github](https://github.com/discourse/discourse/blob/main/docs/developer-guides/docs/03-code-internals/11-converting-modals.md).

* * *

1. Classic Ember Components are recommended in this guide because they provided the easiest migration path from Ember Controllers. But for simple modals, or if you’re happy to spend some time refactoring, modern Glimmer components are the better choice.

---

<div class="post-metadata">

### Author: ![pfaffman](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/pfaffman/32/120154_2.png) [@pfaffman](https://meta.discourse.org/u/pfaffman)
#### Post date: [July 3, 2023, 10:19am UTC](https://meta.discourse.org/t/converting-modals-from-legacy-controllers-to-new-dmodal-component-api/268057/6 "2023-07-03T10:19:09Z")

</div>

This looks really great. It gives me hope they I can convert my modals to ember 4. I only barely understand the ember code that I write, so writing documentation that I can understand is not easy. Thanks very much for this.

---

<div class="post-metadata">

### Author: ![damienalexandre](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/damienalexandre/32/293019_2.png) [@damienalexandre](https://meta.discourse.org/u/damienalexandre)
#### Post date: [July 13, 2023, 12:00pm UTC](https://meta.discourse.org/t/converting-modals-from-legacy-controllers-to-new-dmodal-component-api/268057/7 "2023-07-13T12:00:35Z")

</div>

Thanks for the tutorial! Looking at the examples was highly useful. Had been able to fix my custom plugin modal broken in an hour.

---

<div class="post-metadata">

### Author: ![swong](https://avatars.discourse-cdn.com/v4/letter/s/a3d4f5/32.png) [@swong](https://meta.discourse.org/u/swong)
#### Post date: [July 20, 2023, 11:49pm UTC](https://meta.discourse.org/t/converting-modals-from-legacy-controllers-to-new-dmodal-component-api/268057/8 "2023-07-20T23:49:41Z")

</div>

I’m working on this conversion right now, but running into an issue:

Previously, our modal did not have a corresponding controller/JS definition, and we were able to show the modal through `showModal($HBS_FILE_NAME)`. Since the new `show()` requires a component to be passed in, I need to introduce this JS definition ( **is this a correct assuption?** ).

I added something like:

```plaintext
import Component from '@glimmer/component';

export default class SomeModal extends Component {

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

```

and have the previous `.hbs` file (with required changes to `DModal`) both in the `/components/modal` directory with the same file name. When trying to render the modal (via `getOwner(this).lookup("service:modal").show(SomeModal)`), I see my constructor log printed in console, but the modal is not rendered.

**Is there any other configuration needed in the controller/JS definition needed for this change?** Any guidance would be much appreciated!

---

<div class="post-metadata">

### Author: ![saquetim](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/saquetim/32/116400_2.png) [@saquetim](https://meta.discourse.org/u/saquetim)
#### Post date: [July 21, 2023, 12:09am UTC](https://meta.discourse.org/t/converting-modals-from-legacy-controllers-to-new-dmodal-component-api/268057/9 "2023-07-21T00:09:49Z")

</div>

> [@swong](#):
>
> Since the new `show()` requires a component to be passed in, I need to introduce this JS definition ( **is this a correct assuption?** ).

You don’t need it if you’re not adding any code.

You can have just the `.hbs` file.

`discourse-templates`, for example, doesn’t have a corresponding JS file for the modal handlebars template.

> **[discourse-templates/assets/javascripts/discourse/components/d-templates/modal at...](https://github.com/discourse/discourse-templates/tree/14b98ee447af4fc155085e6a08e4094c2c031482/assets/javascripts/discourse/components/d-templates/modal)**
>
> 14b98ee447af4fc155085e6a08e4094c2c031482/assets/javascripts/discourse/components/d-templates/modal

Did you adapt your handlebars template following the instructions?

Are there any errors in the console?

---

<div class="post-metadata">

### Author: ![swong](https://avatars.discourse-cdn.com/v4/letter/s/a3d4f5/32.png) [@swong](https://meta.discourse.org/u/swong)
#### Post date: [July 21, 2023, 6:10pm UTC](https://meta.discourse.org/t/converting-modals-from-legacy-controllers-to-new-dmodal-component-api/268057/10 "2023-07-21T18:10:55Z")

</div>

Thanks for the feedback! Huge :facepalm: on my end, I had moved files to the `.../discourse/templates/components/modal` dir, instead of `.../discourse/components/modal`. Things are working as expected now (with or without the `.js` controller), thank you!

---

<div class="post-metadata">

### Author: ![Thien\_Nguyen\_Ngoc](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/thien_nguyen_ngoc/32/316503_2.png) [@Thien\_Nguyen\_Ngoc](https://meta.discourse.org/u/Thien_Nguyen_Ngoc)
#### Post date: [September 11, 2023, 6:43am UTC](https://meta.discourse.org/t/converting-modals-from-legacy-controllers-to-new-dmodal-component-api/268057/11 "2023-09-11T06:43:11Z")

</div>

Could you show me how to call showModal() from scrip inside a head\_tag.html please? In my case I need to use

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

```

to catch the click event, check the condition and then show a custom modal.

---

<div class="post-metadata">

### Author: ![merefield](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/merefield/32/176214_2.png) [@merefield](https://meta.discourse.org/u/merefield)
#### Post date: [September 14, 2023, 7:13pm UTC](https://meta.discourse.org/t/converting-modals-from-legacy-controllers-to-new-dmodal-component-api/268057/12 "2023-09-14T19:13:14Z")

</div>

Really appreciate the effort you made here to document this so clearly, David!

I’ve all but managed to clear deprecations for 3.2 in an afternoon on our biggest plugin.

---

<div class="post-metadata">

### Author: ![nathank](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/nathank/32/290039_2.png) [@nathank](https://meta.discourse.org/u/nathank)
#### Post date: [November 6, 2023, 9:17pm UTC](https://meta.discourse.org/t/converting-modals-from-legacy-controllers-to-new-dmodal-component-api/268057/13 "2023-11-06T21:17:11Z")

</div>

How do you now access an existing modal in core to modify it?

In the past I’ve used this (which no longer works):  
` api.modifyClass("controller:poll-ui-builder", {`

In this particular case, that class name seems to be declared nicely and is unchanged.

---

<div class="post-metadata">

### Author: ![keegan](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/keegan/32/383395_2.png) [@keegan](https://meta.discourse.org/u/keegan)
#### Post date: [November 7, 2023, 5:05pm UTC](https://meta.discourse.org/t/converting-modals-from-legacy-controllers-to-new-dmodal-component-api/268057/15 "2023-11-07T17:05:46Z")

</div>

Depending on what you need to modify, I think the best solution would be to use a [`PluginOutlet`](https://meta.discourse.org/t/using-plugin-outlet-connectors-from-a-theme-or-plugin/32727) to inject your custom code, or a [`PluginOutlet` Wrapper](https://meta.discourse.org/t/using-plugin-outlet-connectors-from-a-theme-or-plugin/32727#wrapper-outlets-2) to replace/conditionally show the core implementation. (You can PR to add an outlet if its not available)

If you really want to use `modifyClass` it should be still possible, it’s just that the modal is a component now and its nested in `components/modal` so you would access it like:

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

   // insert custom code
});
```
