Fork me on GitHub

grid

An article by Gaspard Bucher

Display and edit a table from json data, or a zafu generated html table.

Display and edit a table from json data, or a zafu generated html table.

  • attr (attribute containing json data)
  • edit (true)

The “grid” is zenas flexible table editing feature.

With the grid tag it is used to display a table from json encoded data in a property field.

<r:grid attr='shopping'/>

To edit and create real node data, even large batches of it, the “Grid” javascript library can be used on a html table that you render with a zafu template (see “Batch edit” below).

edit

griddemo

Editing a grid in a form.

Features:

  1. Press on the ”+” or ”-” icons to add/remove rows and columns.
  2. Paste content from a spreadsheet when on a cell.
  3. Move around with arrows, tabs, shift-tab.
  4. Create new lines with enter.

To enable editing, you must set edit to true or trigger js grid with:

$$('.grid').each(function(e) {Grid.make(e)})

Editing requires the “grid.js” javascripts methods, which can be included in the head of the document with <r:javascripts list='grid'/> or just <r:javascripts/> (included by default).

When edited, the table is serialized in a hidden input field corresponding to the “data-a” attribute. This means that the user must validate the form to save the new content.

Usually, the edit='true' option is used inside a form (this creates the example shown above):

<r:form do='default' label='t'>
  <r:input name='title'/>
  <r:grid attr='shopping' edit='true'/>
  <r:input type='submit'/>
</r:form>

Grid options

The following options can be passed to the zena’s “Grid.make” javascript function to create a grid:

helper

Defined with helper: in Javascript or with the data-helper attribute on the table element. The helper is a form used to display special select/checkbox input elements when editing a cell:

<div id='h_grid' style='display:none'>
  <!--  Special select for "parent_id" field -->
  <select name='parent_id' do='contracts where status > 0 in site'>
    <option value='#{id}' do='each'><r:title/> (<r:parent do='title'/>)</option>
  </select>

  <!-- employee_id input, "data-d" means the "set by default" -->
  <r:input data-d='true' type='hidden' param='employee_id' value='#{id}'/>

  <!-- Use a textarea instead of an input for "text" attribute -->
  <r:textarea param='text'></r:textarea>
</div>          

newRow

Id of a “tr” element to use when inserting new rows.

  <table style='display:none'>
    <tr id='new_row'>
      <td class='date'></td>
      <td></td>
      <td class='work_done num'></td>
      <td class='num'></td>
      <td class='num'></td>
      <td></td>
      <td class='text'></td>
    </tr>
  </table>

onSuccess / onFailure

Callback function if save/create succeeds or fails. The function has the following signature:

var onSuccess = function(grid, id, op, done_count, todo_count) {}

“op” is the html method (put/post). “id” is the row id concerned by the operation. “done_count” and “todo_count” can be used to display progress bars.

onStart

This is called once all operations are ready. The “operations” contains a hash with the number of operations per method. “data” holds the detailed data that will be submitted. Below is the default implementation:

var onStart = function(operations, data) {
  if (operations.post) {
    return confirm('Create '+operations.post+' nodes ?')
  }
  return true
}

onChange

Triggered whenever the content of a cell changes. If the function returns false, the change is discarded. The function should return a value object or val.

var onChange = function(cell, val, attr) {}

autoSave

If set to “true”, the content is directly saved after change operation.

sort

If set to “false”, do not allow sorting.

add

If set to “false”, do not allow adding rows.

remove

If set to “false”, do not allow removing rows.

keydown

This allows intercepting keyboard events in cells (for example to allow special commands). Return “true” if the event has been used and should not be sent to the cell.

var keydown = function(event, key) {}

show

This is a hash containing:

{ attr: {value: displayValue, value2: display},
  other_attr: {...},
}

zazen grid

You can also “import” add a grid inside zazen text by writing [grid]shopping[/grid] (shopping is the attribute with the json data for the grid to display).

Batch edit of multiple nodes

In addition to edit json data saved in a single node propertiy, the grid feature can also be used to edit the properties of multiple nodes.

First create a zafu template that renders a html table for the data you wish to enter or edit. Every table row needs to be rendered with an id=’id_#{id}’ (providing the ID of the node the line represents). If initially only an empty table is rendered, you will still be able to create new objects with the grid later.

If you want to allow creating new nodes, you may want to render one more table row (hidden with style=’display:none’) to use it as the template for new rows. Later specify this tr element’s id as newRow in the javascript Grid.make() call.

Then add data-type attributes to your table elements that specify the types of the data. The grid javascript will later make the data editable according to these data-type attributes. For example, begin to define the columns you want to edit by adding data-a attribute definitions to the table header “th” cells.

Available attributes:

attribute set on specifies
id rows (tr) ‘id_#{id}’ to identify the corresponding node
data-a (attribute) table header (th) corresponding node property
data-l (list or link lines?) table or row? that lines represent link targets
data-r (rows) table or column? that rows represent link targets
data-m (mode) rows/columns (th) or cells (td) “r” (readonly)
data-v (value) columns/rows (th) or cells (td) actual property value (e.g. #{id}), if table renders a related property (e.g. title)
data-d (default) input elements in helper div “true” (default value for new nodes)
data-base row (tr) json hash added to every request
data-helper ? ?
link_id data-base attribute id of the node where links will be edited

To define specific form input elements, render a div (hidden with style=’display:none’) containing the form input elements for editing data-a (attribute) values. You may set add data-d (default) attributes set to “true” on the elements, to have the final form’s value default to the provided value. Later specify the div’s id in a data-helper attribute on a table element, or as helper: option in the javascript Grid.make() call.

Finally add a r:js tag to the template that calls zena’s Grid.make() javascript function.

Node editing example

Here is an example header table row:

    <table class='grid' id='grid' do='entries'>
      <tr>
        <th data-a='date' do='t'>date</th>
        <th data-a='parent_id' do='t'>contract</th>
        <th data-a='work_done' do='t'>work_done</th>
        <th data-a='work_rate' do='t'>work_rate</th>
        <th data-a='work_expense' do='t'>work_expense</th>
        <th data-a='work_type' do='t'>work_type</th>
        <th data-a='text' do='t'>text</th>
      </tr>

You can also define data-l (list mode) or data-r (column list mode). These modes let users use a grid to toggle relations between columns and rows. See below “Batch toggle”.

You can use data-m (mode) set to “r” for readonly cells such as extra header columns or cells:


      <tr data-m='r'>
        <th data-m='r'></th>
        <th data-m='r'></th>
        <th data-m='r' class='num' id='work_done'></th>
        <th data-m='r'></th>
        <th data-m='r'></th>
        <th data-m='r'></th>
        <th data-m='r'></th>
      </tr>

You then display each object to be edited. The row must contain id_[NODE_ID] as dom id. Note also how data-v is used when the actual value (parent_id for example) is different from the displayed value (name of parent).

      <r:each>
        <tr id='id_#{id}'>
          <td class='date' do='this.date' format='%d.%m.%Y %H:%M'/>
          <r:parent>
            <td data-v='#{id}' do='title'/>
          </r:parent>
          <td class='work_done num' do='work_done'/>
          <td class='num' do='work_rate'/>
          <td class='num' do='work_expense'/>
          <td do='work_type'/>
          <td class='text' do='text'/>
        </tr>
      </r:each>
    </table>

You can then define some helpers to edit some particular attributes by preparing input tags. If data-d is set to “true”, the value of the input element is used as default value if a new object is created.


    <div id='h_grid' style='display:none'>
      <select name='parent_id' do='contracts where status > 0 in site'>
        <option value='#{id}' do='each'><r:title/> (<r:parent do='title'/>)</option>
      </select>

      <r:input data-d='true' type='hidden' param='employee_id' value='#{id}'/>
      <r:input data-d='true' type='hidden' param='klass' value='TimeEntry'/>
      <r:textarea param='text'></r:textarea>
    </div>

You can define a table row to use when adding new objects if you need to set some specific class names to cells:


    <table style='display:none'>
      <tr id='new_row'>
        <td class='date'></td>
        <td></td>
        <td class='work_done num'></td>
        <td class='num'></td>
        <td class='num'></td>
        <td></td>
        <td class='text'></td>
      </tr>
    </table>

And finally setup the javascript hooks:

    <r:js>
      // Some custom JS to compute running total in headers
      Teti.computeTotal('grid')

      // Setup grid
      Grid.make('grid', {
        newRow: 'new_row',
        helper: 'h_grid',
        fdate: '%d.%m.%Y %H:%M',
        // This function computes running total and helps with
        // date entries (guessing current year, etc).
        onChange: Teti.onChange,
        onStart: function(operations, data) {
          return true
        }
      })
    </r:js>

Editing link properties

If you want to edit a link _status or link _comment, you need to pass the ID of the link itself (the “link_id”) on each request (a link’s context only points to the link target). Such default parameters can be added to the data-base parameter that is submitted on each request, even if it has not changed (it could thus also be useed for some auth token). The data-base attribute takes a json hash, and can be added to a row like this:

<table id='grid' class='grid emp' do='gefo_a_employees'> //! context is a list of links now
  <tr>
    <th data-m='r'         do='t'>employee</th>
    <th data-a='l_status'  do='t'>form_status</th>
    <th data-a='l_comment' do='t'>form_comment</th>
  </tr>

  <tr id='id_#{id}' data-base='{link_id:#{link_id}}' do='each'>
    <td data-m='r'><r:title/></td>
    <td data-a='l_status' data-v='#{l_status || 50.0}' do='t("form_#{l_status}")'></td>
    <td data-a='l_comment' do='l_comment || ""'/>
  </tr>
</table>       

Batch toggle

This example assigns roles to employees through the relation a_roles_ids. The headers contain the id to be set in data-a and the first cell in each row is a table header with data-v set to the employee id.

This example is a real-case use and we thought it could be interesting to see some advance tricks such as sub-grouping in the query.

<r:void do='roles from role_groups select title as gp_title, position as gp_pos in site order by gp_pos asc, gp_title asc, position asc, title asc' do='set' roles='this'>
  <table id='grid' data-l='a_roles_ids' class='list fixed grid' do='employees where emp_status >= 100 order by title asc'>
    <tr style='display:none' do='roles' do='set' last='""'>
      <th></th>

      <r:each>

        //! Sub-title
        <r:if test='last != this.gp_title'>
          <r:set last='gp_title'/>
          <th></th>
        </r:if>

        //! Role
        <th data-a='#{id}'></th>
      </r:each>
    </tr>

    <tr id='id_#{id}' do='each' do='set' last='""' emp='this' list='a_role_ids' do='roles'>
      <r:emp>
        <th class='emp' data-v='#{id}' do='link'/>
      </r:emp>

      <r:each>

        <r:if test='last != this.gp_title'>
          <r:set last='gp_title'/>
          <td data-m='r' class='gp_group'></td>
        </r:if>

        <r:if test='list.include?(id)'>
          <td data-v='on' class='on'></td>
          <r:else>
            <td data-v='off'></td>
          </r:else>
        </r:if>
      </r:each>
    </tr>
  </table>
</r:void>
<r:js>
  Grid.make('grid', {
    add: false,
    remove: false,
  });
</r:js>