Tuesday, January 27, 2009

Unobtrusive Javascript Examples with Prototype

There is a well known design pattern when developing a web application known as MVC (Model-View-Controller). We can find an analogy of this design pattern with web applications that run in the client browser. We know the advantages of this separation. We can find an equivalence between the HTML structure and the Model, the CSS presentation and the View, and the JavaScript behavior and the Controller. Now it should be easier to understand why it is a sensible decision to remove style attributes and tags and JavaScript code mixed in the HTML file (MVC analogy article).

The following example shows a way to make this separation effective. The example uses three files: an HTML file (the model or structure), a CSS file (the view or presentation) and a Javascript file (the controller or behavior). We use prototype in order to change the behavior of input fields with class value "required". When the page is fully loaded, javascript will change the default action of the onSubmit event to trigger a new behavior that will validate the form. When the user submits the form, an alert will show up indicating a list of the empty fields that are required. If javascript is disabled or the browser does not accept javascript, the page will still be fully functional.

The basic document structure of our HTML tag will include the prototype.js library and our own set of functions that will be packed into a file named startup.js. In our examples these two files will be stored in folder js/.

startup.js:

document.observe("dom:loaded", function() { 
   // javascript code to be executed when the dom is loaded 
}); 

example-01.html:

<html>
   <head>
      <link type="text/css" rel="stylesheet" href="css/style.css"></link>
      <script type="text/javascript" src="js/prototype.js"></script>
      <script type="text/javascript" src="js/startup.js"></script>
   </head>
   <body>
      <h1>Request information form</h1> 
      <form name="info_rq" method="post" action="info_request.php" id="info_request">
         <p>
            <label for="e-mail">e-mail</label>
            <input name="e-mail" value="" class="required" />
            <span id="error-e-mail">*</span>
         </p>
         <p>
            <label for="comment">your comment</label>
            <textarea name="comment" value="" cols="80" rows="5" class="required"></textarea>
            <span id="error-comment">*</span>
         </p>
         <input type="submit" value="Send comment" />
      </form>
      <p>The * indicates a required field</p>
   </body>
</html>

Now it's time to start designing the code to be executed when the dom is loaded. These are the steps we'll follow to accomplish our goals:

  1. Design the function to be attached to the onSubmit event
  2. Capture the onSubmit event and attach the previously designed function

startup.js:

document.observe("dom:loaded", function() {
   // javascript code to be executed when the dom is loaded
   $('info_request').onsubmit = function() {
      var sError = 'The following fields cannot be empty:\n';
      var sField = '';
      $$('.required').each(function(element) {
         if (!element.present()) {
            sField+= element.name+'\n';
         }
      });
      if (sField!='') {
          alert(sError+sField);
          return false;
      }
      else {
          return true;
      }
   }
});

Now, as you can see, the HTML code is free from any inline behavior attributes, or script tags, with one exception. All the necessary javascript files are described in the head of the document. We have achieved the behavior separation goal.

One of the advantages of this separation is that we can device a completely different behavior for the onSubmit event without changing the HTML file at all. In this second example, when the user submits the form, every empty input field of class required will display a red text next to it indicating the error. Let's see how to do it.

startup.js:

document.observe("dom:loaded", function() {
   // javascript code to be executed when the dom is loaded
   $('info_request').onsubmit = function() {
      var sError_field = '';
      var bError = false;
      $$('.required').each(function(element) {
         sError_field = 'error-'+element.name;
         if (!element.present()) {
            bError = true;
            $(sError_field).update('* EMPTY').addClassName('empty');
         }
         else {
            $(sError_field).update('*').removeClassName('empty');
         }
      });
      if (bError) {
          return false;
      }
      else {
          return true;
      }
   }
});

The style.css file has only one rule that presents all elements of class "empty" in the HTML file in red color. That is why we call the .addClassName('empty') or .removeClassName('empty') in the startup.js file.

style.css:

.empty {
   color:red;
}
In the third example you'll see how to show a confirmation popup when you click a link that targets a deletion action. This is the HTML code:
<html>
   <head>
      <link type="text/css" rel="stylesheet" href="css/style.css"></link>
      <script type="text/javascript" src="js/prototype.js"></script>
      <script type="text/javascript" src="js/startup-03.js"></script>
   </head>
   <body>
      <h1>List of newspapers</h1> 
      <p>Click on the 'X' to delete a register. Confirmation will be requested.</p>

      <ul>
         <li><a href="remove_newspaper.php?id=1" class="confirm">X</a> La Vanguardia</li>
         <li><a href="remove_newspaper.php?id=2" class="confirm">X</a> El Mundo</li>
         <li><a href="remove_newspaper.php?id=3" class="confirm">X</a> El Pa&iacute;s</li>
         <li><a href="remove_newspaper.php?id=4" class="confirm">X</a> El Peri&oacute;dico</li>
      </ul>
      <h2>Remove register</h2>
      <form method="post" action="remove_newspaper.php?id=5">
         <label>Newspaper</label> <input type="text" name="newspaper" value="El Marca" />
         <input type="submit" value="Delete" class="confirm" />
      </form>
   </body>
</html>
The correspondig javascript file captures the onClick event

startup-03.html:

document.observe("dom:loaded",function() {
    $$('.confirm').each(function(element) {
        element.observe("click",function(event) {
            if (!confirm('Are you sure to perform this action?')) {
                event.stop();
            }
        })
    });
});

No comments: