Custom Form Builders Are Evil

Every Formbuilder i see does the same, building some rows/tables/div structure around the fields it is be called on.

f.text_field => tr td label input

In theory this sounds great, but as soon as one tries to make some real life use of it it breaks down:

  • How do i handle I18N ?
  • How can i change the label text from the default
  • How do i put the checkbox in front of the label
  • How can i output only a field/2 fields in one row

Form builders that always output label+input are unsuited for everyday development!

So what else can we do ?

#app/helpers/application_helper.rb
class ActionView::Helpers::FormBuilder
  def build_translated_label(content)
    if content.class == Array
      label(content[0],content[1])#label + text
    elsif content.class == String
      label('',content)#text
    else content.class == Symbol
      #uses gettext translation, feel free to insert your own...
      label(content,_("#{object.class}|#{content.to_s.capitalize.gsub('_',' ')}"))#label + translation        
    end
  end
  
  def row(label,content)
    @template.content_tag('div',build_translated_label(label) + content.to_s,:class=>'row')
  end
end

Usage

f.row(_('Movie|Website')+'http://', f.text_field(:website))
f.row(:tags, f.text_field(:tag_list)+_('comma divided'))

If someone wants the R-specs, drop me a mail 🙂

Small Spec Helpers

So small but so powerful, i just wanted to share my little time savers, and hope you share your most essential helpers in the comments.

(I use @item as an alias for the current object in all my tests, to make generic tests less painful.)

Small example:

it "renders feedback" do
  expects_find
  get :feedback , :id => @item.to_param
  response.should render_template('edit')
end

Code:

#spec/spec_helper.rb
def mock_create(item,success,para=nil)
  if para
    item.class.expects(:new).with(para).returns item
  else
    item.class.expects(:new).returns item
  end
  item.expects(:save).returns success
end

def mock_update(item,success)
  expects_find(item)
  item.expects(:save).returns success
end

def expects_find(item=nil)
  item ||= @item
  item.class.expects(:find).with(item.to_param).returns(item)
end

The Universal email_token

Activation_code, password_reminder_token and more to come.

Strange code:

before_create :make_activation_token

def make_activation_code
  self.activation_code = Digest::SHA1.hexdigest( ...)
end

def forgot_password
  ...
  password_reset_token=Digest::SHA1.hexdigest(...)
end
#and so on...

Let the mailman handle the postbox keys
Stay with a simple email token. It is updated every time we send any activation/reset/verification mail so no user can perform two action with the same token or say ‘find’ an old token and then request a password reset.

#user.rb
def update_email_token
  update_attribute(:email_token,Digest::SHA1.hexdigest(..)
end

#user_mailer.rb
def setup_email(user)
  user.update_email_token
  ...
end

Generic + Smart – link_to_s link_to_edit link_to_destroy

Building on my work in Separate Rights Management from Controllers i killed all linking-view-logic.

Example

before:
(movie.online? or movie.owner_id==current_user.id) ?
  link_to(movie,movie) : ''
(movie.owner_id==current_user.id) ?
  link_to('edit',movie) : ''

after:
link_to_s(movie)
link_to_edit(movie)

Code

#app/helpers/link_helper.rb
module LinkHelper
  def is_record?(something)
    something.kind_of?(ActiveRecord::Base)
  end

  def can_write?(obj)
    user = current_user || User.new
    user.can_write?(obj)
  end

  def can_read?(obj)
    user = current_user || User.new
    user.can_read?(obj)
  end

  #can we build a link ?
  #true if it is an id / path
  #false if it is an record and i cannot reda/write it
  def is_linkable?(object,rw)
    raise ":r or :w" unless [:r,:w].include? rw
    return false unless object
    return true unless is_record?(object)
    return rw == :r ? can_read?(object) : can_write?(object)
  end

  def link_to_s(object,options={})
    return "" unless is_linkable?(object,:r)
    link_to(object.to_s,object,options)
  end

  def link_to_edit(path_or_object,options={})
    return "" unless is_linkable?(path_or_object,:w)
    if is_record?(path_or_object)
      path_or_object = edit_polymorphic_path(path_or_object)
    end
    link_to("edit",path_or_object,options)
  end

  def link_to_destroy(path_or_object,options={})
    return "" unless is_linkable?(path_or_object,:w)
    link_to( 'X', path_or_object, options.merge(:class=>'destroy'))
  end
end

Please drop a comment if you can think of enhancements, if i can find more uses/methods i will build a new link-helper-plugin 🙂

XHR Redirects

All my ‘Ajax’ requests, use simple xhr when they want to load html, and not .js ,which imo is a misuse since requesting .js should yield a json response.

The first thing i always do is define a xhr layout, which usually is nil. Who ever thought that any xhr request should ever respond with stylesheets and javascript?

layout proc {|controller| controller.request.xhr? ? nil :  'application' }

Working this way i came to some limitations of the xhr approach:

  • cannot easily request from the browser
  • cannot redirect

Redirect an xhr ?

When a user requests something in a Lightbox, it is render using xhr. When the requested resource is not accessible by this user, he should see a login screen. This can be done with a normal redirect, which the JS Framework(at least jQuery does) uses to render the login page inside the lightbox.

Sounds good? But does not work…

Redirects will have the default layout, since they are no longer xhr request, which messes up the lightbox.

Xhr redirects

With this xhr redirect plugin the problem should go away.

_xhr=1

The plugin uses an added parameter _xhr to signal if a request is xhr or not, so now one can also request a page with _xhr=1 and see the actual xhr response.