Links For 11.24.07
- JRails - Drop-in JQuery replacement for Rails prototype/script.aculo.us helpers.
- NetBeans 6.0 RC2 - Available for download.
- Gemtacular - Rate and Review Ruby Gems.
- LiveConsole - IRb over TCP remote console. Cool idea.
- Google TV?
- Rake tasks for QueryTrace
- Rails 1.2.6 - Includes an important security fix. gem update rails -y
JQueryCamp Is Tomorrow!
For anyone who's registered (and presumably in or around the greater Boston area), don't forget that JQueryCamp is tomorrow! I'll be there, and looking forward to talks by John Resig, Yehuda Katz (whom I probably owe a beer), and many others.
Rails 1.2.1, Prototype 1.5, Other Goodies...
Wow what a big day. Immediately after the release of Rails 1.2.0, we get a quickie bugfix bump to 1.2.1 and a great post by DHH summarizing the features in the release. On top of this, we get Prototype 1.5.0, complete with a new web presence and some surprisingly good documentation.
Stupid JavaScript Hacks
JavaScript Tricks (RJS-R): Cleaning Up My Mess...
So a couple folks have pointed out that the last Rails+YUI example I posted doesn't work in IE. Or Safari. Eek, that's not so good.
Anyway, this post is kind of a hodgepodge documenting the process I went through to fix those issues and clean it up a bit. Maybe more of a 'note to self' than an actual blog entry, so not required reading by any means. Unless you're having issues with IE and Minus MOR that is, in which case the magic word is content-type. I'm embarassed to say how much sleep I lost tracking that one down. Sigh.
class ExampleController < ApplicationController
layout "standard", :except => :add
def show
end
def add
@response.headers['content-type'] = 'text/javascript';
@thing = params[:thing]
end
end
That's our updated ExampleController. Notice that we're setting the content type of the response in the headers now. The default content type appears to be html, instead of text/javascript. Not entirely sure why this is happening at the moment as Minus-R appears to set the content-type in it's render method. But anyway, for whatever reason, Safari and Firefox both work fine, but IE doesn't like it one bit. Of course, instead of warning us (or giving us an option to warn us, for that matter) it simply discards the asynchronous response. Hence, we never see an update. Nice, eh?
I also took the opportunity clean up the rest of our example a little bit. Here's our new layout template:
<html>
<head>
<title>YUI Tester: <%= controller.action_name %></title>
<%= javascript_include_tag "yui/yahoo", "yui/event", "yui/dom", "yui/dragdrop", "yui/connection", "yui/container"%>
<%= stylesheet_link_tag 'yui/container'%>
<script language="javascript">
YAHOO.namespace("yuitest.container");
function init() {
var handleCancel = function() { this.cancel(); };
var handleSubmit = function() { this.submit(); };
var handleFailure = function(o) { alert("failure: " + o.responseText); };
var handleSuccess = function(o) { eval(o.responseText); };
YAHOO.yuitest.container.myDialog = new YAHOO.widget.Dialog("myDialog", {
width: "500px",
modal: true,
visible: false,
fixedcenter: true,
constraintoviewport: true,
draggable: true });
var escKeyListener = new YAHOO.util.KeyListener(document, { keys : 27 },
{fn:handleCancel,scope:YAHOO.yuitest.container.myDialog,correctScope:true} );
YAHOO.util.Event.addListener( 'myDialogForm', 'submit', function(e) {
YAHOO.util.Event.preventDefault(e);
YAHOO.yuitest.container.myDialog.submit();
});
YAHOO.yuitest.container.myDialog.cfg.queueProperty("keylisteners", escKeyListener);
YAHOO.yuitest.container.myDialog.cfg.queueProperty("buttons",
[{ text:"Save", handler:handleSubmit, isDefault:true },{ text:"Cancel", handler:handleCancel } ]);
YAHOO.yuitest.container.myDialog.callback = {
success: handleSuccess,
failure: handleFailure
};
YAHOO.yuitest.container.myDialog.render();
}
function addThing() {
YAHOO.yuitest.container.myDialog.show();
}
YAHOO.util.Event.addListener(window, "load", init);
</script>
</head>
<body>
<div id="main">
<% if flash[:notice] -%>
<div id="notice"><%= flash[:notice] %></div>
<% end -%>
<%= @content_for_layout %>
</div>
</body>
</html>
OK, a bunch of changes there. First, we've removed all the Prototype JS libs because we no longer need it -- YUI's connection manager can take care of this stuff for us, and since we're not using the default behavior of RJS, there are no worries about dependence on Prototype. Next, we've added a couple key listeners on the popup dialog to handle enter (submit) and escape (cancel). Note that we have to use Event.preventDefault() in our enter key listener to suppress the default form submission action. Otherwise, we end up redirected to a new page that just contains our result string, and we don't want that...
Finally, we've also eliminated the clumsy body of the success handler and replaced it with a single statement: eval(o.responseText). Yup, we can just evaluate the JavaScript returned from our Rails app. No need to append a new script tag to the body, yehck. Here's the code that's returned from our add.ejs template, as a reminder (it's unchanged):
document.getElementById('hello_msg').innerHTML = '<%=@thing[:name]%>';
So yeah, it just replaces the inner HTML in the element named hello_msg. Easy enough. The next step here would be to figure out how to encapsulate the stuff in the layout using some sort of helper module or plugin. But that's it for now.
(Progress on my current Rails project has been pretty slow lately, as we're nearing completion on a big client project (a slick Java-based webstart app that's been occupying the majority of my time for the past 6 or so months). It's nice to finally see the light at the end of the tunnel! Hopefully once that wraps, we'll have some significant time to pour into the RoR ideas and prototypes we've been playing around with...)
RJS & MinusMOR
RJS rocks. It lets me write JavaScript without writing any JavaScript. That's very Zen, and I like it. But sometimes I find myself writing really ugly, narsty things in my RJS templates. Instead of using Ruby to write JavaScript I end up writing JavaScript and appending it to the page with Ruby, especially when client side conditionals are involved. Needless to say, the template code quickly devolves into what can only be described as frankenrubyscript.
To rid ourselves of the monster, take a look at Dan Webb's MinusMOR plugin, which lets you return plain ol JavaScript to the browser using templates with an .ejs file extension. This may not seem like a big deal but it's pretty damn helpful if you return any significant amount of client-side logic within RJS.
[Note that you'll need to be on the Rails 1.2 codebase to use the plugin]
As a simple example, let's overhaul our sample YUI test app to use MinusMOR. Now this really isn't a great example, as the JavaScript code is super simple and therefore lends itself well to being written in Ruby. But oh well. At least it gives you an idea of how it works.
Our add.rjs file used to contain the following one-liner:
page.replace_html 'hello_msg', @thing[:name]
Now, our add.ejs file will contain the following code instead. Note the use of ERb in the template:
document.getElementById('hello_msg').innerHTML = '<%=@thing[:name]%>';
In this case the normal RJS approach is quicker, sexier, and, above all else, easier to read. And, admittedly, 90% of your client-side code will more than likely be the simple kind of stuff that RJS rocks at (update this text within that DIV, switch the visible state of that DOM element to hidden, etc). But you can certainly imagine scenarios where it'd make sense to write pure JavaScript in the template instead.
Use RJS wherever you can, but once bits and pieces of frankenrubyscript start sneaking into your code, be sure to check out Dan's plugin. Cuz there's no denying that there are cases where using plain ol JavaScript is cleaner, as un-Zen-like as that may seem....!
Prototype vs YUI Connection Manager: Dialog Continued...
In the last blog entry, I wrote about doing a first pass integration between Rails and the Yahoo UI Library's Dialog widget. In the event handler for the save action, we created an Ajax.Request object using the Prototype library to make the Ajax call but I didn't elaborate on why just calling this.submit() in the handler wouldn't work. Well, the answer is pretty obvious: I wanted to tease out a second article :-).
YUI expects us to set up a callback handler for the success/failure of the XMLHttpRequest request that it generates when this.submit() is called. If you don't have a callback set up, the client receives the response but just ends up throwing it away. So let's look into how we need to change our handler to do things the "right" way (according to Yahoo, anyway). We'll also see how this is somewhat at odds with Prototype and RJS.
YUI's Connection Manager utility exists to simplify your interface to the XMLHttpRequest object and provides a handler pattern for callbacks. It's used throughout their library, so let's use it here. This updated code should obviate the need for using Prototype in the body of the save action handler:
var handleSubmit = function() {
this.callback = {
success: function(o) {
var scriptObj = document.createElement("script");
scriptObj.setAttribute("type", "text/javascript");
scriptObj.text = o.responseText;
document.body.appendChild(scriptObj);
},
failure: function(o) {
// do something here
alert(o.responseText);
}
};
this.submit();
}
In the code listing above, we define the callback object to handle the success / failure cases reported by XMLHttpRequest. On success, we're going to create a new script element in the DOM and load it up with the response that our RJS template renders. Then we have to append the new element to the DOM. Remember, what we're appending here is really just some more JavaScript that we'll use to alter the page contents. Here's the code in that RJS template again (add.rjs), as a reminder:
page.replace_html 'hello_msg', @thing[:name]
Alright, that's great. But what does the JavaScript code look like that actually gets generated?
try {
Element.update("hello_msg", "Welcome Interstate Managers");
} catch (e) {
alert('RJS error:\n\n' + e.toString()); alert('Element.update(\"hello_msg\", \"sdafdsfsdff\");'); throw e
}
That Element.update instruction is Prototype's way of saying "take the DIV identified as hello_msg and replace it's guts with the string Welcome Interstate Managers". Of course, that's just the string we gave it when we serialized our form data and sent it off (see previous article for details). Simple but powerful.
OK so that rocks, but there's a bit of redundancy here. Yahoo's Connection Manager utility and Prototype do a lot of the same stuff. That duplication is totally against the DRY spirit we're going for. Sigh. Now I'm not well versed enough in the finer details to advocate The One True Way here, but I should note that I'm much more experienced with Prototype than I am with the YUI connection manager. Prototype is also much more entrenched in Rails, serving as the basis for both Scriptaculous and RJS, and it looks to remain that way as Sam Stephenson (Prototype's creator) is part of the Rails core team.
Anyway, the point I'm trying to make is that the Yahoo UI connection manager library adds further unnecessary bloat to our applications, since it's essentially just replicating what Prototype already does. I'm sure the YUI guys have their reasons for re-implementing this stuff (Prototype isn't universally well-loved), but it's kind of unfortunate for Rails developers, who are pretty well married to Prototype at this point. That isn't to say that the Yahoo stuff is better or worse.
In the meantime, if we want to use YUI's widgets, we just have to suck it up and eat a little extra pie this holiday season. Maybe it's worth writing an alternative connection.js to act as a Prototype wrapper. Maybe we could whip up some helper modules to get these excellent UI widgets as well integrated into the Rails framework as the Scriptaculous stuff is. I'm sure this is all very possible, but just need to spend some time digging through the stuff in more detail. In any case, it seems clear that proper RoR integration needs a bit more thought. Add it to the TODO list!
PS the folks at OpenRico have done a nice job providing a set of controls that are very RoR-friendly. Their focus is somewhat different than that of the Yahoo library, more emphasis on behaviors and cinematic effects rather than on widgets, per se. They have a great accordion widget though, and a live grid too (although it currently lacks cell editing support, which is one of the key things we're looking for).
YUI Dialog On Rails: First Pass
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.