Fork me on GitHub

HTML Forms with Zena

Build-in Helpers

The r:form helper can be used to render a html form with input widgets that can set properties of the current context’s node (through node[...] parameters used when the name= attribute is used on r:input) or arbitrary URL parameters (using param= attribute on r:input).

If a form is rendered within a r:new context, the form creates a new node upon form submission.

A zena block is a dynamic javascript controlled part in the browser’s document representation of the page, and blocks can be used to render a form composed of dynamic parts.You call do=’block’ to render the current element as a block. The block will then be known by the browser and server, and you can:
  • Reload a block. This means to request newly rendered html code and substitute or insert it to the browsers copy of the block in the page.
  • Use the block as a preview area. And for example, use r:link to update this block (maybe even with insert=”bottom”). Then clicking the link will reload the block in the browser with the linked node id (beware, there may be link crawlling robots coming around). For just passing the IDs, this does not even require explicitly adding any url query parameter at all.

The r:filter helper renders a javascript enhanced form that can update a block. With the preview feature r:form now also supports this explicitly.

Zena also has a javascript based grid feature, It allows to edit a data table stored in a single (json) node property, as well as regular node data. The latter works based on a rendered html table “view” that presents the node data that should be editable. Check out the grid demonstration!

Form Design

Basic Forms

Basic forms only provide inputs for properties of a single node object. These can be created very trivially with zenas edit or add features.

Interactive Forms

With a good (well normalized) data model, meaningfull views – and thus forms as well – will most of the time require to manipulate multiple nodes in a single form (or at least template view appearence), and need to interactively create or update different nodes. Is this possible? Certainly yes, even though there is not yet a trival way available in zena.

Regardless if interactive forms are complicated or not, they can either be linear or complex (i.e. contain feedback loops).

Linear Interactive Forms

In linear forms the input is only processed in parallel “input chains”. For example, seachboxes update selectboxes that update parts of a form that can ultimately be submitted. In the process, the selectbox may offer to create new nodes if the search did return no appropriate existing entry.

linear-form
  • Linear forms are appropriate if no input aggreation is requried.
  • If submission requires more than one input, server side logic alone can not show the submit button only if submission is possible.
  • Filter and selection elements in the input chains are not updated after entering data, will stay visible and clutter the page.
  • May be made more useable with javascript tricks but this quickly gets complicated and hard to maintain.

Complex Interactive Forms

In complex forms the selection input is processed either in a “url parameter loop”, or a javascript loop of event handlers. Using an url parameter loop allows complex interactive forms to be functional even without javascript, and allows to simply add query strings to links in order to point users to prefilled forms. Here, we use an url parameter loop as example: searchboxes update selectboxes that update the overall block (or the whole page without javascript). Using separate blocks for the searchboxes allows to only update separate selections, and avoids to repeat all database queries when just one filter input is changed. Only selections made have to trigger a global update. To avoid dead times and stale states, the url parameter passing loop should not span over more than two blocks that update each other.

complex-form

Without the looping and reloading of the parent page or block, changing one selection would render the state saved in other selections stale, and intermittent creation of different type of nodes would loose or reset other selections to a previous state.

General Implementation

Use blocks (do=’block’) for parts that need to be dynamicly updated, and use r:filter and r:link to update those block or insert the content into the blocks.

Blocks:
  • Get rendered on updates according to the template that defined it.
  • Can be made to update (rendered again) by clicking on a special <r:link update='block_name'> link, or with a Javascript call (see below).
  • The context of the update will be the link’s target node (current context by default).
  • Blocks within linear forms may be “link updated” with insert=’bottom’, for example to add multiple relation links to the form. In complex forms that use an url parameter loop, the target block will need to add the ID of the “link updated” context into the parameter loop, in order to maintain the state accross further updates.
Creating new nodes:
  • Update a block that conditionally renders a parameterized new context containing a creation form.
Input states (filter selections etc.) can be passed on through
  • another r:filter that updates the final form (Currently filters get disturbed if they update themself, because the updates while still only browsing the options can not be disabled)
  • url parameters (may also be self defined arrays)
  • or by “link updating” the target block
Default and suggest values:
  • Assignments for r:new and r:filter blocks are meant to be passed on.
  • The selected= attribute of select widgets should show up as suggested choice.
  • The blank= assignments for checkboxes etc. are meant as fallback values.

Linear Interactive Form Problems:

  • Submitting a creation form opens the new node. I managed to let the form redirect to reload the current page, but this requires to explicitly pass on all current state parameters of the page (same as for complex forms).
  • Is there maybe a way to let submitting a form only update or reload the specific selection block?

Complex Interactive Form Problems:

  • Easy way to automatically pass on all url parameters by default, without maintaining manual encode_params etc. lists? (The parameter passing should be overrideable.)

Bugs?

Attribute assignments for r:filter blocks are only passed on during the
initial request. (workaround: add hidden input fields for every
parameter to pass on)

Limitations

  • Single click creation of multiple nodes will need javascript. Or loading a special node creation block with appropriate parameters?
  • Atomic submissions (create or modify all nodes or none) will require a special rails controller and route.

Interactive Form Examples

Filtered selections

1. Solution with a radio select

//! Use CSS to position the filter near the input field
<r:filter key='f_club' update='selector'/>

<r:form> //! Main form for object to be created/edited
  //! We cannot insert <r:filter/> here because it creates a nested html form.
  <div id='selector' do='block' do='clubs where title like "%#{params[:f_club]}%" in site limit 5 paginate p'>
    <p do='link' page='list' update='selector'/>
    <p do='each'><input type='radio' name='node[club_id]' value='#{id}'/> <r:title/></p>
  </div>

  ... rest of the form
</r:form>

2. Solution with links

//! Use CSS to position the filter near the input field
<r:filter key='f_club' update='club_selector'/>

<r:form> //! Main form for object to be created/edited
  <div id='club_selector' do='block' do='if' test='main.id != start.id'>
    //! Reloaded from click in selector
    You selected "<r:title/>"
    <input type='hidden' name='node[club_id]' value='#{id}'/>
    <r:else>
      <div id='selector' do='block' do='clubs where title like "%#{params[:f_club]}%" in site limit 5 paginate p'>
        <p do='link' page='list' update='selector'/>
        <p do='each' do='link' update='club_selector'/>
      </div>
    </r:else>
  </div>

  ... rest of the form
</r:form>

If you want to have a filtering input field inside the form, you need to write a custom observing Javascript filter by hand (see below).

Custom Javascript

For an introduction see: ZJS

Javascript reloading of blocks

This can be triggered with Zena.reload.

Zena.reload('block_name', { id:"<r:main do='id'/>", f_cat:"<r:id/>"})

Note that to change or preview node properties, you have to nest these into the “node:” parameter.

Zena.reload('block_name', { id:"<r:main do='id'/>", node:{title:"new title"}})

If you want to preview a form’s edited values, you can use “preview_node”, this applies all the params[:node] values to the current node context (klass must somehow match or you can use “new”):

<div id='preview' do='block' do='Product.new' do='preview_node'>
</div>

Observing javascript filter

<r:form> //! Main form for object to be created/edited
  <input name='f_club' id='f_club'/>
  <r:js>
    $('f_club').observe('change', function(ev) {
      // this = observed input
      var title = this.value
      Zena.reload('selector', {id:"<r:main do='id'/>", f_club:title})
    })
  </r:js>

  //! We cannot insert <r:filter/> here because it creates a nested html form.
  <div id='selector' do='block' do='clubs where title like "%#{params[:f_club]}%" in site limit 5 paginate p'>
    <p do='link' page='list' update='selector'/>
    <p do='each'><input type='radio' name='node[club_id]' value='#{id}'/> <r:title/></p>
  </div>

  ... rest of the form
</r:form>

Adjusting has_many relations

For the form element:

  • Initialize a variable in the browser, using the form element value if present.
  • Initialize a form setting function to adjust the variable according to clicks on the selectable elements (rendered with specific css class and data-id attribute).
<r:form>
  <r:input id='add_ids' name='linked_post_ids' value='#{linked_post_ids.join(",")}'/>
</r:form>

<r:js>
Post_ids = $('add_ids').value.split(/\s*,\s*/)
PostClick = function(ev) {
  var e = this
  var id = e.getAttribute('data-id')
  var pos = Post_ids.indexOf(id)
  if (pos >= 0) {
    // remove
    Post_ids.splice(pos, 1)
    e.removeClassName('on')
  } else {
    // add
    Post_ids.push(id)
    e.addClassName('on')
  }
  $('add_ids').value = Post_ids.join(',')
}
</r:js>

For each selectable element:

  • Adjust the css class according to current selection, and initialize an observer to call the form setting function.
<r:filter update='list' key='f'/>
<ul id='list' do='block' do='posts where title like "%#{params[:f]}%" in site limit 5'>
  <li data-id='#{id}' class='post' do='each' do='title'/>
</ul>

<r:js>
$$('#list .post').each(function(e) {
  var id = e.getAttribute('data-id')
  if (Post_ids.indexOf(id) >= 0) {
    e.addClassName('on')
  }
  e.observe('mousedown', PostClick)
})
</r:js>

Optionally, if you want to show the list of selected items in the form as a list (and hide the text input box):

  • Render a block that lists the nodes selected for linking.
<ul id='sel_list' do='block' do='set' ids='params[:post_ids] || linked_post_ids.join(",")' do='posts where id in #{ids} in site'>
  <li do='each' do='link'/>
</ul>
  • And add a ‘reload’ at the end of the PostClick function.
Zena.reload('sel_list', {post_ids:Post_ids.join(',')})

Creating multiple nodes with a single javascript form

This can be done by using Zena.post, the “grid” editor is a (complex) example using table data.

The gist of it all is the Ajax call to create the node:

new Ajax.Request('/nodes', {
          parameters: attrs,
          onSuccess: function(transport) {
            done_count++
            var reply = transport.responseText.evalJSON()
            // = created node (reply.id ==> object id)
          },

          onFailure: function(transport) {
            done_count++
            var errors = {}
            transport.responseText.evalJSON().each(function(e) {
              errors[e[0]] = e[1]
            })
            // = errors

          },
          method: 'post'
        });

This implies reading the form content with Javascript and then submitting node creation for each object. You can also pass the list of params. Here is a real-world example:

Gefo.make_default_chapters = function(r) {
  Zena.do('post', 'chapters', [
      {node:{parent_id:<r:start do='id'/>, klass:'GefoRoleChapter', title:'Exigées', position:1, gefo_priority:10}},
      {node:{parent_id:<r:start do='id'/>, klass:'GefoRoleChapter', title:'Normales', position:2, gefo_priority:5}},
      {node:{parent_id:<r:start do='id'/>, klass:'GefoRoleChapter', title:'Optionnelles', position:3, gefo_priority:1}}
      ], {
        onSuccess: function(e) {
          window.location.href = r || window.location.href
        },
      })
}