zerosum dirt(nap)

evolution through a series of accidents

zerosum dirt(nap)

YUI Dialog On Rails: First Pass

November 18, 2006 by nap · Comments

RoR seamlessly integrates with Prototype and the Scriptaculous library, buying us slick auto-complete fields, sortable lists, some nifty effects and all sorts of other web2.0-ish goodies. But when it comes to more advanced UI toolkit widgets, ones you’d find in desktop app frameworks, like draggable modal dialogs, tabbed panels, and active data grids, it seems hard to beat the library that Yahoo has put together. [That is, unless you want to start mucking with Adobe’s Flex. But that’s a topic for another conversation…]

The Yahoo UI Library has been around for a while now (since February) and I’m just now getting around to playing with it. Unfortunately, there’s no Rails helper module or anything of that sort. But that doesn’t mean we can’t build one ourselves, or at least make them play nice together…

Let’s start with something simple. Oh yeah, I should mention that Sonjaya Tandon covered this same sorta stuff and what I’m doing is based on what I found there, but I’m going to do it a little bit differently, and demonstrate how you can leverage RJS. Here we go:

First, let’s set up a Rails project and create a controller. Call it ExampleController:

class ExampleController < ApplicationController
  layout "standard", :except => :add

  def show
  end

  def add
    @thing = params[:thing]
  end
end

Pretty simple stuff. We’ll need an RHTML view template for the show action, and an RJS template for our add action. We’ll also need a layout that’ll be added to everything but our add action. Let’s take a look at that layout (standard.rhtml):

<html>
<head>
  <title>YUI Tester: <%= controller.action_name %></title>
  <%= javascript_include_tag :defaults %>
  <%= javascript_include_tag "yui/yahoo", "yui/dom", "yui/event", "yui/connection", "yui/dragdrop", "yui/container" %>
  <%= stylesheet_link_tag  'yui/container'%>
  <script language="javascript">
    YAHOO.namespace('yuitest');

    function initDialog() {
      var handleCancel = function() {
        this.cancel();
      }

      var handleSubmit = function() {
        //this.submit();
        new Ajax.Request('/example/add', {asynchronous:true, evalScripts:true, parameters:Form.serialize(myDialogForm)});
        this.hide();
      }

      YAHOO.yuitest.myDialog = new YAHOO.widget.Dialog("dlg", {
        width: "500px",
        modal: true, 
        visible: false,
        fixedcenter: true, 
        constraintoviewport: true, 
        draggable: true });

      var myButtons = [ { text: "Save", handler: handleSubmit, isDefault: true },
        { text: "Cancel", handler: handleCancel } ];    
      YAHOO.yuitest.myDialog.cfg.queueProperty("buttons", myButtons);
      YAHOO.yuitest.myDialog.render();
    }

    function addThing() {
      document.myDialogForm.thing_name.value = "";
      YAHOO.yuitest.myDialog.show();
    }

    YAHOO.util.Event.addListener(window, "load", initDialog);
  </script>
</head>
<body>
  <div id="main">
    <%= @content_for_layout %>
  </div>
</body>
</html>

So there’s a bunch of JavaScript in there. I’m going to work on cleaning all that up and building a helper module later but for now let’s just get something functional.

We’ve copied the relevant YUI library javascripts to our public/javascripts directory, and now we include them in the layout along with our defaults (prototype, etc). We’d want to trim this down to just what we need before deploying an app for real world use of course.

We’re going to create a dialog from the YUI and add two buttons on it (save, cancel) and the corresponding event handlers. We also need to add a function addthing() that will be called when the user clicks on the link in the show template. It will simply reveal our modal dialog. If you’re confused about any of the syntax here, see the YUI docs and tutorials. Lots of good stuff over there, extremely well documented.

Now here’s our show template (show.rhtml):

<div name="hello_msg" id="hello_msg">hello</div><br />
<a href="#" onclick="addThing()">Add Thing</a>
<!-- begin: dialog box -->
<div id="dlg">
  <div class="hd">this is a dialog</div>
  <div class="bd">
    <%= form_tag({:action => :add}, {:name => 'myDialogForm'}) %>
      <label for="thing_name">Thing Name:</label> <%= text_field('thing', 'name') %>
    <%= end_form_tag %>
  </div>
</div>
<!-- end: dialog box -->

Note that the form name (myDialogForm) must match up. Also note that the dlg DIV uses the same name we initialize the dialog to in the layout. This is important, it means the contents of this div are rendered within the modal dialog and are therefore hidden from the view by default. When you click on the ‘Add Something’ link, the dialog will pop up. You’ll be able to drag it around the screen and enter some text in it. Yay. Then you can click save, and it should update the hello_msg DIV. There’s an easier way to do this of course, but we’re going to use RJS because the approach is demonstrative of a lot more powerful stuff you can do, leveraging Rails model data you may have on the backend and perhaps doing some processing or database access before rendering the results. You get the idea.

So when you hit that save button, the code in the submit handler is run. Let’s look at that JavaScript again:

new Ajax.Request('/example/add', {asynchronous:true, evalScripts:true, parameters:Form.serialize(myDialogForm)});
this.hide();

We serialize the data in the form (that one text field) and submit the data asynchronously by making a direct call to Ajax.Request (thanks Prototype!). Then we hide the model dialog again, returning control to the main page. Notice that URL: /example/add. This calls the add action on our ExampleController, which does something important — in theory — and then renders out an RJS template. This RJS template, for our simple example, just replaces the hello_msg div’s inner HTML with whatever it was we typed into that text field. Here’s the RJS template, add.rjs:

page.replace_html 'hello_msg', @thing[:name]

That’s it for now! We’ll revisit this later if I have time, and try to figure out how to wrap it up in a module to make it easier to use and more general purpose.

blog comments powered by Disqus