Module: TentSteakFeatures::TableView

Defined in:
lib/tent_steak/table.rb

Overview

TentSteak view methods for :table feature. Provides helpers to populate HTML tables from Ruby objects.

Instance Method Summary collapse

Instance Method Details

#auto_table(rows, options = {}) ⇒ Object

Autogenerate an HTML table from an array of ActiveRecord objects. By default, uses database column names for headers, and displays all columns in database order.

To generate a table with all defaults, simply pass in the array of ActiveRecord objects. The table will list all database columns in database schema order, i.e. whatever ActiveRecord::Base.column_names returns.

auto_table @rows

Options

  • :methods: an optional array of row object method names to expand a row object into an array of cell values; useful to change column order or to restrict which columns are shown. Defaults to the value of column_names() with ActiveRecord objects.

  • :humanize: set to true for more human friendly column headers; ignored for columns that are explicitly set with :column_titles. Defaults to false.

  • :column_titles: an optional array of column header names; defaults to the database column names. Pass in nil for any individual column you want to fall back on the defaults.

  • :group_col: a column index to highlight adjacent rows with identical cell values. Watches the data content in the given column; when the cell content changes, it toggles between highlighted (CSS styled) and plain rows. Best if used with a sorted column.

  • :group_col_class: CSS style for :group_col rows; defaults to <tr class=“altrow”> for highlighted rows.

Column Titles

You can restrict or reorder the columns shown with the :methods option. This example creates a table with four columns, titled “field_one”, “field_six”, “field_two”, and “field_three”:

auto_table @rows, :methods => %w{field_one field_six field_two field_three}

The :humanize option translates the default database column names into a more human friendly format. The sample above would generate the columns “Field One”, “Field Six”, “Field Two”, and “Field Three”:

auto_table @rows, :methods => %w{field_one field_six field_two field_three},
  :humanize => true

Furthermore, you can override the defaults by supplying an array of custom titles in the :column_titles option. This example would create the column titles “First Field”, “Status Thingie”, “Field Two”, and “Field Three”:

auto_table @rows, :methods => %w{field_one field_six field_two field_three},
  :humanize => true, :column_titles => ["First Field", "Status Thingie"]

Pad with nil to override later fields. This example skips field_two to yield “First Field”, “Status Thingie”, “Field Two”, and “Last Field”:

auto_table @rows, :methods => %w{field_one field_six field_two field_three},
  :humanize => true, :column_titles => ["First Field", "Status Thingie", nil, "Last Field"]


551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
# File 'lib/tent_steak/table.rb', line 551

def auto_table(rows, options = {})
  return if rows.nil? || (rows.instance_of?(Array) && rows.empty?)

  # Initialize values for group_col tracking.
  last_group_value = nil
  alt_group = true

  # Pull column names from first ActiveRecord class in rows if not specified.
  # If rows are not AR objects, treat them as arrays.  Push the column names into
  # options if not already set, so tablerow will see our chosen column_names.
  column_names = options[:methods] ||
    (rows.first.class.respond_to?(:column_names) && rows.first.class.column_names) || []
  options[:methods] ||= column_names

  # Use given header text, or default to database column names; pretty up database
  # column names if :humanize is set.  If any titles in :column_titles are nil,
  # fall back on the database column name for that column.
  column_titles = []
  column_names.each_with_index do |dbcol, i|
    column_titles << ((options[:column_titles] && options[:column_titles][i]) ||
      (options[:humanize] ? dbcol.titleize : dbcol))
  end

  # If column_names is empty (non-AR objects) and :column_titles is set, use that.
  column_titles = options[:column_titles] if column_names.empty? &&
    options[:column_titles]

  # Insert edit checkbox header if any.
  column_titles.unshift(options[:select_row]) if options[:select_row]

  alt_style = options[:group_col_class] || "altrow"

  table do
    tableheader column_titles unless column_titles.empty?
    for row in rows
      # Optional row group highlighting.
      rowopts = options.dup
      if options[:group_col]
        this_group_value = row.instance_of?(Array) ? row[options[:group_col]] :
          row.send(column_names[options[:group_col]].to_sym)

        # If this/last group value changes between rows, then toggle alt_group
        alt_group = !alt_group if this_group_value != last_group_value

        rowopts[:rowclass] = alt_style if alt_group
        last_group_value = this_group_value
      end

      tablerow row, rowopts
    end
  end
end

#blankrow(colspan = 1) ⇒ Object

Insert a blank table row with an optional colspan. The colspan should probably match the total number of columns in the table.



46
47
48
# File 'lib/tent_steak/table.rb', line 46

def blankrow(colspan = 1)
  tr { td(:colspan => colspan) {cellpad " "} }
end

#celldate(date, format = '%Y/%m/%d') ⇒ Object

Format a date for a table cell.



40
41
42
# File 'lib/tent_steak/table.rb', line 40

def celldate(date, format = '%Y/%m/%d')
  date.respond_to?(:strftime) ? date.strftime(format) : cellpad(date)
end

#cellpad(value, pad = "&nbsp;") ⇒ Object

If value is nil or whitespace, substitute with “&nbsp;”. Otherwise pass value through untouched. Useful for padding table cells.



35
36
37
# File 'lib/tent_steak/table.rb', line 35

def cellpad(value, pad = "&nbsp;")
  (value.nil? || value.to_s.blank?) ? pad : value
end

#grouping_table(headers, groups, rowopts = {}, &block) ⇒ Object

Generate a table with blank rows between groups of related rows. headers is an array of header names to be passed to #tableheader. groups is an array of row groups to render as the table content. Each row group is a nested array of table rows; the grouping_table inserts a #blankrow between each row group.

A single row can be either an array (triple-nested at this point!) of cell values, or a discrete object to expand into table cells via the :methods row option. See #tablerow for details.

The example below shows the first technique, using an array per row:

row1 = %w{val1 val2 val3}
row2 = %w{val4 val5 val6}
row3 = %w{val7 val8 val9}
group1 = [row1, row2]
group2 = [row3]
grouping_table %w{one two three}, [group1, group2]

It will create a table that looks like this:

+======+======+=======+
| one  | two  | three |
+======+======+=======+
| val1 | val2 | val3  |
+------+------+-------+
| val4 | val5 | val6  |
+------+------+-------+
|                     |
+------+------+-------+
| val7 | val8 | val9  |
+------+------+-------+

This example shows the second form, with rows as discrete objects and a list of methods to act on. Each row is a single String object which we’ll call to_s, length, and upcase on to generate each table row.

row1 = "Nobody"
row2 = "knows"
row3 = "my"
row4 = "beeses"
group1 = [row1, row2]
group2 = [row3, row4]
grouping_table %w{value length upcase}, [group1, group2],
  { :methods => [:to_s, :length, :upcase] }

The table will look like this:

+========+========+========+
| value  | length | upcase |
+========+========+========+
| Nobody |   6    | NOBODY |
+--------+--------+--------+
| knows  |   5    | KNOWS  |
+--------+--------+--------+
|                          |
+--------+--------+--------+
|  my    |   2    |  MY    |
+--------+--------+--------+
| beeses |   6    | BEESES |
+--------+--------+--------+

If you don’t want to call #blankrow between each group, you can pass in a block; the block will be called between each group with the number of columns in the table. For example, to put “–” in all three cells between groups:

grouping_table %w{one two three}, [group1, group2] do |col_count|
  tr { col_count.times { td "--" }}
end

You could also use the block to insert a graphic separator or change the CSS style of the blank row.



675
676
677
678
679
680
681
682
683
684
685
686
687
# File 'lib/tent_steak/table.rb', line 675

def grouping_table(headers, groups, rowopts = {}, &block)
  # Default to number of :methods or 1 if no methods are set.
  default_count = (rowopts[:methods] || [""]).length

  cols = (headers && headers.any? && headers.length) || default_count
  table do
    tableheader headers unless headers.nil? or headers.empty?
    groups.each do |rows|
      rows.each { |row| tablerow row, rowopts }
      block_given? ? yield(cols) : blankrow(cols) unless rows == groups.last
    end
  end
end

#hrow(cells, options = {}) ⇒ Object

Add pairs of fields in a vertical-style table (field names along the side, not the top). Useful for field=value lists. Expects a cell array with an even number of elements. Pass :titleclass to the options parameter to assign a CSS style to all field names, i.e. every other column; otherwise :titleclass defaults to “tabletitle”.

For example, the table below

table do
  hrow ["header1", "value1"], :titleclass => "titlealt"
  hrow ["header2", "value2", "header3", "value3", "header4", "value4"]
end

would render to the following HTML:

<table>
  <tr>
    <td class="titlealt">header1</td>
    <td>value1</td>
  </tr>
  <tr>
    <td class="tabletitle">header2</td>
    <td>value2</td>
    <td class="tabletitle">header3</td>
    <td>value3</td>
    <td class="tabletitle">header4</td>
    <td>value4</td>
  </tr>
</table>


79
80
81
82
83
84
85
86
87
# File 'lib/tent_steak/table.rb', line 79

def hrow(cells, options = {})
  titlestyle = { :class => options[:titleclass] || "tabletitle" }
  tr do
    cells.in_groups_of(2).each do |header, value|
      td(titlestyle) { cellpad(header) }
      td { cellpad(value) }
    end
  end
end

#tableheader(cells, options = {}) ⇒ Object

Create a table header from an array of column titles, e.g. for a three-column table with headers “One”, “Two”, and “Three”:

table do
  tableheader %w{One Two Three}
end

This generates the HTML:

<table>
  <tr class="tabletitle">
    <td>One</td>
    <td>Two</td>
    <td>Three</td>
  </tr>
</table>

The options hash accepts parameters which affect how the HTML header row is generated. By default, the header receives a CSS class of “tabletitle”. Override that with the :titleclass option:

tableheader %w{One Two Three}, :titleclass => "myheader"

which yields:

<tr class="myheader">
  <td>One</td>
  <td>Two</td>
  <td>Three</td>
</tr>

The options hash also accepts a parameter of :colspan to specify header columns that should have a colspan attribute. For example, to set colspan of the (zero-based index) headers “Two” and “Three” to colspan 2, and the “Four” header to 7 (for a total of 12 columns in our example):

tableheader %w{One Two Three Four}, :colspan => { (1..2) => 2, 3 => 7 }

All column indexes in the range (1..2) are set to colspan 2, and the single column “Four” at index 3 is set to colspan 7. This produces:

<tr class="tabletitle">
  <td>One</td>
  <td colspan="2">Two</td>
  <td colspan="2">Three</td>
  <td colspan="7">Four</td>
</tr>


136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/tent_steak/table.rb', line 136

def tableheader(cells, options = {})
  titlestyle = { :class => options[:titleclass] || "tabletitle" }
  colspans = options[:colspan] || {}
  tr(titlestyle) do
    if colspans.instance_of? Hash
      cells.each_with_index do |hdr, i|
        span = _getspan(options[:colspan], i)
        span ? td(hdr, :colspan => span) : td(hdr)
      end
    else
      cells.each { |hdr| td hdr }
    end
  end
end

#tablerow(row, options = {}) ⇒ Object

Add a row of HTML table cells. row can be an array of table cell values; if a cell is itself a nested array, #tablerow will separate the values with <br/> inside the cell. row can also be a single Ruby object. options is an optional hash of display parameters:

Options

  • :rowclass is a CSS class to tag the row with. Strings are assigned directly. If :rowclass is an Array, then the row’s CSS class is dependent on the contents of a specific column in the table. See below for details.

  • :colspan is a nested hash of index to span value; each hash key is a zero-based column index and each hash value is the number of columns (or rows) to span.

  • :rowspan works like :colspan except with the colspan HTML attribute.

  • :altcells is an array of cell indexes to assign the :altcell_class CSS style to. Useful for highlighting specific table cells.

  • :altcell_class is the CSS class for the :altcells array; defaults to “altcell”.

  • :methods: is an array of method symbol names to call on each row object to expand it into an array of cell values. Ignored if row is already an array. Defaults to [:to_s], which will convert the row object to a single cell containing its string value.

  • :id_links: hash of { “column_name” => ControllerClass } for any columns you want to call id_link() on. The column name is the database column name, not the displayed text.

Table Form Options

  • :key_columns is an array of column indexes to uniquely identify each row.

  • :text_columns defines which table columns should be editable as a text field.

  • :select_columns defines which table columns should be editable as a selection menu.

  • :select_row adds a leading column with checkboxes to select rows, e.g. to save.

  • :form_prefix overrides the default field name prefix of “ts_selected”; useful if you have more than one table form on the same page.

Row As Array

The simple form of #tablerow accepts the row as an array of literal cell values.

table do
  tablerow %w{cell_one cell_two cell_three}
end

This produces:

<table>
  <tr>
    <td>cell_one</td>
    <td>cell_two</td>
    <td>cell_three</td>
  </tr>
</table>

The commands below will create a 4x2 table rows with two rows of four cells each; the third cell of the first row will be “c<br/>d”. The second row will be tagged with the CSS class “altrow” through the :rowclass option. The :rowspan and :colspan options each take a hash of zero-based column index to span value. Thus, in the example, the ‘e’ cell (column 3) is spanned vertically by two, to merge with the fourth cell in the second row; the ‘g’ cell (column 1) is spanned horizontally by two, to merge with the third cell in the second row.

table do
  tablerow ["a", "b", ["c", "d"], "e"], :rowspan => {3 => 2}
  tablerow %w{f g}, :rowclass => "altrow", :colspan => {1 => 2}
end

The resulting HTML would look like this:

<table>
  <tr>
    <td>a</td>
    <td>b</td>
    <td>c<br/>d</td>
    <td rowspan="2">e</td>
  </tr>
  <tr class="altrow">
    <td>f</td>
    <td colspan="2">g</td>
  </tr>
</table>

The table should look like this (with * implying alternate row style):

+---+---+---+---+
| a | b | c |   |
|   |   | d |   |
+---+---+---+ e |
|*f*|***g***|   |
+---+-------+---+

Row As Object

The row parameter can also be a single object, to be expanded into an array of cells according to the :methods option. This example shows the implicit :methods of [:to_s], and also an explicit list of methods to call on the row object of “Huzzahh!!”.

table do
  tablerow "Huzzahh!!"
  tablerow "Huzzahh!!", :methods => [:to_s, :length, :upcase]
end

This results in the following table:

+-----------+-----+-----------+
| Huzzahh!! |     |           |
+-----------+-----+-----------+
| Huzzahh!! |  9  | HUZZAHH!! |
+-----------+-----+-----------+

Row Classes

The :rowclass option determines the CSS class, if any, of the table row. To unconditionally set the row’s CSS class, pass in a string value:

table do
  tablerow %w{cell_1 cell_2}, :rowclass => "altrow"
end

The HTML would look like this:

<table>
  <tr>
    <td>cell_1</td>
    <td>cell_2</td>
  </tr>
</table>

To assign CSS classes according to the text contents of the table cells, pass in an array of array-tuples with the column index, matching regexp, and CSS class name to apply if that column is a match. The first successful match is applied.

For example, to assign the “digit” CSS class to all rows with a numerical first column (index 0, regexp /d/), and CSS class “letter” to alphanumeric first column (index 0, regexp /w/):

rowopt = { :rowclass => [ [0, /\d/, "digit"], [0, /\w/, "letter"] ] }
table do
  tablerow %w{a b c}, rowopt
  tablerow (3..5).to_a, rowopt
end

This would create the following HTML:

<table>
  <tr class="letter">
    <td>a</td>
    <td>b</td>
    <td>c</td>
  </tr>
  <tr class="digit">
    <td>3</td>
    <td>4</td>
    <td>5</td>
  </tr>
</table>

Editable Table Forms

#tablerow accepts a series of options to create editable table forms. Tell #tablerow which columns should be editable and which columns will uniquely identify each row, and it will generate HTML field names and edit-controls for each editable field. Later after the form is POSTed you can pass the Camping @input hash to the TentSteak.extract_form_fields method to automatically parse those field names and generate a hash of changed values.

First, choose the array of row key columns which will uniquely identify each row in the table. TentSteak uses this to generate unique HTML field names. Pass the array of column indexes to the :key_columns options (defaults to [0], for the case that the first column is a primary key).

The :text_columns option is an array of column names (i.e. method names to be called on a row-object) to render as editable text fields. The :select_columns option is an array of value pairs that define the column name and an option_list of display values to pass to select_menu.

For example, this

table do
  tablerow "Huzzahh!!", :methods => [:to_s, :length, :upcase]
    :text_columns => [:upcase], :select_columns => [[:length, (6..9)]]
end

This results in the following HTML table:

<table>
  <tr>
    <td>Huzzahh!!</td>
    <td>
      <select name="TS__Huzzahh!!__length__9">
        <option value="6">6</option>
        <option value="7">7</option>
        <option value="8">8</option>
        <option value="9" selected="selected">9</option>
      </select>
    </td>
    <td>
      <input type="text" value="HUZZAHH!!" name="TS__Huzzahh!!__upcase__HUZZAHH!!"/>
    </td>
  </tr>
</table>

#tablerow generates field names by joining a prefix (“TS”), the row key (created from :key_columns, in this case the first column of :to_s), the method name of this column (the matching element from :methods), and the current value of the column. This last part comes in handy in TentSteak.extract_form_fields for discarding POSTed values that haven’t changed. Also note that the current value of the select menu is selected by default.

You can pass in :form_prefix to customize the field prefix, as sort of a namespace for field sets; pass the custom prefix to #extract_form_fields to pull only the fields in that set.

If :select_row is set, #tablerow will add a non-data first column with a single checkbox to indicate whether the row as a whole is selected or not. This can be used by #extract_form_fields to filter out unselected rows, for example to only save checked rows to the database.

Here’s an example with custom :form_prefix and :select_row.

table do
  tablerow "Huzzahh!!", :methods => [:to_s, :length, :upcase]
    :text_columns => [:upcase], :select_columns => [[:length, (6..9)]],
    :form_prefix => "WOWZA", :select_row => true
end

This would yield:

<table>
  <tr>
    <td>
      <input type="checkbox" name="WOWZA__Huzzahh!!__ts_selected"></input>
    </td>
    <td>Huzzahh!!</td>
    <td>
      <select name="WOWZA__Huzzahh!!__length__9">
        <option value="6">6</option>
        <option value="7">7</option>
        <option value="8">8</option>
        <option value="9" selected="selected">9</option>
      </select>
    </td>
    <td>
      <input type="text" value="HUZZAHH!!" name="WOWZA__Huzzahh!!__upcase__HUZZAHH!!"/>
    </td>
  </tr>
</table>


409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'lib/tent_steak/table.rb', line 409

def tablerow(row, options = {})
  cells = if row.instance_of?(Array)
    row
  else
    # If row is a single object, expand it to an array of cell values.
    methods = options[:methods] || [:to_s]
    id_links = options[:id_links] || {}

    methods.map do |meth|
      value = row.send(meth)
        value = celldate(value) if value.instance_of?(Time) || value.instance_of?(Date)
        value = id_link(value, options[:id_links][meth]) if id_links.has_key? meth
        value
    end
  end

  rowclass = options[:rowclass]
  if rowclass.instance_of?(Array)
    # If a rowclass regexp matches the text in the given column, return the
    # associated CSS class, or nil if no match.
    #   [ [ 3, Regexp, "rowclass1" ], [ 5, Regexp, "rowclass2" ], ... ]
    rowop = rowclass.find { |col, op, css| cells[col].to_s =~ op }
    rowclass = rowop ? rowop[2] : nil
  end

  form_prefix = options[:form_prefix] || "TS"
  altcells = options[:altcells] || []
  altcell_class = options[:altcell_class] || "altcell"

  # Begin HTML row.
  tr(rowclass ? { :class => rowclass } : {}) do

    # Create (hopefully) unique row name according to cell indexes in :key_columns.
    # Default to first column.
    row_key = options[:key_columns] ?
      options[:key_columns].map { |i| cells[i] }.join('_') : cells.first.to_s

    # Insert leading checkbox to select row.
    if options[:select_row]
      td { input_checkbox _build_field_name(form_prefix, row_key, "ts_selected") }
    end

    # Render each table cell in this row.
    cells.each_with_index do |cell, i|
      style = altcells.include?(i) ? {:class => altcell_class} : {}

      # Only set rowspan/colspan if there's something to set.
      rowspan = _getspan(options[:rowspan], i)
      colspan = _getspan(options[:colspan], i)
      style[:rowspan] = rowspan if rowspan
      style[:colspan] = colspan if colspan

      if cell.instance_of?(Array)
        td(style) { cell.each { |line| text cellpad(line); br unless line == cell.last } }
      elsif field_spec = _find_text_spec(i, options)
        td(style) { input_text _build_field_name(form_prefix, row_key, field_spec, cell), cell }
      elsif field_spec = _find_select_spec(i, options)
        # Critical sanity check to avoid data loss from missing options.
        raise "Option #{cell.inspect} not found in #{field_spec[1].inspect}" unless field_spec[1].include?(cell)

        td(style) { select_menu _build_field_name(form_prefix, row_key, field_spec[0], cell), field_spec[1], nil, cell }
      else
        td(style) { cellpad(cell) }
      end
    end
  end
end