Subscribe to
Posts
Comments

So we’ve put in the effort to make our JavaScript code nicely unobtrusive and all is well — until it isn’t. A pesky Ajax request has messed up the DOM and suddenly there are elements in there where our unobtrusive behavioral goodness has not been attached. Never give up, never surrender. You won’t even have to get out Grabthar’s hammer, it’s only just a small matter of programming.

First, let’s shrink the problem a little. Let’s concentrate on changes effected through the abstraction layer provided by the Prototype library. Prototype implements several DOM-changing functions on the Element singleton and also mixes them into extended DOM elements. These functions are remove, update, replace, insert, wrap, and empty. Prototype also gives us the wrap function that wraps a function around another, somewhat akin to AOP’s around advice, but without pointcuts.

Now the strategy is clear: Replace the original DOM-changing functions with versions that record a bit of information about these changes. A good mechanism to serve the actual notification is to use Custom Events. Then, anyone intersted in changes to a branch of the DOM can register as an observer for our new dom:changed event.

However, it is a good idea not to fire the event straightaway after the DOM has been changed. We might be in the middle of a batch of changes and the DOM in a correspondingly unorderly state. Besides, it is sometimes necessary to give the browser’s rendering engine a bit of breathing room after changes. For all this, at first the event data is appended to a list and a timer is set to dispatch the corresponding events some time later. This timer is reset for every change, so that for a batch of changes it only executes once and and then fires all events successively.

To understand the resulting behavior, it is important to know that JavaScript is single-threaded. As a result, when a timer times out, it does not interrupt the currently executing code. Rather, the function attached to the timer is scheduled to be executed as soon as there is nothing else to do. Therefore, the usual sequence is like this

  • An event triggers a DOM-changing function
    • Change the DOM
    • Remember the change
    • Change the DOM
    • Remember the change
  • Fire events for all remembered changes

Finally, here’s the code.

(function() {
  var methods = ['remove', 'update', 'replace', 'insert', 'wrap', 'empty'];
  var changes = [];
  var timeout;

  function rememberChange(element, method) {
    changes.push({
      element: $(element),
      parent: element.parentNode,
      operation: method
    });
  }

  function scheduleEvent() {
    if (timeout) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(fireEvent, 10);
  }

  function fireEvent() {
    changes.each(function(change) {
      var affectedNode = change.element;
      var operation = change.operation;
      if (!change.parent ||
          (change.parent && !affectedNode.descendantOf(change.parent))) {
        affectedNode = change.parent || affectedNode.parentNode;
        operation += 'Child';
      }
      affectedNode.fire('dom:changed', {operation: operation});
    });
    changes = [];
  }

  methods.each(function(m) {
    Element.Methods[m] = Element.Methods[m].wrap(
      function(proceed, element) {
        rememberChange(element, m);
        scheduleEvent();
        return proceed.apply(null, $A(arguments).slice(1));
      });
  });
})();

When an element has been removed from the DOM, it is pointless to fire an event on it that bubbles upwards. There’s no surface to bubble up to anymore. Therefore, in this case, the dom:changed event is fired on the parent of the removed element and the operation is signified as removeChild.

To peek at the modification events, or do something useful with them, add code like this

document.observe('dom:loaded', function() {
  document.body.observe('dom:changed', function(event) {
    console.log('dom:changed: ', event.element(), event.memo.operation);
  });
});

JavaScript FSM v0.2.2

I’ve released a new version, 0.2.2, of the JavaScript FSM builder. There are some enhancements, such as accessors for successorStates and expectedEvents that I found useful for implementing a first (almost) practical example involving drag & drop.

Speaking of which, this whole thing didn’t come into being because of a pressing need. Currently, it’s more of a solution looking for a problem. I know that state machines can be very helpful for UI construction, however, this particular incarnation still has to prove itself.

All the rest

I like finite state machines and I wanted to try metaprogramming in JavaScript ever since I’d seen Adam McCrea presentation on the topic.

The result is an FSM builder in JavaScript. Machine descriptions look like my test machine here:

var Machine = FSM.build(function(fsm) { with (fsm) {
  onUnexpectedEvent(function() { ... });

  state('start', 'initial')
    .event('go')
      .goesTo('middle')
      .doing(function() { ... })
      .doing('phew')
    .event('run')
      .goesTo('finish')
    .onExiting(function() { ... });

  state('middle')
    .onUnexpectedEvent(function() { ... })
    .onEntering(function() { ... })
    .event('back')
      .goesTo('start')
      .onlyIf(function() { return true_or_false })
    .event('go')
      .goesTo('finish');

  state('finish', 'final');
}});

function TestMachine() {}
// amend TestMachine.prototype here all the way you want
TestMachine.prototype.phew = function() { ... };
TestMachine.prototype = new Machine(TestMachine.prototype);

There’s no documentation yet, but thorough unit testsspecs
using Nicolás Sanguinetti’s very nice js-spec framework.

Something to look at:

The script is meant to be usable independently of and play nicely with any other libraries.

The manner in which an FSM is attached to classes/objects is still rather convoluted. The general idea is that it (a) should be possible to insert the FSM as the first link in the prototype chain and (b) should be possible to directly attach it to an existing object. I’d appreciate suggestions how to make this nice and shiny.

Gem: Updated Bash completion

Victor Serbin sent me a version of the completion updated for Gem 1.0.1. You can get it here.

Apparently, Ubuntu and Debian have begun to bundle this file with their rubygems packages. Great!

The Rails generator script/generate knows pretty well what things it can generate. In fact, it knows much better than I do. So, I think it could really give me some help when I’m typing along on the command line.

If you save the snippet below as /etc/bash_completion.d/generate you can enjoy this help, too.

_generate()
{
  local cur

  COMPREPLY=()
  cur=${COMP_WORDS[COMP_CWORD]}

  if [ ! -d "$PWD/script" ]; then
    return 0
  fi

  if [ $COMP_CWORD == 1 ] && [[ "$cur" == -* ]]; then
    COMPREPLY=( $( compgen -W '-h -v\\
      --help --version'\\
      -- $cur ))
    return 0
  fi

  if [ $COMP_CWORD == 2 ] && [[ "$cur" == -* ]]; then
    COMPREPLY=( $( compgen -W '-p -f -s -q -t -c\\
      --pretend --force --skip --quiet --backtrace --svn'\\
      -- $cur ))
    return 0
  fi

  COMPREPLY=( $(script/generate --help | \\
    awk -F ': ' '/^  (Plugins|Rubygems|Builtin|User):/ { gsub(/, */, "\n", $2); print $2 }' | \\
    command grep "^$cur" \\
  ))
}

complete -F _generate $default generate

I’m not yet so enlightened that all of my Rails unit and functional tests run without accessing the database. Indeed, I’m still using YAML fixtures to populate the database for testing.

I also insist on having foreign key constraints in the database, a thing that’s not exactly encouraged by Rails, but which is quite possible nonetheless. The various plugins from RedHill Consulting are a big help.

But then, when you feel all warm and cosy due to the additional safety at the database-level, you’re suddenly trapped by a snag: Sooner or later you find that your fixtures contain dependencies among objects that preclude any attempt at clever ordering by violating one foreign key constraint or another. Fixture files are loaded one after another in their entirety and when an object in an earlier fixture refers to an object in a later fixture, the database aptly notices as an inconsistency.

Well, you may think, it is an inconsistency, but only a temporal one. After all the fixture files are loaded, everything is consistent again. That’s the clue. We need to tell the database that, yes, indeed, things may be inconsistent for a time, but we’ll be cleaning up, promise. The good thing is that there is even an SQL standard-compliant way to express this promise.

  START TRANSACTION
  SET CONSTRAINTS ALL DEFERRED
  COMMIT

If you use transactional fixtures, the transaction bracket is already provided by Rails, but there’s no pretty way to sneak in the "SET CONSTRAINTS ..." line. There are two ways of slightly different brutality. First, you can edit activerecord/lib/fixtures.rb and just insert the required line.

  def self.create_fixtures(fixtures_directory, table_names, class_names = {})
    ...
    connection.transaction(Thread.current['open_transactions'].to_i == 0) do
      # insert the following line
      connection.execute("SET CONSTRAINTS ALL DEFERRED")
      ...
    end
    ...
  end

Alternatively, you can overwrite the entire method in, say, <railsapp>/lib/transactional_fixture_loading_hack.rb like this

require 'active_record/fixtures'

Fixtures.class_eval do
  def self.create_fixtures(fixtures_directory, table_names, class_names = {})
    ...
    connection.transaction(Thread.current['open_transactions'].to_i == 0) do
      # inserted line
      connection.execute("SET CONSTRAINTS ALL DEFERRED")
      ...
    end
    ...
  end

Whatever you do, you’ll have to inspect your code whenever you update your Rails version.

We’re still not done, unfortunately. The database defers only those constraints that are deferrable. Have a look at the Foreign Key Migrations Plugin for how to achieve this.

As a matter of convenience, I suggest that in your test_helper.rb you add a method that loads all your fixtures

class Test::Unit::TestCase
  self.use_transactional_fixtures = true

  def self.load_all_fixtures
    fixtures :users, :thingamajigs, :gadgets, :widgets
  end
end

Then, in a testcase class you can use it like this

require File.dirname(__FILE__) + '/../test_helper'

class ThingamajigTest < Test::Unit::TestCase
  load_all_fixtures

  ...
end

Note that with transactional fixtures this results in each fixture file loaded only once for all the tests.

So, there we are a last. Or those with a reasonable DBMS, I might say. For, of course, this technique is no use, if your database does not support deferrable constraints. PostgreSQL for one does support them.

I’ve recently started to add XML support to a Rails application, meaning that the application provides data in XML format, if the request asks for it, and it understands XML data on create or update.

To keep the application as well as myself sane, I’ve written a test that ensures the round trip of getting XML and updating an object by sending XML works. This is only a very basic test and there surely is more that can and should be tested.

class XmlRoundtripTest < ActionController::IntegrationTest

  RESOURCES = [
    :people, :things
  ]
  fixtures :stuff, *RESOURCES

  def self.assert_roundtrippability_for(*resources)
    resources.each do |resource|
      define_method("test_xml_roundtrip_for_#{resource}") do
        @user = user
        @user.logs_in

        xml = @user.gets_xml(resource)
        @user.changes_object_name!(xml)

        old_version = @user.extracts_lock_version(xml)
        @user.sends_xml(xml, resource)

        # Make sure that the resource was really updated
        new_version = @user.extracts_lock_version(@user.gets_xml(resource))
        assert_equal(old_version + 1, new_version)
      end
    end
  end

  assert_roundtrippability_for *RESOURCES

  def user
    open_session do |user|
      def user.gets_xml(resources, id = 1)
        get "/#{resources}/#{id}.xml"
        assert_response :success
        @response.body
      end

      def user.sends_xml(xml, resources, id = 1)
        put "/#{resources}/#{id}.xml", xml, :content_type => 'application/xml'
        assert_response :success
        @response.body
      end

      def user.changes_object_name!(xml)
        # arbitrarily change an attribute we know is there
        xml.gsub!(%r{<name>(.*?)</name>}, '<name>X\1Y</name>')
      end

      def user.extracts_lock_version(xml)
        xml =~ %r{<lock-version>(\d+)</lock-version>}
        $1.to_i
      end
    end
  end
end

The gem command is the command line user interface to the RubyGems system for managing Ruby packages.

This command has a few sub-commands itself and a long list of options that differ per sub-command. Remembering and typing them can be tedious, but thankfully, if you are using the bash command shell, it can help with this task. It only has to be told how.

I’ve packaged the how in this file. Copy it to /etc/bash_completion.d and hope that your bash installation picks it up the next time you start a shell.

On Debian/Linux system that’s just how it works. If this does not work on your particular version of Linux or Unix, you’ll have to look up how completions are handled on your system.

Make your layout look good a different screen and font sizes.

What it looks like

Example 1,
Example 2.

Here’s what the form example (example 1) is supposed to look like.

At 1024 x 768 pixels, the navigation links and Save button are positioned fixed in a column to the left.

large layout

At 800 x 600 pixels, the navigation links are only shown on hover over the icon in the top-left corner; the Save button is tucked under the form itself.

large layout
large layout

What you need

Grab layout.js.

You also need Prototype.js.

What you need to do

Include the scripts in your page.

<script src="javascripts/prototype.js" type="text/javascript"></script>
<script src="javascripts/layout.js" type="text/javascript"></script>

Activate layout switching in the head of your page

<script type="text/javascript">
//<![CDATA[
Event.observe(window, 'load', function() {
  Layout.initialize({observeFontSize: true});
});
//]]>
</script>

Most likely, the default size thresholds set in layout.js won’t fit your needs. See the file itself for how to set them to different values.

(Re-)Write your stylesheets so that they make use of the size information. As an example, here’s the CSS I use for styling the navigation list on the two example pages.

#navitrigger {
  display: none;
}

#navigation {
  position: fixed;
  top: 0em;
  left: 0em;
  margin: 1em;
  padding: 0;
  text-align: right;
  z-index: 10;
}

#navigation ul {
  width: 10em;
  margin: 0;
  padding: 0;
  list-style-type: none;
}

#navigation li {
  font-weight: bold;
}

#navigation .current {
  display: block;
  width: 100%;
  color: #bbb;
  background-color: #ffecac;
}

#navigation li.admin {
  border-top: 1px solid #bbb;
}

#navigation li a {
  display: block;
  width: 100%;
}

body.small #navigation {
  position: absolute;
  text-align: left;
  z-index: 30;
}

body.small #navigation ul {
  display: none;
  background: #fff;
  border: 1px solid #888;
}

body.small #navitrigger {
  display: block;
  font-size: 150%;
  font-weight: bold;
}

body.small #navigation:hover ul {
  display: block;
}

On my continuing quest to become more Web-2.0-ish, I’ve tried my luck tilting at… well, tag clouds. To get an idea what I’m talking about, please have a look.

These are the rudimentary basics to create a tag cloud like that.

In the controller

def tags
  @popular_tags = Tag.find_most_popular(:limit => 30)
end

In the view

<ul id="tags" class="tagcloud">
<% @popular_tags.sort_by(&:name).each do |t| -%>
  <%= weighted_tag t, tag_options %>
<% end -%>
</ul>

or, to get a somewhat nicer look, and to demonstrate some of the options

<%
  tag_options = {
    :wrap => ['li', '<span class="dlm">[</span>', '<span class="dlm">]</span>'],
    :min_size => 100, :max_size => 500,
    :link_options => { :class => 'tag' })
  }
-%>
<h2>Tags</h2>
<ul id="tags" class="tagcloud">
<% @popular_tags.sort_by(&:name).each do |t| -%>
  <%= weighted_tag t, tag_options %>
<% end -%>
</ul>

Here you can download a do-it-yourself kit of all the pieces.

« Prev - Next »

Fork me on GitHub