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
-
#auto_table(rows, options = {}) ⇒ Object
Autogenerate an HTML table from an array of ActiveRecord objects.
-
#blankrow(colspan = 1) ⇒ Object
Insert a blank table row with an optional colspan.
-
#celldate(date, format = '%Y/%m/%d') ⇒ Object
Format a date for a table cell.
-
#cellpad(value, pad = " ") ⇒ Object
If
valueis nil or whitespace, substitute with “ ”. -
#grouping_table(headers, groups, rowopts = {}, &block) ⇒ Object
Generate a table with blank rows between groups of related rows.
-
#hrow(cells, options = {}) ⇒ Object
Add pairs of fields in a vertical-style table (field names along the side, not the top).
-
#tableheader(cells, options = {}) ⇒ Object
Create a table header from an array of column titles, e.g.
-
#tablerow(row, options = {}) ⇒ Object
Add a row of HTML table cells.
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 totruefor more human friendly column headers; ignored for columns that are explicitly set with:column_titles. Defaults tofalse. -
:column_titles: an optional array of column header names; defaults to the database column names. Pass innilfor 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_colrows; 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, = {}) 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 = [:methods] || (rows.first.class.respond_to?(:column_names) && rows.first.class.column_names) || [] [: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 << (([:column_titles] && [:column_titles][i]) || ([:humanize] ? dbcol.titleize : dbcol)) end # If column_names is empty (non-AR objects) and :column_titles is set, use that. column_titles = [:column_titles] if column_names.empty? && [:column_titles] # Insert edit checkbox header if any. column_titles.unshift([:select_row]) if [:select_row] alt_style = [:group_col_class] || "altrow" table do tableheader column_titles unless column_titles.empty? for row in rows # Optional row group highlighting. rowopts = .dup if [:group_col] this_group_value = row.instance_of?(Array) ? row[[:group_col]] : row.send(column_names[[: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 = " ") ⇒ Object
If value is nil or whitespace, substitute with “ ”. 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 = " ") (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, = {}) titlestyle = { :class => [: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, = {}) titlestyle = { :class => [:titleclass] || "tabletitle" } colspans = [:colspan] || {} tr(titlestyle) do if colspans.instance_of? Hash cells.each_with_index do |hdr, i| span = _getspan([: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
-
:rowclassis a CSS class to tag the row with. Strings are assigned directly. If:rowclassis an Array, then the row’s CSS class is dependent on the contents of a specific column in the table. See below for details. -
:colspanis 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. -
:rowspanworks like:colspanexcept with thecolspanHTML attribute. -
:altcellsis an array of cell indexes to assign the:altcell_classCSS style to. Useful for highlighting specific table cells. -
:altcell_classis the CSS class for the:altcellsarray; 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 ifrowis 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_columnsis an array of column indexes to uniquely identify each row. -
:text_columnsdefines which table columns should be editable as a text field. -
:select_columnsdefines which table columns should be editable as a selection menu. -
:select_rowadds a leading column with checkboxes to select rows, e.g. to save. -
:form_prefixoverrides 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, = {}) cells = if row.instance_of?(Array) row else # If row is a single object, expand it to an array of cell values. methods = [:methods] || [:to_s] id_links = [: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, [:id_links][meth]) if id_links.has_key? meth value end end rowclass = [: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 = [:form_prefix] || "TS" altcells = [:altcells] || [] altcell_class = [: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 = [:key_columns] ? [:key_columns].map { |i| cells[i] }.join('_') : cells.first.to_s # Insert leading checkbox to select row. if [: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([:rowspan], i) colspan = _getspan([: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, ) td(style) { input_text _build_field_name(form_prefix, row_key, field_spec, cell), cell } elsif field_spec = _find_select_spec(i, ) # 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) { _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 |