Using jQuery and Ajax to load sub-sections of a webpage

OK, so this isn’t really rocket science, but there are a couple of neat uses for this technique:

Ajax powered tab sets

Tabs can; broadly-speaking, be loaded in three different ways:

  • Immediately loaded – i.e. when the webpage has loaded, it is the default selected tab.
  • Later (deferred loading) – i.e. after the user clicks a tab, go off to the server and retrieve the contents of a tab, or
  • Lazy loading – e.g. load in the background before the user clicks a tab. ┬áThis enhances the user experience as the contents of a tab are immediately shown, but on the flip side means requires more traffic and the user may never click the tab anyway, effectively wasting bandwidth.

Content panes, portal layouts etc

As with tabs, content panes on a page can be loaded in several ways, although lazy loading is probably not very useful.

Solution

Here’s a javascript (using jQuery) implementation of the above which supports loading now, loading on page load and lazy loading (which takes place after the on page loads):

ajaxLoader : {
    /** A queue of embeds to load on document ready */
    loadQueue: {},
    
    /** A queue of embeds to load on after the load queue */
    lazyQueue: {},
    
    /**
     * Called on Document Ready to load all the queued embeds
     */
    init: function() {
        jQuery.each(this.loadQueue, function(id, url) {
            wbHtmlWidgets.embed._load(id, url);
        });
        
        jQuery.each(this.lazyQueue, function(id, url) {
            wbHtmlWidgets.embed._load(id, url);
        });
    },
    
    _load: function(id, url) {
        $("#" + id).load(url);
    },
    
    /**
     * Loads a url into a dom id immediately - ensure that the page has
     * completely loaded before using this method, as manipulating the DOM
     * before it has been created does nasty things in browsers such as IE.
     * If you want to embed content after the page has loaded, use embed.afterLoad()
     * @param {string} id The div id to load the embed into
     * @param {string} url The url to load into the specified div id
     */
    now: function(id, url) {
        this._load(id, url);
    },
    
    /**
     * Loads a url into a dom id after the onLoad queue has been processed.
     * @param {string} id The div id to load the embed into
     * @param {string} url The url to load into the specified div id
     */
    lazy: function(id, url) {
        this.lazyQueue[id] = url;
    },
    
    /**
     * Loads a url into a dom id after the page has loaded.
     * If you want to embed content now, use embed.now()
     * @param {string} id The div id to load the embed into
     * @param {string} url The url to load into the specified div id
     */
    afterLoad: function(id, url) {
        if ( $("#"+id).html() == '' ) {
            $("#"+id).html('<image src="/images/spinner.gif" />');
        }
        this.loadQueue[id] = url;
    }
}

To get this to work, ensure that you have an onLoad (document ready) event on your page…

$(document).ready(function(){
    ajaxLoader.init();
});

Then to make the magic work, in your HTML you may want to do something like:

<span id="tab-0"></span>              
<script type="text/javascript">
//<!&#91;CDATA&#91;
    ajaxLoader.afterLoad("tab-0", "http://your/tab/address");
//&#93;&#93;>
</script>
Advertisements

Dumping and loading seed data for Rails database resets

As mentioned in our previous posts regarding database migrations in Rails, we have introduced a method of managing seed data in migrations. Although this methodology still has some problems, generally speaking it works pretty well.

After a database migration, a schema dump takes place. This allows fast recreation of databases without having to go through every single migration. The problem with this is that any seed data added during the migrations is not added back in when doing a db:reset. This can be solved by having a similar function to Schema Dumper.

Here are the seed data tasks (you probably want to add these into the db namespace):

namespace :seed_data do
  desc 'Load schema seed data'
  task :load => :environment do
    puts "== Loading dumped seed data"
    config = ActiveRecord::Base.configurations[RAILS_ENV || 'development']
    ActiveRecord::Base.establish_connection(config)
    require 'active_record/fixtures'
    Dir.glob(RAILS_ROOT + '/db/seed_data/global/*.yml').each do |file|
      Fixtures.create_fixtures(RAILS_ROOT + '/db/seed_data/global', File.basename(file, '.*'))
    end
  end
  
  desc 'Save schema seed datae'
  task :dump => :environment do
    config = ActiveRecord::Base.configurations[RAILS_ENV || 'development']
    ActiveRecord::Base.establish_connection(config)
    Database::SeedDataDumper.dump(ActiveRecord::Base.connection)
  end
end

You’ll need the SeedDataDumper class:

module Database
  class SeedDataDumper

    def self.dump(connection)
      new(connection).dump()
      true
    end

    def dump()
      @connection.tables.each do |table|
        if @connection.select_all("DESCRIBE #{table}").inject(false) { |b, col| b || (col["Field"]=="came_from_migration") }
          data = @connection.select_all("SELECT * FROM #{table} WHERE came_from_migration IS NOT NULL")
          if !data.empty?
            outfile = File.new("#{@folder}/#{table}.yml",  "w")
            header(outfile)
            i = "0"
            outfile.write data.inject({}) { |hash, record|
              hash["#{table}_#{i.succ!}"] = record
              hash
            }.to_yaml
            footer(outfile)
          end
        end
      end
    end

    private
      def initialize(connection)
        @connection = connection
        @folder = "#{RAILS_ROOT}/db/seed_data"
        puts "== Dumping seed data"
      end

      def header(stream)
        stream.puts <<HEADER
# This file is auto-generated from all the current "came_from_migration" rows in the selected
# database.  It generates a yaml file per table which can be loaded in again using fixtures.
HEADER
      end

      def footer(stream)
        stream
      end

  end
end
&#91;/sourcecode&#93;

Remember to put the following line at the end of your <code>db:migrate</code> task:


Rake::Task["db:seed_data:dump"].invoke if ActiveRecord::Base.schema_format == :ruby

Seeding data in Rails Migrations (Part 2)

We’ve had a significant number of hits on our first article on Seeding data as part of Rail database migrations which just goes to show that people are finding this a bit of a problem! As we have been using our technique for a few weeks now, we’d like to point out a few of the problems with it that we have found (some with solutions, and some that we just haven’t figured out yet!).

  1. Fixtures#create_fixtures deletes data – Whoops! After looking at the source code, create_fixtures effectively drops all the data in the table it is just about to populate. Obviously this isn’t very useful if you want to use a migration on any table that isn’t empty.
  2. Related data – Migrations in Rails 2 have a nifty feature allowing you to create inter-related table rows by using named data items. The methodology outlined in Part 1 doesn’t load all the yaml files into the migration and then run them, instead it runs them one at a time meaning (and with no model class) so Rails is unable to resolve the relationships between rows.
Solutions
The easiest way to fix the related data is to extend the reader for yaml files to have more than one table per file, consider a yaml seed data migration file “006_add_monkeys_and_fruit.yml”:
monkeys:
  george:
    name: George the Monkey
    pirate: reginald
    fruits: apple, orange, grape

fruits:
  apple:
    name: apple

  orange:
    name: orange

  grape:
    name: grape

This naming convention follows the ideas behind migration naming convention and doesn’t stop you having more than one seed file per migration (obviously of unrelated data) e.g. 006_add_tables_and_chairs.yml

Temporary work around

For the time being (until we extend the yaml parser) we have written a slightly different seed data function for our migration files This allows us to seed a row into the database, read its insertion ID and then reference that row object in subsequent inserts.

Conclusion

We still think that Fixtures provides a pretty good way of introducing structure data into our tables, but just needs a little bit more work before it plays nicely with migrations. We have also found the time to start writing the schema dump/load equivalents for seed data (when creating database from schemas, i.e. in production). These methods are just being finished and tested and we will post them up here in the next couple of days.