Click here to Skip to main content
15,880,469 members
Articles / Programming Languages / Ruby

A PropertyGrid implemented in Ruby on Rails

Rate me:
Please Sign up or sign in to vote.
4.79/5 (4 votes)
22 Nov 2013CPOL6 min read 19.5K   146   3  
Using JQuery UI and minimal Javascript to create a dynamic property grid editor that can be initialized in a fluid programming style or with a minimal DSL.

Image 1

Get The Source From GitHub

git clone https://github.com/cliftonm/property_grid_demo 

The code for this control can now be installed as a gem:

gem install property_grid 

and can be downloaded from here:

git clone https://github.com/cliftonm/property_grid

Introduction

I needed a general purpose property grid editor that supported some fancy things like date/time pickers, color pickers, etc., based on record fields known only at runtime (this is ultimately a part of my next installment of the "Spider UI" article series.)  There's a snazzy Javascript-based property grid here, but I wanted something that was minimally Javascript and more Ruby-on-Rails'ish.  I also wanted a server-side control that could interface well with record field types and that would dynamically generate the grid based on schema information like table fields.

I have put together is a set of classes to facilitate building the content of a property grid control on the server-side.  You will note that I opted for actual classes and a "fluid" programming style, but if you don't like the way the actual implementation looks using a "fluid" technique, I have also put together a very minimal internal Domain Specific Language (DSL) that you can use instead -- basically just method calls that hide (using static data) the internal management of building the property grid instance.

As in my previous articles, I will be using Sass and Slim scripting for the CSS and HTML markup.

Supporting Classes

There are several supporting classes:

  • PropertyGrid - the container for the groups and group properties
  • Group - a group of properties
  • GroupProperty - a property within a group

Class PropertyGrid

# A PropertyGrid container
# A property grid consists of property groups.
class PropertyGrid
  attr_accessor :groups

  def initialize
    @groups = []
  end

  # Give a group name, creates a group.
  def add_group(name)
    group = Group.new
    group.name = name
    @groups << group
    yield(group)         # yields to block creating group properties
    self                 # returns the PropertyGrid instance
  end
end

There are two important points to this class:

  1. Because add_group executes yield(group), the caller can provide a block for adding group properties. 
  2. Because add_group returns self, the caller can continue, in fluid programming style, to add more groups.

Thus, we can write code like this:

@property_grid = PropertyGrid.new().
  add_group('Text Input') do |group|
    # add group properties here.
  end.  #<----  note this syntax
  add_group('Date and Time Pickers') do |group|
    # add group properties here.
  end

Notice the "dot": end. - because add_group returns self after the yield, we can use a fluid programming style to continue adding groups.

Class Group

# Defines a PropertyGrid group
# A group has a name and a collection of properties.
class PropertyGroup
  attr_accessor :name
  attr_accessor :properties

  def initialize
    @name = nil
    @properties = []
  end

  def add_property(var, name, property_type = :string, collection = nil)
    group_property = GroupProperty.new(var, name, property_type, collection)
    @properties << group_property
    self
  end
end

A group has a name and manages a collection of properties.  The add_property class returns self, so again we can use a fluid notation:

group.add_property(:prop_c, 'Date', :date).
  add_property(:prop_d, 'Time', :time).
  add_property(:prop_e, 'Date/Time', :datetime)

Notice the "dot" after each call to add_property, allowing us to call add_property again, operating on the same group instance.

Nothing about this is stopping us from using more idiomatic Ruby syntax, for example:

group.properties <<
  GroupProperty.new(:prop_c, 'Date', :date) << 
  GroupProperty.new(:prop_d, 'Time', :time) <<
  GroupProperty.new(:prop_e, "Date/Time", :datetime)

Class GroupProperty

This class is the container for the actual property:

include PropertyGridHelpers

class GroupProperty
  attr_accessor :property_var
  attr_accessor :property_name
  attr_accessor :property_type
  attr_accessor :property_collection

  # some of these use jquery: http://jqueryui.com/
  def initialize(var, name, property_type, collection = nil)
    @property_var = var
    @property_name = name
    @property_type = property_type
    @property_collection = collection
  end

  def get_input_control
    form_type = get_property_type_map[@property_type]
    raise "Property '#{@property_type}' is not mapped to an input control" if form_type.nil?
    erb = get_erb(form_type)

    erb
  end
end

I will discuss what get_erb does later.

Note that three fields are required:

  1. The symbolic name of the model's property
  2. The display text of the property
  3. The property type

Optionally, a collection can be passed in, which supports dropdown controls.  The collection can either be a simple array:

['Apples', 'Oranges', 'Pears']

or a "record", implementing id and name attributes, for example:

# A demo of using id and name in a combo box
class ARecord
  attr_accessor :id
  attr_accessor :name

  def initialize(id, name)
    @id = id;
    @name = name
  end
end

@records =
[
  ARecord.new(1, 'California'),
  ARecord.new(2, 'New York'),
  ARecord.new(3, 'Rhode Island'),
]

which is suitable for collections of ActiveRecord objects.

Class ControlType

This class is a container for the information necessary to render a web control:

class ControlType
  attr_accessor :type_name
  attr_accessor :class_name

  def initialize(type_name, class_name = nil)
    @type_name = type_name
    @class_name = class_name
  end
end

This is very basic - it's just the type name and an optional class name.  At the moment, the class name is used just for jQuery controls.

Defining Property Types

Property types are defined in property_grid_helpers.rb - this is a simply function that returns an array of hashes of type => ControlType.

def get_property_type_map
  {
    string: ControlType.new('text_field'),
    text: ControlType.new('text_area'),
    boolean: ControlType.new('check_box'),
    password: ControlType.new('password_field'),
    date: ControlType.new('datepicker'),
    datetime: ControlType.new('text_field', 'jq_dateTimePicker'),
    time: ControlType.new('text_field', 'jq_timePicker'),
    color: ControlType.new('text_field', 'jq_colorPicker'),
    list: ControlType.new('select'),
    db_list: ControlType.new('select')
  }
end

It is here that you would extend or change the specification for how types map to web queries.  Obviously you're not limited to using jQuery controls.

What Would a DSL Implementation Look Like?

Let's see what it would look like if I wrote the property grid container objects as a DSL.  If you're interested, there's a great tutorial on writing internal DSL's in Ruby here, and what I'm doing should look very similar.  Basically, DSL's use a builder pattern, and if you're interested in design patterns in Ruby, here's a good tutorial.

What we want is to be able to declare a property grid instance as if it were part of the Ruby language.  So I'll start with:

@property_grid = new_property_grid
group 'Text Input'
group_property 'Text', :prop_a
group_property 'Password', :prop_b, :password
group 'Date and Time Pickers'
group_property 'Date', :prop_c, :date
group_property 'Time', :prop_d, :date
group_property 'Date/Time', :prop_e, :datetime
group 'State'
group_property 'Boolean', :prop_f, :boolean
group 'Miscellaneous'
group_property 'Color', :prop_g, :color
group 'Lists'
group_property 'Basic List', :prop_h, :list, ['Apples', 'Oranges', 'Pears']
group_property 'ID - Name List', :prop_i, :db_list, @records

The implementation consists of three methods:

  1. new_property_grid 
  2. group 
  3. property

that are essentially factory patterns for building an instance of the property groups and their properties.  The implementation is in a module and leverages our underlying classes:

module PropertyGridDsl
  def new_property_grid(name = nil)
    @__property_grid = PropertyGrid.new

    @__property_grid
  end

  def group(name)
    group = Group.new
    group.name = name
    @__property_grid.groups << group

    group
  end

  def group_property(name, var, type = :string, collection = nil)
    group_property = GroupProperty.new(var, name, type, collection)
    @__property_grid.groups.last.properties << group_property

    group_property
  end
end

This implementation takes advantage of the variable @__property_grid which maintains the current instance being applied in the DSL script.  We don't use a singleton pattern because we want to allow for multiple instances of property grids on the same web page.

The advantages are fairly obvious - the resulting script to generate the property grid is compact and readable.  The above DSL is simple - it's effectively nothing more than helper methods that wrap the details of working with the underlying classes. 

As Martin Fowler writes here, while an internal DSL can often increase "syntactic noise", a well written DSL should actually decrease "syntactic noise", as this simple DSL does.  For example, compare the DSL:

@property_grid = new_property_grid
group 'Text Input'
group_property 'Text', :prop_a 

with a non-DSL implementation: 

@property_grid = PropertyGrid.new().
  add_group('Text Input') do |group|
    group.add_property(:prop_a, 'Text').
    add_property(:prop_b, 'Password', :password)
  end 

Certainly working with the class implementation, even in its "fluid" form, is noisier than the DSL!

Putting It Together

You will need a view, a controller, and a model to put this all together.

The View 

The basic view is straight-forward.  Given the model, we instantiate a list control where each list item is itself a table with two columns and one row:

=fields_for @property_grid_record do |f|
  .property_grid
    ul
      - @property_grid.groups.each_with_index do |group, index|
        li.expanded class="expandableGroup#{index}" = group.name
        .property_group
          div class="property_group#{index}"
            table
              tr
                th Property
                th Value
              - group.properties.each do |prop|
                tr
                  td
                    = prop.property_name
                  td.last
                    - # must be processed here so that ERB has the context (the 'self') of the HTML pre-processor.
                    = render inline: ERB.new(prop.get_input_control).result(binding)

  = javascript_tag @javascript
  
  javascript:
    $(".jq_dateTimePicker").datetimepicker({dateFormat: 'mm/dd/yy', timeFormat: 'hh:mm tt'});
    $(".jq_timePicker").timepicker({timeFormat: "hh:mm tt"});
    $(".jq_colorPicker").minicolors()

I'm not going to bother showing the CSS that drives the visual presentation of this structure. 

Javascript

Note that there are two javascript sections.  One is coded directly in the form to support the jQuery dateTimePicker, timePicker, and the colorPicker controls.

The other javascript is programmatically generated because it controls whether the property group is collapsed or expanded, which requires unique handlers for each property group.  Since this is known only at runtime, the javascript is generated by this function (in property_grid_helpers.rb):

def get_javascript_for_group(index)
  js = %Q|
    $(".expandableGroup[idx]").click(function()
    {
      var hidden = $(".property_group[idx]").is(":hidden"); // get the value BEFORE making the slideToggle call.
      $(".property_group[idx]").slideToggle('slow');

      // At this point, $(".property_group0").is(":hidden");
      // ALWAYS RETURNS FALSE

      if (!hidden) // Remember, this is state that the div WAS in.
      {
        $(".expandableGroup[idx]").removeClass('expanded');
        $(".expandableGroup[idx]").addClass('collapsed');
      }
      else
      {
        $(".expandableGroup[idx]").removeClass('collapsed');
        $(".expandableGroup[idx]").addClass('expanded');
      }
    });
  |.gsub('[idx]', index.to_s)

  js
end 

The ERB 

Note this line from above:

= render inline: ERB.new(prop.get_input_control).result(binding)

This takes ERB code that has been generated programmatically as well, as we need a control specific to the property type.  This is generated by the function get_erb which we saw earlier.

# Returns the erb for a given form type. This code handles the construction of the web control that will display
# the content of a property in the property grid.
# The web page must utilize a field_for ... |f| for this construction to work.
def get_erb(form_type)
  erb = "<%= f.#{form_type.type_name} :#{@property_var}"
  erb << ", class: '#{form_type.class_name}'" if form_type.class_name.present?
  erb << ", #{@property_collection}" if @property_collection.present? && @property_type == :list
  erb << ", options_from_collection_for_select(f.object.records, :id, :name, f.object.#{@property_var})" if @property_collection.present? && @property_type == :db_list
  erb << "%>"

  erb
end 

The Model

We need a model for our property values.  In the demo, the model is in property_grid_record.rb:

class PropertyGridRecord < NonPersistedActiveRecord
  attr_accessor :prop_a
  attr_accessor :prop_b
  attr_accessor :prop_c
  attr_accessor :prop_d
  attr_accessor :prop_e
  attr_accessor :prop_f
  attr_accessor :prop_g
  attr_accessor :prop_h
  attr_accessor :prop_i
  attr_accessor :records

  def initialize
    @records =
      [
        ARecord.new(1, 'California'),
        ARecord.new(2, 'New York'),
        ARecord.new(3, 'Rhode Island'),
      ]

    @prop_a = 'Hello World'
    @prop_b = 'Password!'
    @prop_c = '08/19/1962'
    @prop_d = '12:32 pm'
    @prop_e = '08/19/1962 12:32 pm'
    @prop_f = true
    @prop_g = '#ff0000'
    @prop_h = 'Pears'
    @prop_i = 2
  end
end

All this does is initialize our test data.

The Controller

The controller puts it all together, instantiating the model, specifying the property grid properties and types, and acquiring the programmatically generated javascript:

include PropertyGridDsl
include PropertyGridHelpers

class DemoPageController < ApplicationController
  def index
    initialize_attributes
  end

  private

  def initialize_attributes
    @property_grid_record = PropertyGridRecord.new
    @property_grid = define_property_grid
    @javascript = generate_javascript_for_property_groups(@property_grid)
  end

  def define_property_grid
    grid = new_property_grid
    group 'Text Input'
    group_property 'Text', :prop_a
    group_property 'Password', :prop_b, :password
    group 'Date and Time Pickers'
    group_property 'Date', :prop_c, :date
    group_property 'Time', :prop_d, :date
    group_property 'Date/Time', :prop_e, :datetime
    group 'State'
    group_property 'Boolean', :prop_f, :boolean
    group 'Miscellaneous'
    group_property 'Color', :prop_g, :color
    group 'Lists'
    group_property 'Basic List', :prop_h, :list, ['Apples', 'Oranges', 'Pears']
    group_property 'ID - Name List', :prop_i, :db_list, @property_grid_record.records

    grid
  end
end

There is also the supporting function (in property_grid_helpers.rb):

def generate_javascript_for_property_groups(grid)
  javascript = ''

  grid.groups.each_with_index do |grp, index|
    javascript << get_javascript_for_group(index)
  end

  javascript
end

And voila:

Image 2

Conclusion

Something like this should be easily ported to C# / ASP.NET as well, and I'd be interested to hear from anyone who does so.  Otherwise, enjoy and tell me how you've enhanced the concept. 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
-- There are no messages in this forum --