Embracing Ember: Routes

Modal dialogs are a common UI component often used to draw attention to vital pieces of information.

FormKeep's payment flow involves presenting a Stripe payment form embedded within a modal dialog.

FormKeep's payment
modal

We went through a few implementations, each of which involved tradeoffs between user experience and code maintainability.

Managing visibility with internal state

The initial implementation involved the PayController managing an internal showModal flag, toggled with a <a> tag.

templates/state.hbs:

<a href="#" >Open Stateful Modal</a>


{{#if showModal}}
  {{#x-modal}}
    {{credit-card card=model}}
  {{/x-modal}}
{{/if}}

controllers/state.js:

App.StateController = Ember.Controller.extend({
  showModal: false,

  actions: {
    closeModal: function() {
      this.set('showModal', false);
    },

    showModal: function() {
      this.set('showModal', true);
    },

    saveCard: function(card) {
      /* save to Stripe here */
      this.set('showModal', false);
    }
  }
});

This functioned well enough, but had some drawbacks.

From a maintenance perspective:

  • Our controller was responsible for maintaining the visibility flag.
  • Our controller required an action to mutate the modal's visibility.
  • Our template wrapped the modal within a guard conditional.

From a usability perspective:

  • Modal visibility state was lost during browser navigations and refreshes.

Thanks to Ember's emphasis on serializing application state to the URL, we had options.

Managing visibility with a query parameter

Exposing the modal's visibility flag to the URL was the simplest improvement we could make.

One possible approach was to embed the flag as a query parameter.

Opening the payment modal would append ?showModal=true to the browser's URL, while closing it would remove ?showModal=true.

templates/query-params.hbs:

{{#link-to "query-params" (query-params showModal=true)}}
  Open Query Params Modal
{{/link-to}}

{{#if showModal}}
  {{#x-modal}}
    {{credit-card card=model}}
  {{/x-modal}}
{{/if}}

controllers/query-params.js:

App.QueryParamsController = Ember.Controller.extend({
  queryParams: ['showModal'],

  showModal: false,

  actions: {
    closeModal: function() {
      this.set('showModal', false);
    },

    showModal: function() {
      this.set('showModal', true);
    },

    saveCard: function(card) {
      /* save to Stripe here */
      this.set('showModal', false);
    }
  }
});

This approach improved upon our previous implementation:

  • Users could link to, bookmark, navigate away from, or refresh the page while maintaining consistent modal visibility.
  • Modal visibility could be mutated by:

However, the implementation still had drawbacks:

  • Our controller was still responsible for maintaining the visibility flag.
  • Our template still had to guard the visibility of the modal with a conditional.

Managing modal visibility with nested routes

As an alternative approach to persisting visibility state as a query parameter, we could instead dedicate a separate route to the modal.

templates/nested.hbs:

{{#link-to "nested.sub"}}
  Open Sub-Route Modal
{{/link-to}}

{{outlet}}

templates/nested/sub.hbs:

{{#x-modal}}
  {{credit-card card=model}}
{{/x-modal}}

routes/nested.js:

App.NestedRoute = Ember.Route.extend({
  actions: {
    closeModal: function() {
      this.transitionTo('nested.index');
    },

    saveCard: function(card) {
      /* save to Stripe here */
      this.transitionTo('nested.index');
    }
  }
});

We found this approach to be the best:

  • Users could link to, bookmark, navigate away from, and refresh the page while maintaining the modal's visibility.
  • The modal's visibility could be mutated by:
  • Our controller no longer maintains the visibility flag.
  • Our template no longer guards the visibility of the modal with a conditional.

Wrapping up

Dedicating a sub-route to our payment modal met all of our criteria, however, depending on the modal's intended behavior, this may not be the case for you.

If the modal is meant to be quick and ephemeral, use Ember.Route#replaceWith to close it.

Ember.Route#replaceWith removes the current URL from the browser's history stack, which would prevent access to the modal through the back or refresh button.

View the live JSBin code below.

JS Bin

The next time you're faced with maintaining application state with an internal flag, consider embracing the power of the URL.

Additional reading