In hierarchy modals without being "in template"?

It seems like common convention now to have “in template” modals, like with ember-modal-dialog:

In Template

<!-- button.hbs-->

<!-- 1 -->
<button {{action "toggleModal"}}>Toggle Modal</button>

<!-- 3 -->
{{#if isShowingModal}}
  {{#modal-dialog onClose="toggleModal"
                  targetAttachment="center"
                  translucentOverlay=true}}
    Oh hai there!
  {{/modal-dialog}}
{{/if}}
import Ember from 'ember';

export default Ember.Controller.extend({
  isShowingModal: false,
  actions: {
    toggleModal: function() {
      //--- 2 ---
      this.toggleProperty('isShowingModal');
    }
  }
});

However, I don’t like this pattern… it forces you to create a flag for the modal and look for the {{modal-dialog}} in your template to follow the behaviour. I’ve preferred the “render controller” modals syntax (ex. #1 Najbardziej zaufany przewodnik po polskich kasynach online w 2022 roku)

Render Controller

<!-- button.hbs-->
<button {{action "showModal" "logout-modal" }}>Toggle Modal</button>
// application.js
// boilerplate, doesn't count as part of modal
export default Ember.Route.extend({
  actions: {
    showModal: function(name, model) {
      this.render(name, {
        into: 'application',
        outlet: 'modal',
        model: model
      });
    },
    removeModal: function() {
      this.disconnectOutlet({
        outlet: 'modal',
        parentView: 'application'
      });
    }
  }
});

// not really needed
// logout-modal.js
export default Ember.Controller.extend({});
<!-- logout-modal.hbs -->
{{#my-modal title='Logout'}}
  Are you sure you want to logout?
{{/my-modal}}

This felt simpler because there’s no flag or boilerplate code in button.js. The modal being it’s separate controller/component made sense to me too.

So why are does common convention prefer “in template” modals?

My guess is this.render moves the modal to another part of the hierarchy—in it’s own controller— and the data-down actions-up pattern breaks.

I guess… Given that button.hbs represents a button.js component or controller. button.js can’t have “children components” without putting it in the button.hbs template…? Is that true?

I just wish there was another way to make modals while respecting the hierarchy.

this.render is deprecated RENDERING INTO A {{RENDER}} HELPER THAT RESOLVES TO AN {{OUTLET}}. Instead they suggested to use ember-elsewhere or another DOM-redirection library.

Could you please try ember-elsewhere addon and update your comments?.

ah… there’s a depreciation too…

fundamentally, ember-elsewhere, ember-wormhole and ember-modal-dialog work fundamentally the same don’t they?

For ember-elsewhere. you create a new modal like thus?

<!-- button.hbs-->
<button {{action "toggleModal"}}>Toggle Modal</button>

{{#if isShowingModal}}
{{to-elsewhere named="modal"
               send=(hash body=(component "warning-message")
                          onOutsideClick=(action "close")) }}
{{/if}}

With the same controller code, requiring the flag and looking for {{to-elsewhere }} to follow the behaviour. They’re fundamentally the same… and “in template”.

Is it having a have an action on your controller for each modal that you don’t like about “in template” modals?

In most of the applications that I’ve written the modals don’t need access to the template scope they’re called from. That means a component controlled by a service is all that was required for our modal system. Controllers or components called a method on the service, passing options if needed. The component was in the application template and watched the service to see which modal to render

yeah, that’s part of it. “in template” modals need boilerplate to connect the show/hide flag to the modal information. the “render controller” way passes the modal information to the show/hide actions—grouping all the data together.

hmm… how does the modal show up from the service? i can only see outlets working.

You can use a component in a similar way to an outlet. The key here is the use of the component helper. It allows you to dynamically change which component you want to render. The basic modal setup is as follows:

Modal service takes requests to open and close modals from anywhere really. This is basic but you can have a queue system, pass deferreds for control, all sorts

// services/modal.js
export default Ember.Service.extend({
  openModal(name, config) {
    this.set('name', name);
    this.set('config', config);
  },

  closeModal() {
    this.set('name', null);
    this.set('config', null);
  }
});

Modal outlet is put in application.hbs. It watches the modal service and renders a modal when the service has one

// components/modal-outlet.js
export default Ember.Component.extend({
  modal: Ember.inject(),

  name: Ember.computed.reads('modal.name'),
  config: Ember.computed.reads('modal.config'),
  hasModal: Ember.computed.bool('name'),

  path: Ember.computed('name', function() {
    return 'modals/' + this.get('name');
  })
});
<!-- templates/components/modal-outlet.hbs -->
{{#if hasModal}}
    {{component path config=config}}
{{/if}}

An example call to the modal service to open the ‘hello world’ modal

showHelloWorld(text) {
  let modalService = this.get('modal');
  modalService.openModal('hello-world', { world: test });
}

This would make the modal outlet component attempt to render a component components/modals/hello-world, which might have a template like below

<h1>Hello, {{config.world}}!</h1>

Calling closeModal on the modal service would remove the hello world modal from the page

Hope this helps

1 Like

oh… smart idea! as you said, the main tradeoff is no access to the template scope. think i’m going to convert my outlet code this way :slight_smile:

thanks @martletandco.

Let us know how it goes!

What do you think about this?

Anyway I have a problem: how to activate willDestroy() and willRender() of every component and sub-component?

I need this for the Bootstrap modals methods.

How to?

@johnunclesam that’s pretty close. I’m not sure if it’s just a leftover but you shouldn’t need isModalVisible as you’re already checking in modal-outlet (hasModal)

As for the willRender and willDestroy calls, if you move them out of the actions hash and have them as methods on the component then they’ll fire. Have a look at the component lifecycle so you can be sure your code is called exactly when you want it to be