Node v6
Starting with v0.19 Astro must be used with Node v6+!
Menu

Managing Modal Views

What you’ll learn and how you can apply it

By the end of this section, you’ll be able to:

And you’ll be able to:

This course is for you because...

Pre-requisites

Why?

Modal views (modals) are a great way to focus your user's attention to a specific task. On mobile web there is no ability to "lock" an end user into a workflow because they always have access to the browser chrome (NOTE: Chrome - The chrome is all the UI components surrounding the content being displayed such as the tab bar, toolbar icons, menu, scrollbars, etc. A web browser basically has two pieces: the content area and the chrome.), which enables the user to go back, jump to any website, or close the tab at any time. Apps, on the other hand, provide modal views that help to eliminate distractions and guide your user in a focused way through a task. Common examples where a modal view is appropriate include opening the shopping cart or checkout, a sign-up flow, or an onboarding flow.

Astro supports the modal view pattern through the ModalViewPlugin. Modals are similar to the AnchoredLayoutPlugin in that they are a container but they present their content view modally. Everything that the user will see in the modal is embedded in the content area of the ModalViewPlugin. You are completely free to lay out the contents of the modal using any techniques available in Astro. Think of the ModalViewPlugin as the shell and you fill it with whatever you need.

In this section, we look at how to use the ModalViewPlugin and how to manage modal views in your app. We're going to use the controller pattern that you learned about in the previous section to manage all of the logic with displaying a modal and it's content.

Create the Modal

To begin we need to create the modal. Let's create a controller that creates a ModalViewPlugin that it will own. Let's create the constructor and init functions.

var Astro = require('astro/astro-full');
var ModalViewPlugin = require('astro/plugins/modalViewPlugin');

var ModalController = function(modal) {
    this.modal = modal;
    this.modal.show();
};

ModalController.init = function() {
    return ModalViewPlugin.init().then(function(modal) {
        return new ModalController(modal);
    });
};

module.exports = ModalController;

Now we have a controller that creates a ModalViewPlugin and then instantiates itself. Notice that we're passing the resolved modal plugin into the constructor and then keeping a reference to it by assigning it to this.modal.

PRO TIP! We don't set the modal view plugin as the controller's viewPlugin here. This is because the modal plugin will not be embedded anywhere in the view hierarchy. It takes over the whole screen and manages presenting itself when its show() method is called. As a result, don't expose ModalViewPlugins as the viewPlugin on a controller. It's ok for a controller not to have a viewPlugin if it doesn't participate in the view hierarchy.

When you run this code the modal opens up, but it's completely empty! That's because the ModalViewPlugin is just a container. It does not have any content by default, you have to fill that in. So let's do that now!

Specify the Content for the Modal

We need to start by importing the new plugins. We'll be using in the require() call to import the ones we need:

var Astro = require('astro/astro-full');
var ModalViewPlugin = require('astro/plugins/modalViewPlugin');

var AnchoredLayoutPlugin = require('astro/plugins/anchoredLayoutPlugin');
var HeaderBarPlugin = require('astro/plugins/headerBarPlugin');
var WebViewPlugin = require('astro/plugins/WebViewPlugin');

var ModalController = function(modal) {
    this.modal = modal;
    this.modal.show();
};

ModalController.init = function() {
    ModalViewPlugin.init().then(function(modal) {
        return new ModalController(modal);
    });
};

module.exports = ModalController;

Now we can use these new plugins in the ModalController's init function:

var Astro = require('astro/astro-full');
var ModalViewPlugin = require('astro/plugins/modalViewPlugin');
var AnchoredLayoutPlugin = require('astro/plugins/anchoredLayoutPlugin');
var HeaderBarPlugin = require('astro/plugins/headerBarPlugin');
var WebViewPlugin = require('astro/plugins/WebViewPlugin');

var ModalController = function(modal) {
    this.modal = modal;
    this.modal.show();
};

ModalController.init = function() {
    return Promise.join(
        ModalViewPlugin.init(),
        AnchoredLayoutPlugin.init(),
        HeaderBarPlugin.init(),
        WebViewPlugin.init(),
        function(modal, anchoredLayout, headerBar, webView) {
            return new ModalController(
                modal,
                anchoredLayout,
                headerBar,
                webView
            );
        });
};

module.exports = ModalController;

We also need to update the constructor function to take the new objects we're passing to it:

var Astro = require('astro/astro-full');
var ModalViewPlugin = require('astro/plugins/modalViewPlugin');
var AnchoredLayoutPlugin = require('astro/plugins/anchoredLayoutPlugin');
var HeaderBarPlugin = require('astro/plugins/headerBarPlugin');
var WebViewPlugin = require('astro/plugins/WebViewPlugin');

ModalController = function(modal, anchoredLayout, headerBar, webView) {
    this.modal = modal;
    this.anchoredLayout = anchoredLayout;
    this.headerBar = headerBar;**
    this.webView = webView;**
    headerBar.setCenterTitle('Google', 'title');
    modal.setContentView(anchoredLayout);
    anchoredLayout.addTopView(headerBar);
    anchoredLayout.setContentView(navigator);
    navigator.navigate('https://www.google.com');
};

ModalController.init = function() {
    return Promise.join(
        ModalViewPlugin.init(),
        AnchoredLayoutPlugin.init(),
        HeaderBarPlugin.init(),
        WebViewPlugin.init(),
        function(modal, anchoredLayout, headerBar, webView) {
            return new ModalController(
                modal,
                anchoredLayout,
                headerBar,
                webView
            );
        });

};

module.exports = ModalController;

If you run the app now, you will see that the modal pops up and displays a header and the Google homepage. This is great, but right now when we create our ModalController it will show immediately and we have no way to show or dismiss it.

Present the Modal

Our controller is now beginning to take shape. Let's add a method so that the object that creates it can show it when needed instead of it being shown as soon as you create it. This is preferred because it's often preferable to create the controller and hold a reference to it for the life of the app so that we don't have to keep creating instances of it.

Add a method just above the return statement at the bottom of the file:

ModalController.prototype.show = function() {
    // Setup state (navigate the webview)
    this.modal.show();
};

This is pretty simple. We've created a prototype function (show) that just calls show on the ModalViewPlugin. You might want to make sure that the WebViewPlugin has reset to some initial state before showing it. You could do that just before calling modal.show().

How to Dismiss a Modal

An app typically dismisses a modal in one of two ways: user interaction or because the user completed some workflow within the modal. Which one you use depends on what you're using the modal for. In some cases you might block dismissal of the modal until the user has completed some workflow. Be cautious of completely locking the user into a modal as it can lead to poor user experience if the user has to work through a task in the modal before dismissing it. The two modes for dismissing the modal boil down to two slightly different approaches for our code.

A good practice is to centralize logic like this into one function in the controller and then use it wherever it's needed. We'll create a hide function on the controller that can be called from code within the controller or from outside if there are other ways that the modal needs to be closed. Create a new method by adding the following code right after the controller's show method.

ModalController.prototype.show = function() {
    this.modal.show();
};

ModalController.prototype.hide = function() {
    // Cleanup and/or reset state
    this.modal.hide();
};

As with the show method, if there is any cleanup that we need to do when the modal closes, we now have a central place that we can do that.

Close Button

Now let's give the user a way to dismiss the modal. This provides a nice user experience because the user can then exit the workflow at any time by tapping the Close button. We'll place a Close button on the HeaderBarPlugin that will dismiss the modal. This step involves creating an event handler for the HeaderBarPlugin and then calling hide() on our ModalViewPlugin instance.

In the constructor function for the ModalController, let's add the following code just before the closing brace:

var ModalController = function(...) {
    // … existing code
    var self = this;

    headerBar.setRightTitle('Close', 'close');
    headerBar.on('click:close', function() {
        self.hide();
    });
};

There are a few interesting things going on here that are important. The first is that we've created a variable named self that points to the current instance of the ModalController.

Next we set a right title on the HeaderBarPlugin and give it the ID of "close" (the ID is the second parameter). When the user taps this title (which will be a button) the header bar will raise an event named click:close.

NOTE: The event name is composed using a template click:{ID} (where {ID} is replaced with the ID you used in the setRightTitle() call).

Finally, we subscribe to this event and close the modal by calling the close() method on self (which is the current instance of the ModalViewController).

In the event handler function we used self.hide() to reference the modal instance instead of this.hide(). This is because this within the event handler function is not bound to the ModalController instance, but instead is dynamic depending on how the function is called when the event is raised. Rather than dealing with this we use a convention of declaring a self variable that points to this before we set up the event subscription and just use self within the event subscription callback. You can read all about how this works in JavaScript in the Mozilla Developer Network's this documentation.

Workflow Completion

The other way that a modal is commonly dismissed is through the completion of some workflow (for example, from the confirmation page of a checkout flow). In this case it'd be natural to place a button or link on the final page of the workflow that is hosted within the modal that tells the ModalController to close the modal.

To do this, let's create an RPC method in the ModalController that any hosted page can call to tell the modal it should close. In the constructor function, let's add the following code just before the closing brace:

var ModalController = function(...) {
    // … existing code
    var self = this;

    Astro.registerRpcMethod('close-checkout-modal', [], function() {
        self.close();
    });
};

This registers an RPC method that any page can call after it loads the astro- client.js file. The second parameter is the list of arguments this RPC method takes. We don't need any so it's an empty array ([]).

For this example, let's say that the modal is hosting a checkout flow. On the final page of the checkout (the order confirmation) we'll place a link that dismisses the modal. Place the following code just before the closing </body> tag.

<a id="close-checkout" href="#">Close this window</a>
...
<script href="http://assets.mobify.com/astro/astro-client-1.0.0.min.js"></script>
<script>
    var closeCheckoutModal = Astro.jsRpcMethod('close-checkout-modal', []);
    $('#close-checkout').on('click', function(e) {
        e.preventDefault();
        closeCheckoutModal();
    });
</script>
</body>
</html>

Once we've loaded the Astro client we can get a reference to the 'close-checkout-modal' function. You can see the Close anchor as part of the markup above our two <script> tags. We use jQuery to bind a click handler to the anchor element. In the handler we simply call the RPC method which will close the modal.

Challenges with Modal Views

Moving some part of the user interface into a modal view often leads to a better, and more "app-like", experience. Most often, you will host a page from the website inside the modal. One challenge is when the modal you want to present is embedded in the page that you open it from. Often the app design will call for moving content in a "web modal" (a <div> that is styled to look like it overlays the entire page and be modal, for example) to a native/Astro modal.

An example of the modal pattern implemented on a website. This example uses an open source component called Pinny to slide a <div> element up and cover the full content area of the browser.

Figure 1 - Simulated modal web content.

This is challenging because we often display web content in Astro, but when you present a modal, you actually use a second, different web view plugin (or WebViewPlugin) to show the modal content. This means another page load from the server.

Adaptive.js

Adaptive.js projects often include a component named Pinny. This component does exactly what was described above (it presents "modal" content within a page but that content is part of the page, no browser page load occurs when it opens). This is problematic for situations where you want to present the Pinny content in an Astro modal.

Solutions

Now that we understand the problem let's talk about solutions. There are two basic approaches to this problem:

1. Load page a second time

The easiest option is to load the page a second time within the modal and trigger the flag that makes it show the modal. Many pages do this by using a fragment change to signal the page to convert to it's "modal" look. For this example, let's assume that the page we are on is /products/DC-Court-Graffik-Skate-White. When the user adds the shoes to the cart, the page displays a Pinny modal with the available sizes. When it does this it changes the url to /products/DC-Court-Graffik-Skate-White#size. Once the user selects a size the Pinny modal closes and the page automatically navigates to the cart which now includes the selected shoe in the right size.

Let's build a modal controller to see how this all works together.

// SelectSizeController
import Astro from ('astro/astro-full');
import AnchoredLayoutPlugin from ('astro/plugins/anchoredLayoutPlugin');
import HeaderBarPlugin from ('astro/plugins/headerBarPlugin');
import ModalViewPlugin from ('astro/plugins/modalViewPlugin');
import WebViewPlugin from ('astro/plugins/webViewPlugin');

var SelectSizeController = function(modal, anchoredLayout, headerBar, webView) {
    this.modal = modal;
    this.anchoredLayout = anchoredLayout;
    this.headerBar = headerBar;
    this.webView = webView;

    // Allow the hosted web page to ask to be closed
    // Once the user has selected a size we could use Astro.trigger('size-selected')
    // which would result in this handler being executed and close this modal
    var self = this;
    this.webView.on('size-selected', function() {
        self.close();
    });
}

SelectSizeController.init = function() {
    return Promise.join(
        AnchoredLayoutPlugin.init(),
        HeaderBarPlugin.init(),
        WebViewPlugin.init(),
        ModalViewPlugin.init(),
        function(modal, anchoredLayout, headerBar, webView) {
            modal.setContentView(anchoredLayout);

            anchoredLayout.addTopView(headerBar);
            anchoredLayout.setContentView(webView);

            headerBar.setCenterTitle('Select a Size', 'select-size');
            headerBar.setLeftTitle('Close', 'close');

            return new ModalController(
                modal,
                anchoredLayout,
                headerBar,
                webView);
            });
}

SelectSizeController.prototype.selectSize(productUrl) {
    this.webView.navigate(productUrl + '#size');
    this.modal.show();
}

SelectSizeController.prototype.close() {
    this.modal.close();
    // Reset webview so user doesn't get a glimpse of the last product
    // when we re-open this modal to a different product
    this.webView.navigate('about:blank');
}

We now have a modal that we can pass a productUrl to and it will open the size selector modal for that product. That page can also trigger an Astro event once the user has selected a size and it will close. At that point you would need to hook up code to navigate the original web view to the cart.

2. Refactor web modal

The second option, if you are able to make server-side changes, is to refactor the web modal. In this option you would move the modal markup to a new page and make it so you could pass in the product identifier in some way. Now when you load the product URL it wouldn't include any logic or markup for the modal except the minimum amount to tell Astro it needs to open a modal. Once Astro is triggered, it would still open a modal as in the above example, but the URL it navigates to would be for the new page and it would pass in the product identifier.

Choosing the best option

In almost every case, option #2 is preferable because it avoids reloading the original page a second time in the Astro modal. This is often not possible though because many Astro apps (and Progressive Web-powered websites) cannot change what markup the server renders.

Summary

In this section you learned about modals and how to use them effectively. A ModalViewPlugin is just a container that you can populate with whatever content you want. Once you've set up the content for the modal you can show it. You can provide UI for the user to close the modal or close it automatically at the end of a workflow. Finally, you've seen some basic principles for adapting an existing web page that incorporates "web modals" into an Astro modal view.