jQuery TreeView, Flexigrid を Rails で使ってみた

訳あって、jQuery プラグインの TreeView, Flexigrid を Ruby on Rails + jRails で使ってみました。以下の画像のように左のツーリー(TreeView)で選択したディレクトリー内のファイル一覧が 右側の テーブル(Flexgrid) に表示されます。


使っているソフトなど

今回作ったコード

実際にディレクトリーを参照しているわけではなく、listsテーブルに ディレクトリー階層 dir1,dir2,dir3 と ファイル情報 file,size,mtime を持っていて、その内容を表示します。

View

layoutでは、jQueryプラグインJavascript,CSSを追加しています。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title>Lists: <%= controller.action_name %></title>
  <%= stylesheet_link_tag 'scaffold' %>
  <%= stylesheet_link_tag 'flexgrid/flexigrid/flexigrid' %>
  <%= stylesheet_link_tag 'treeView/jquery.treeview.css' %>
  <%= javascript_include_tag 'jquery-1.2.6' %>
  <%= javascript_include_tag 'flexigrid' %>
  <%= javascript_include_tag 'jquery.treeview' %>
</head>
<body>

<p style="color: green"><%= flash[:notice] %></p>

<%= yield  %>

</body>
</html>

テンプレート index.html.erb に TreeView, Frexgrid の設定が書かれています。

  • Frexgridへのデータ表示は flexReload()を呼び出す事でブラウザー側からリクエストするようになっています。
  • Frexgridはまだドキュメントが整備されていませんが、ソースを見るとアトリビュートは直ぐに判ります。
  • FrexgridにURL指定でreloadするメソッド flexLoadUrl を定義しています (これで良いのかな?)
<h2>ファイル一覧</h2>

<script type="text/javascript" charset="utf-8">
  jQuery.fn.extend({
    flexLoadUrl: function(load_url) {
      return this.flexOptions({url: load_url, newp: 1}).flexReload();
    }
  });

  $(document).ready(function() {
    $("#dir_tree").treeview({
      animated: "fast",
      collapsed: true,
      unique: true,
    });

    $("#file_list").flexigrid( {
      url: '',
      dataType: 'json',
      method: 'GET',
      colModel : [
         {display: 'ファイル名', name : 'file', width : 220, sortable : true, align: 'left'},
         {display: 'サイズ', name : 'size', width : 70, sortable : true, align: 'right'},
         {display: '更新日時', name : 'mtime', width: 180, sortable : true, align: 'center'},
         ],
      sortname: "file",
      sortorder: "asc",
      usepager: true,
      useRp: true,
      rp: 10,
      width: "auto",
      height: "auto",
      showToggleBtn: false,
      pagestat: '{from} 〜 {to} 件を表示 (全件:{total})',
      procmsg: 'データ取得中 ...',
    });

    $('#file_area').hide();
  });
</script>

<div id="dir_area" style="margin-left: 10px; float:left; width: 240px;">
  <h4 style="margin: 0 0 10px 0;">ディレクトリー選択</h4>
  <%= tree_view @tree, :id=>"dir_tree", :class=>"filetree", :span_class=>"folder", :list_id=>"file_list" %>
</div>

<div id="file_area" style="margin-left: 240px; padding-right:60px;">
  <h4 style="margin: 0 0 10px 0;"></h4>
  <table id="file_list"></table>
</div>
helper

TreeViewは <ul> <li> ... </ul> で作られた箇条書をツリーとして表示していますので ul 箇条書 を生成するコードをヘルパーに作りました。Railsの link_to_function APIでツリーをクリックした際に、 FlexGridでデータをロードするようにしています。通常のlink_to_function APIPrototype.js用のコードを生成しますが、jRailsプラグインを使うと jQuery用のコードが生成されます。

module ListsHelper
  def tree_view(tree, opt = {})
    ul_tag_tree(tree, opt, 1)
  end

  private

  def ul_tag_tree(tree, opt, level)
    html =  "  " * level + (level == 1 ? "<ul id='#{opt[:id]}' class='#{opt[:class]}'>\n" : "<ul>\n")
    while tree.first
      t = tree.first
      if  t.level == level
        html << "  " * level + "  <li>" + 
          link_to_function("<span class='#{opt[:span_class]}'>#{t.name}</span>",
                           "$('##{opt[:list_id]}').flexLoadUrl('#{formatted_list_path(t.path, :json)}');
                            $('#file_area').show();
                            $('#file_area > h4').html('/#{t.path} ファイル一覧')")
        html << (tree[1] && tree[1].level == level ? "</li>\n" : "\n")
        tree.shift
      elsif t.level > level
        html << ul_tag_tree(tree, opt, level + 1)
      else
        break
      end
    end
    html + "  " * level + "</li></ul>\n"
  end
end
Controller
  • index : まずは ディレクトリーのツリーを表示します
  • show : Flexgrid からのリクエストで1ページ分のファイル一覧情報を JSON で戻します。
    • Flexgridからは rp: 1ページ当たりの表示行数、 page:ページ番号、 sortname:ソートカラム、sortorder: ソート方向(ASC/DESC) がパラメターとして送られてきます。
    • また、id には dir1/dir2/dir3 のような ディレクトリーツリーの情報が送られてきます。
    • Flexgridに戻すJSON には page: ページ番号、total: 表示行数、rows: データの配列が入ります
{"page": 1,
 "rows": [{"cell": ["excel_text.jar", 14674, "2008-04-28 22:22:11"]},
          {"cell": ["poi-3.0.2-FINAL-20080204.jar", 964285, "2008-04-08 16:19:10"]},
          {"cell": ["template.xls", 35328, "2008-04-08 16:19:10"]}],
 "total": 3}
class ListsController < ApplicationController
  def index
    @tree = List.find_tree_info()
  end

  def show
    lines = params[:rp].to_i
    page = params[:page].to_i
    @lists = List.find_by_path(params[:id], 
                               :order => params[:sortname] + ' '+ params[:sortorder],
                               :limit => lines,
                               :offset => (page - 1) * lines)
    count = List.count_by_path(params[:id])
    data = {:page => page,
      :total => count,
      :rows => @lists.map {|u| {:cell =>
          [u.file, u.size, u.mtime.strftime('%Y-%m-%d %H:%M:%S')]}}}
    render :json => data.to_json
  end
end
Model
  • find_tree_info : 下のようなディレクトリーツリーの情報を戻します。
[#<struct List::TreeInfo level=1, name="acl81", path="acl81">,
 #<struct List::TreeInfo level=2, name="acache", path="acl81/acache">,
 #<struct List::TreeInfo level=3, name="doc", path="acl81/acache/doc">, ....
  • find_by_path : path で指定されたディレクトリー下のファイルの一覧を戻します。
  • count_by_path : path で指定されたディレクトリー下のファイル数を戻します。
class List < ActiveRecord::Base
  TreeInfo = Struct.new(:level, :name, :path)

  def self.find_tree_info
    find_dir_tree([])
  end

  def self.find_by_path(path, opt = {})
    List.find(:all, opt.merge({:conditions => build_path_conditions(path)}))
  end

  def self.count_by_path(path, opt = {})
    List.count(:all, opt.merge({:conditions => build_path_conditions(path)}))
  end


  private

  def self.build_path_conditions(path)
    cond = {}
    path.split(/\//).each_with_index {|e,ix| cond["dir#{ix + 1}"] = e}
    cond
  end

  def self.find_dir_tree(tree)
    cond = {}
    tree.each_with_index {|e,ix| cond["dir#{ix + 1}"] = e}
    level = tree.size + 1
    dir_col = "dir#{level}"
    entries = List.find(:all, :select => "distinct #{dir_col}",
                        :conditions => cond).map {|e| e[dir_col]}
    list = []
    entries.select {|e| e != ""}.each {|e|
      down_tree = tree + [e]
      list.push(TreeInfo.new(level, e, down_tree.join('/')))
      list.concat(find_dir_tree(down_tree))  if (level <= 2)
    }
    list
  end
end

感想

  1. jQueryを使うのは初めてでしたがとても簡潔に書けていいですね。どこかに書いてありましたが、もう普通のJSを書く気がしなくなります ^^)
  2. jQuery にはたくさんのプラグインがあり、どれを選ぶかは時間がかかります。
    • しかし、プラグインのページにはデモ等があるので動作や機能が判ります。
    • また、ソースをザッと眺めてみるのも必要かも知れません、大規模ではない物の方が良いかもしれません。
  3. jQuery UI というものがありますが、プラグインとの関係が良く判りませんでした。 まだ、Table/GridやTree はありませんし・・・だれか教えて!!
  4. jRailsプラグインを入れると、 Rails の link_to_remote, link_to_function ... API が使えるようになるので便利です。しかし同梱されている jQuery.js等は 最新ではないので置き換えました。