Tutorial: Modal Dialogs in Visualforce using the Yahoo! User Interface Library

Tutorial: Modal Dialogs in Visualforce using the Yahoo! User Interface Library

Introduction

Visualforce is a powerful user interface framework, and when developers work with Visualforce for a while they will often comment that many things are easy and simple. However, occasionally we've heard that getting popups right is difficult. This short tutorial will introduce a method that allows developers to build popup functionality into their Visualforce pages and still retain the simplicity of working in a single page. Specifically, we will cover the problem of modal dialogs, which popup above a Visualforce page.

Problem Statement

Building a link or button to popup a new Visualforce page is actually quite simple, and getting this popup to be above the current page is also quite easy. The problem occurs when you would like to collect information in the popup and pass that information back to the page that launched the popup.

The issue is that the new window is launched as a separate browser request when you use window.open(). Since this is a separate request to the server, the new page does not share the same controller context/session. Even if the two pages both use the name of the same controller! This is due to the fact that these are two different requests at the browser level.

So, how do you pass information from one page to another in this case? You would be forced to use a query string parameter, passed from the popup window back to the opener window. The issue here is that the parent page will reload and lose its context. Ouch. This tutorial provides a solution to this problem.

DHTML Solution

While it sounds serious, there is a straightforward method to build a popup and make it modal if required. This technique is sometimes called In-Page DIV or Hidden DIV.

You have many options to choose from with the proliferation of various AJAX toolkits. Almost every AJAX toolkit provides the framework for creating containers of various flavors for various purposes. Selecting the right toolkit or framework is likely going to be the hardest part of the process. Each has its pros and cons and you should make sure you choose the one that best fits your needs.

After some experimentation, we decided to use the Yahoo! User Interface Library (YUI), more specifically the YUI Panel class, an implementation of the YUI Container class. We chose this one because it is pretty easy to use and has a robust set of configuration options. Most modern AJAX frameworks allow you to extend the objects they provide, and YUI is no exception. Because of this extensibility, if the Panel is not exactly what you need you can extend it for your specific circumstance.

We will now develop a simple solution around this library. This example will implement a nice popup that lives in the same page, with the same context and controller, so that all the information gathered is available in one place. It really is an elegant solution to the "how do i popup a dialog with Visualforce?" question.

Visualforce Code

The key to learning how to write your own popups is to write your own code. But before we dive in, take a look at a live demo of the finished product running on Force.com Sites. It produces a simple page that provides a button to launch a popup dialog, as depicted in the following figure:

Image:VFPopupExample.jpg

The code for the demo is listed below. Let's look at the code, after which we will describe the steps to get this working. It's a simple example, and shows all the important aspects of making the popup functionality work for you.

<apex:page standardController="Account" >
 
<apex:styleSheet value="http://yui.yahooapis.com/2.6.0/build/assets/skins/sam/skin.css" />
 
<apex:includeScript value="http://yui.yahooapis.com/2.6.0/build/yahoo-dom-event/yahoo-dom-event.js" />
<apex:includeScript value="http://yui.yahooapis.com/2.6.0/build/container/container-min.js" />
<apex:includeScript value="http://yui.yahooapis.com/2.6.0/build/animation/animation-min.js" />
 
<script>
    
    // Create a namespace for our custom functions
    YAHOO.namespace("force.com");

    // Function called when we want to show the dialog
    YAHOO.force.com.showMe = function() {
        document.getElementById("myPanel").style.display = "block";
        YAHOO.force.com.myDialog.show();
    }
   
    // Function called when we want to hide the dialog
    YAHOO.force.com.hideMe = function() {
        YAHOO.force.com.myDialog.hide();
    }

    // Function called when the DOM is ready to create the dialog,
    // render the dialog into the document body, add our dialog skin
    // css to the body tag, and wire up the buttons on our dialog    
    YAHOO.force.com.init = function() {
        document.body.className = document.body.className + " yui-skin-sam";
        
        YAHOO.force.com.myDialog = new YAHOO.widget.Panel(
            "myPanel",  // The id of our dialog container
            { 
                    width           :   300,    // You can play with this until it's right
                    visible         :   false,  // Should be invisible when rendered
                    draggable       :   true,   // Make the dialog draggable
                    close           :   false,  // Don't include a close title button
                    modal           :   true,   // Make it modal
                    fixedCenter     :   true,   // Keep centered if window is scrolled
                    zindex          :   40,     // Make sure it's on top of everything
                    
                    // This line adds the appear/vanish fade effect
                    effect          :   {
                                          effect:YAHOO.widget.ContainerEffect.FADE,
                                          duration:0.35
                                        } 
            }
         );
        
        // Render the dialog to the document.body level of the DOM
        YAHOO.force.com.myDialog.render(document.body);
    }
    
    // Add the init method to the window.load event
    YAHOO.util.Event.addListener(window, "load", YAHOO.force.com.init);
</script>

<!-- This is the page that we want to display to the user -->
<apex:outputPanel >
  <apex:pageBlock title="Basic Modal Dialog" id="none">
    <apex:pageBlockButtons >
        <input type="button" class="btn" 
                    onclick="YAHOO.force.com.showMe();" 
                    value="Popup Demo" />
    </apex:pageBlockButtons>
    <apex:outputPanel layout="block">
      <apex:outputPanel layout="block" style="margin-bottom: 10px;">
        <apex:outputLink value="http://developer.yahoo.com/yui/container/" target="_blank">
          Yahoo Developer Network - Container
        </apex:outputLink>
      </apex:outputPanel>
      <apex:outputPanel layout="block" style="margin-bottom: 10px;">
        A basic modal dialog with minimal styling and without any additional
        settings. The basic overall style is set by the CSS defined in the skins.css
        file included at the top of the page source. The example Visualforce 
        page will collect information and place it back on this page.
      </apex:outputPanel>
    </apex:outputPanel>
  </apex:pageBlock>
</apex:outputPanel>

<!-- This is the content of the modal dialog -->
<div id="myPanel" style="display: none" >
  <div class="hd">
    <apex:outputText value="Cool Modal Dialog" />
  </div> 
  <div class="bd">
      <apex:form >
        <apex:pageBlock >
          <apex:pageBlockSection columns="1">
            <apex:inputField id="fName" value="{!account.name}" />
          </apex:pageBlockSection>
        </apex:pageBlock>
        <div style="text-align: right;" >
          <apex:commandButton value="Select" 
              oncomplete="YAHOO.force.com.hideMe();" />
          <apex:commandButton value="Cancel" immediate="true" 
              oncomplete="YAHOO.force.com.hideMe();"/>
        </div>
      </apex:form>
  </div>
  <div class="ft" style="font-size: 10px;">
    <apex:outputPanel layout="block">
      The dialog in this demo is using a "hidden" DIV containing and contains 
      its own Visualforce Form. The information entered into the inputText 
      component is bound to data in the Apex Controller.
    </apex:outputPanel>
  </div>
</div>
</apex:page>

This page can be considered as four discrete parts; imports, plumbing, page content and dialog markup that enable the modal dialog. The following sections discuss each of these parts in turn.

Section One - Imports

The first part of the code includes a stylesheet and a number of JavaScript files. Please note that we are using scripts and style sheets that are hosted by Yahoo on their web properties. If you want to be completely certain that nothing will change over time, download the scripts and style sheets from Yahoo and reference them as static resources instead. All the necessary style sheets and JavaScript libraries are available at the YUI site. The three scripts we import enable creating the Panel, hooking up events, enabling drag and drop (if desired) and supporting basic animation.

Section Two - Plumbing

Although you might not, in practice, construct your page from the top to the bottom following these sections, you certainly could with enough understanding of the moving parts. The plumbing section is what will turn your vanilla HTML/Visualforce components into a pleasant and efficient user experience. We've annotated the JavaScript shown in the sample so that you can better understand what is happening. But we'd also like to highlight a few important aspects as well.

The very first line of JavaScript is an important one.

    // Create a namespace for our custom functions
    YAHOO.namespace("force.com");

We are defining a "namespace" in which any custom functions and JavaScript objects that we create will execute within. A namespace is a useful concept because it provides a degree of isolation and protection from other JavaScript that might be running on your page. By defining a namespace and using it for variable definition we can avoid "overwriting" variables of the same name that we may not be aware of. Because we are using namespaced variable and function definitions, we need to put our script above the rest of the page markup so that the functions and objects we create and reference in the markup are available when the page loads.

In this sample we are using a standard HTML input tag to display the dialog:

       <input type="button" class="btn" 
            onclick="YAHOO.force.com.showMe();" 
            value="Popup Demo" />


This next area that we want to highlight is the Yahoo.force.com.init function.

    // Function called when the DOM is ready to create the dialog,
    // render the dialog into the document body, add our dialog skin
   // css to the body tag, and wire up the buttons on our dialog    
    YAHOO.force.com.init = function() {
        document.body.className = document.body.className + " yui-skin-sam";
        
         YAHOO.force.com.myDialog = new YAHOO.widget.Panel(
            "myPanel",  // The id of our dialog container
            { 
                    width           :   300,    // You can play with this until it's right
                    visible         :   false,  // Should be invisible when rendered
                    draggable       :   true,   // Make the dialog draggable
                    close           :   false,  // Don't include a close title button
                    modal           :   true,   // Make it modal
                    fixedCenter     :   true,   // Keep centered if window is scrolled
                    zindex          :   40,     // Make sure it's on top of everything
                    
                    // This line adds the appear/disapper fade effect
                    effect          :   {
                                          effect:YAHOO.widget.ContainerEffect.FADE,
                                          duration:0.35
                                        } 
            }
         );
        
        // Render the dialog to the document.body level of the DOM
        YAHOO.force.com.myDialog.render(document.body);
    }

This function is called once the page is finished loading all the scripts, CSS and markup. Init is what actually turns a run-of-the-mill DIV tag into the more capable modal dialog. There are two very important lines in this function, that if left out or modified will result in weird and hard to track down behavior.

The first line that you need to make sure is in every page in which you use this code is the first line of the function:

        document.body.className = document.body.className + " yui-skin-sam";

This line adds a style selector to the body tag of the page. Notice that we are not just setting the className of the body tag to the one shown. Instead, we are adding it to whatever other existing style class has been previously set. If you just set it, then your page will look strange and you will spend a lot of time trying to figure out why.

The second line to pay attention to and understand is the last line of this function:

        // Render the dialog to the document.body level of the DOM
        YAHOO.force.com.myDialog.render(document.body);

It does not need to be the last line, by the way. This line renders the dialog into the DOM. When the dialog is created there are elements that are created to "wrap" existing markup, or contain dynamically set markup for the dialog as well as elements for the modal mask and so on. Where this object is rendered is important. For the most consistent behavior in Visualforce you should render your dialog as a direct descendant of the body tag. Hence the single argument to the render function document.body. This will put your dialog in the DOM hierarchy directly underneath the body tag.

Now, the rest of the function (which happens to only be one line in this case) is where you define the attributes of your dialog that determine content, behavior and look and feel. There are many more possible configuration options available than those presented above; see the YUI site for details.

Section Three - Page Content

If you are familiar with Visualforce, then there should be no surprises here. The page content is what is rendered to the user when the page first loads. A couple of points of interest need to be mentioned.

If your page has a form, then make sure you add an enclose your content in an actionRegion. The next section that we will describe, the dialog markup, also has a form. Segmenting your page into a "content" section and sections for each dialog that you might have (yes, you can put more than one dialog into a page) and enclosing each in an actionRegion will ensure that you are submitting the correct data for each post.

In our sample, we use a standard controller. We chose the Account object as an example - you can use any object you like. You would typically use whatever controller and/or extensions that make sense for your page. As mentioned we use a standard HTML element and hook the onclick event:

    <apex:pageBlockButtons >
            <input type="button" class="btn" 
                     onclick="YAHOO.force.com.showMe();" 
                     value="Popup Demo" />
    </apex:pageBlockButtons>

The onClick event calls our handler, YAHOO.force.com.showMe(), to display the dialog.

Section Four - Dialog Markup

The last section of the page is what defines the content of the dialog. Although this is not the only technique for specifying dialog content, we like this technique because you can visually setup the dialog content and then wrap it in the appropriate structure.

This section has a specific structure that makes it easy to properly render your dialog. The dialog itself is made up of three parts - the header, the body and the footer. The structure that we employ here maps to the dialog structure. We have an outer div that defines the whole of the dialog, and three child divs of the outer div that correspond to the header, body and footer of the dialog.

<div id="myPanel">
    <div name="header" class="hd" />
    <div name="body" class="bd" />
    <div name="footer" class="ft" />
</div>

Once this simple structure is in place, you can add any markup or Visualforce components as children of the header, body or footer. To get the dialog parts to render properly, remember to assign the appropriate style class to each one - header class = "hd", body class = "bd", footer class = "ft" - as shown above

Dialog Content

The dialog content is the div with the id - myPanel, and wraps the three parts of the dialog. You must give the Dialog Content div an id so that you can use that id when creating the dialog as we did in the init function above.

<!-- This is the content of the modal dialog -->
<div id="myPanel" style="display: none" >

Notice also that we use an inline style so that the dialog is not visible and takes up no space when the page is rendered. Leaving this out will cause your dialog to be visible when the page loads and to then vanish when the plumbing is completed. Because we are setting the display style to none explicitly here, we need to set it to block explicitly when the dialog is first shown. Take a look at the YAHOO.force.com.showMe() function to see this.

	// Function called when we want to show the dialog
    YAHOO.force.com.showMe = function() {
        document.getElementById("myPanel").style.display = "block";
        YAHOO.force.com.myDialog.show();
    }

(We don't need to set the display property back to block when hiding; the Yahoo Panel object handles this for us.)

Dialog Header

Dialog with Close Button

The header is where you put the markup that you want to appear in the "title bar" of the dialog. Keeping the header simple and descriptive is a good practice. In addition to the markup in your header div, the header can also contain a "close" box that will immediately close the dialog.

This sample doesn't use the close box, but instead uses the "Cancel" button.

Dialog Body

The body is where the main action and most of your focus resides. In the case of our sample, we wanted something more that a simple "Hello World", so we've built the body like a mini Visualforce page. We use a pageBlock, pageBlockSection, inputField and commandButtons. The dialog will (mostly) render this faithfully using all the styling provided by Visualforce.

  <div class="bd">
      <apex:actionRegion >
        <apex:pageBlock >
          <apex:pageBlockSection columns="1">
            <apex:inputField id="fName" value="{!account.name}" />
          </apex:pageBlockSection>
        </apex:pageBlock>
        <div style="text-align: right;" >
          <apex:commandButton value="Select" action="{!save}"
              oncomplete="YAHOO.force.com.hideMe();" />
          <apex:commandButton value="Cancel" immediate="true" 
              oncomplete="YAHOO.force.com.hideMe();" />
        </div>
      </apex:actionRegion>
  </div>

Note the use of actionRegion in this sample. Although not strictly required, it is a good practice to use actionRegions for your dialogs. These define discrete elements that will be posted to the controller. Since our dialog is itself a fully contained mini Visualforce page, we want to ensure that only the data in our dialog is sent to the controller.

We have two commandButtons on the page: one for submitting the data - "Select" and one for canceling the operation - "Cancel". Consider the case where, using your dialog, you want to create a new record that is somehow related to the main object that is the focus of your page.

          <apex:commandButton value="Cancel" immediate="true" 
              oncomplete="YAHOO.force.com.hideMe();" />

For instance, on an Account page you might use the dialog to create a new Contact. The controller that the dialog will post data to and bind its form elements to, will instantiate a new Contact record. If your dialog has a firstName and lastName field, bound to the new Contact record, then whatever data is on that new Contact record will display in the form fields of your dialog.

Suppose a user starts to create a new Contact, gets as far as the first name, then decides not to finish creating the record. If you simply allow the user to close the dialog, then the next time the dialog is shown (during that page session) the values previously entered will still be in the form fields. The Contact creation was not really cancelled, more like delayed. To resolve this, you would create a cancelCreate action in your controller that re-initialized the new Contact record, effectively un-setting what ever values may have been entered.

The other button on our dialog is the "Select" or Ok button.

          <apex:commandButton value="Select" action="{!save}"
              oncomplete="YAHOO.force.com.hideMe();" />

This button is used to indicate that the user is done entering data and wants to submit the response to the system. For the sample we are calling the {!save} action. Once that submission has completed the YAHOO.force.com.hideMe() JavaScript function is called to hide the dialog once again, and the new record page is shown.

Configuring Code

To run this code yourself, you can install the AppExchange package that includes the page and static resources using the Get It Now link below.

Get It Now

That's it, now you are ready to run the simple example.

When you install this app and select it from the AppExchange app picker, you can click on the demo button to open the popup. You will be able to enter a string and see that string placed into the parent page by clicking on the Select button.

Summary

This article shows the ease with which you can leverage AJAX libraries to enable better UI design in you Visualforce pages. I hope you enjoy experimenting with this component as well as other component in the YUI library, and indeed other AJAX frameworks.

Please let us know about your experience with this and other libraries. Feel free to post tips and tricks so that the community at large can experience the joys and challenges of innovating of the Force.com platform. Just edit this page with your feedback!

Thanks and cheers!

References

  • Read more about YUI

About the Authors

Ron Hess & Dave Carroll 5 February 2009 (PST)

Developer Feedback?

Drop us a note here..