Stateful Widgets

Stateful widgets freeze themselves to the session and automatically restore their state in the next request. State here means

  1. structure – where each widget preserves its children – and
  2. instance variables – which survive between requests

This can be very helpful when building sophisticated widget trees, like forms and wizards with workflows. However, often stateless widgets will fit your needs and involve less internal complexity.

Example?

Stateful form

Why not use that form.

We have a container widget that keeps the upload fields, one widget instance per field.
When clicking “Attach another file” we trigger an event and add another upload field in the container.

During the whole process, the widget developer doesn’t care about state-housekeeping at all.

class FormController < ApplicationController
  def index
    use_widgets do |root|
      root << widget('form/base', 'uploads')
    end

We add stateful widgets to our controller with #use_widgets in an action. These #use_widgets blocks are invoked only once per session, once called, stateful widgets will stick to root till the session is cleared.

It is also possible to add stateless widgets here, they won’t survive the next request, though.

Adding widgets dynamically

The Form::Base widget initially adds one upload field and renders.

module Form
  class Base < Apotomo::StatefulWidget
    responds_to_event :addField, :with => :add_field  # triggered when "Attach another file" is clicked.
    
    def display # the initial state.
      self << widget("form/upload_field", 'upload_1')
      render
    end
    
    def add_field
      self << widget("form/upload_field", 'upload_2')
      
      new_upload_markup = self['upload_2'].invoke  # render the new widget.
      
      render :js => "Element.insert('#{widget_id}', { below: '#{new_upload_markup}' });"
  end

This looks worse than it is!

In #add_field we just add another stateful widget to the container, render that new kid and send back a JavaScript call to insert the new upload field under the first.

Keeping instance variables

Stateful form elapsed time

A really handy feature with Statefuls is that they restore their instance variables in each request.

Say we wanted to display the elapsed time between the first rendering of our form widget and the time we added a new field. An absolutely useless requirement for a form, isn’t it?

Anyway, we’d change the code like this.

    def display # the initial state.
      @created_at = Time.now  # save the time we were rendered the first time.
      
      self << widget("form/upload_field", 'upload_1')
      render
    end
    
    def add_field
      self << widget("form/upload_field", 'upload_2')
      new_upload_markup = self['upload_2'].invoke  # render the new widget.
      
      new_upload_markup += "Added after #{(Time.now - @created_at)} secs"  # We still have @created_at.
      
      render :js => "Element.insert('#{widget_id}', { below: '#{new_upload_markup}' });"
  end

Notice how we define @created_at in the initial state. As time and requests go by, there might be a click on "Attach another file" which triggers #add_field.

Each time an upload field is added, we compute the elapsed time. The ivar `@created_at` is around as if there had been no request at all!

Discussion

Stateful form fields overflow

Although there might be multiple requests between the initial rendering of the form and the click on “Attach another file” the whole widget tree starting at 'uploads' will remain the same. If you add 20 new fields, they will still be there, even after 200 requests.

The same applies when removing widgets, they’re lost and gone forever. It is important to understand that Statefuls just provide a convenient way for something you could do yourself with state less widgets, too.

Stale session while developing statefuls

When developing statefuls you often want to flush the widget tree, as classes, initial structure or content changed.

  1. Add ?flush_widgets=1 to your url to instruct Apotomo to flush the existing widget session at request start.
  2. You might also fire $ rake db:sessions:clear

Watch out!

We got two limitations when using statefuls.

Hey- that were three!

[cookie overflow] [benchmarks stateful <-> stateless]
blog comments powered by Disqus