Flexible Rails Modeling with ActiveRecord::Store – Part II

The quick recap – we need a flexible model structure to handle similar but distinct Blocks (read Part I).

Now that we’ve got the base model setup and the ability to define model attributes with simple YAML files, we need to figure out how to enter them in the system (controllers/forms) and how to display them (views).

Controllers

We first require that when we create a new Block we specify the desired type. In the controller:

class BaseBlocksController < ApplicationController
  before_filter :type_is_required, :only => [:index, :new]

  protected
  def type_is_required
    message = "You must specify a block type."
    redirect_to('/', :flash => {:error => message}) unless params[:type]
  end
end

Then we can define our new endpoints to this type to build a new Block.

  def new
    @block = Block.new :block_type => params[:type]
  end

We also need to update our #update and #create methods to use the :without_protection param when building our new objects. This will avoid the ActiveModel::MassAssignmentSecurity::Errorthat we would get since our custom attributes don’t have attr_accessors defined in the normal way.

def create
    @block = Block.new(params[:block], :without_protection => true)
    if @block.save
      redirect_to @block, notice: 'Block was successfully created.'
    else
      render action: "new"
    end
  end

  def update
    @block = Block.find(params[:id])
    if @block.update_attributes(params[:block], :without_protection => true)
      redirect_to @block, notice: 'Block was successfully updated.'
    else
      render action: "edit"
    end
  end

You’ll notice, in the model initialization (in Part I) that we protected our database defined fields in the #add_block_type so we can’t accidentally override database fields with the custom fields.

Forms

We’ve already written some model code that figures out how to handle the dynamic fields using ActiveRecord::Store. With a little help from ActionView::Helpers::FormBuilder and SimpleForm, we can setup dynamic form fields pretty easily.

To build custom forms, we write a small helper:

module BlocksHelper
  def render_dynamic_form_fields(form, block, opts={})
    s = ''
    begin
      (Block.block_types[block.block_type] || {}).each do |k,v| 
        opts.merge!({as: v[:datatype], label: v[:display_name]})
        s += form.send('input', k, opts)
      end
      s.html_safe
    rescue Exception => ex
      Rails.logger.warn 'Failed to add form fields for block [%s]' % block.inspect
      Rails.logger.warn 'Check the YAML file for that block and make sure it\'s properly configured'
      Rails.logger.warn ex
      ''
    end
  end
end

With this helper in place, we can update our Block form (views/blocks/_form.html.slim) to look like this:

= simple_form_for(@block, :html => {:class => 'form-horizontal' }) do |f|

  .form-inputs
    = f.hidden_field :block_type
    = f.input :title, placeholder: 'Insert a title'
    = render_dynamic_form_fields(f, @block)

  .form-actions
    = f.button :submit, class: 'btn btn-primary'
    '
    = link_to 'Back', blocks_path, class: 'btn'

Our new helper will build input fields for our custom fields using their datatype (defined in the block YAML) to figure out what kind of field it is.

Views

Finally, to show our new widget, we use a custom template that knows about our custom fields. For this example, I put the following slim template in views/blocks/_rock_block.slim

dl
  dt Title
  dd= @block.title
  dt Rock
  dd= @block.rock
  dt Roll 
  dd= @block.roll

And our corresponding show template simply renders that partial (using a model method that derives the template name from the block type)

= render @block.block_type.tableize.singularize

Tada

That pretty much does it. We’ve got custom blocks which we can define easily with a simple YAML file. They can have custom fields. They all sit in the same database table (kind of like STI). We have a single controller and single edit form with some smarts to draw the custom fields. And each block gets a custom template to render itself which knows about the custom fields.

If you want to try it out, most of the code here was taken from my sample app, custo_blocks, available on github.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s