Tony Bach

Musings about frontend, tech, and startups

7 July 2022

Opening modals using query params in Ember.js

Basic example

In modern web design, modals are a powerful tool that allows content to displayed to the user in-context without navigating to a different page.

In most implementations, modal components take in a flag which controls its open/closed state, and a callback which is called when the “Close” button is clicked. In other words, the open state of the modal is controlled by a parent component. Let’s say we have a button component that when clicked on should open a modal. Here’s a super simplified implementation:

{{!-- wrapper-component.hbs --}}
<Button @click={{this.openModal}}>
  Open modal
</Button>
<Modal
  @open={{this.isModalOpen}}
  @onClose={{this.closeModal}}
>
  ...
</Modal>

And in our component/route logic, we define the properties and actions:

// wrapper-component.js
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
 
export default class WrapperComponent extends Component {
 @tracked isModalOpen = false;
 
 @action
 openModal() {
   this.isModalOpen = true;
 }
 
 @action
 closeModal() {
   this.isModalOpen = false;
 }
}

Thanks to Ember’s powerful “tracked properties” mechanism, we can use simple property assignment to update the isOpen property in our click handlers, and the template will be updated to use the new isOpen value. In this case, clicking on our Button component will set the isOpen property to true, which will open our modal. If you’re not familiar with the @tracked and @action syntax, you can read up about them in the super helpful Ember Guides.

Now let’s say instead of manually clicking on a button to open the modal, we want to allow users to deeplink to the page with the modal already opened. This is a common use case, especially when the user navigates from a different page and we want to show some special content (e.g. promotions) when they land on the page.

A common way to handle this is to use a query parameter to indicate the modal open state. Let’s say our domain is www.ourawesomepage.com. We have an “About” page, where we want to open the modal, so the full deep link can be www.ourawesomepage.com/about?openModal=true. In Ember.js, to handle query parameters on a route, we have to define them in the controller for that route:

import Controller from '@ember/controller';
 
export default class AboutController extends Controller {
  queryParams [ 'openModal'];
  @tracked openModal = null;
}

Now when the user navigates to www.ourawesomepage.com/about?openModal=true, the openModal property will be set to true in our about route.

The next step is to change our above component logic to use this openModal property to control the modal state, instead of using a property defined in the component. Luckily, Ember provides us a way to access query params of a route directly from a component, by using RouterService. This is a public Ember API that we can inject into the component, allowing us to access the global router state.

// wrapper-component.js
import { service } from '@ember/service';
 
export default class WrapperComponent extends Component {
  @service router;
}

Now we can refer to the router within the component through calling this.router. The router service has a currentRoute property, which contains queryParams property. We can leverage this to transform our previous isModalOpen tracked property to be a getter instead, which derives its value from the route’s query params:

// wrapper-component.js
import { service } from '@ember/service';
 
export default class WrapperComponent extends Component {
  @service router;
 
  @tracked isModalOpen = false;
  get isModalOpen() {
   return this.router.currentRoute.queryParams?.openModal;
  }
}

Finally, we also need to change our previous openModal and closeModal actions, since we no longer update the isModalOpen property directly. Instead, now clicking on openModal should append an openModal=true query parameter to the currentURL. Luckily, the router service has just the right method for that. By calling this.router.transitionTo and passing in an object with the key queryParams, we stay on the same route while appending the passed-in query param:

// wrapper-component.js
 @action
 openModal() {
   this.isModalOpen = true;
   this.router.transitionTo({
     queryParams: { openModal: true },
   });
 }
 
 @action
 closeModal() {
   this.isModalOpen = false;
   this.router.transitionTo({
     queryParams: { openModal: null },
   });
 }
}

Now this works as expected! If we’re on www.ourawesomepage.com/about, clicking on the button will append openModal=true to the URL, resulting in www.ourawesomepage.com/about?openModal=true without navigating away. Since our isModalOpen getter gets the value of the openModal query param in the URL, it will now return true, and our modal will open. Closing the modal will invoke our closeModal callback, which sets the openModal query param to null, and will result in our URL being www.ourawesomepage.com/about again. See the below for our complete component JS code:

// wrapper-component.js
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { service } from '@ember/service';
 
export default class WrapperComponent extends Component {
  @service router;
 
  get isModalOpen() {
   return this.router.currentRoute.queryParams?.openModal;
  }
 
  @action
  openModal() {
   this.router.transitionTo({
     queryParams: { openModal: true },
   });
 }
 
  @action
  closeModal() {
   this.router.transitionTo({
     queryParams: { openModal: null },
   });
 }
}

And that’s it!

In this blog post we started with opening a modal in Ember.js using the state maintained within a component, then progressed to using query params to control modal state, and using Ember’s RouterService to access and change the query param in the URL. Let me know your thoughts in the comments below, and hope to see you all in the next blog post!

References

Ember Guides page on query params

Ember Guides page on tracked properties and component actions

RouterService API docs

tags: Ember,front-end,technical