Thursday, January 31, 2008

Generator: Stage

Update: Stage has become a gem and project details can be found at stonean.com

This post was updated on 04-27-08 to reflect the improved design. I'm using the RubyOnRails methods as this is where Stage was born, but Stage is available for Merb as well.

I've just completed a new generator called stage. This is a direct copy/modification of the scaffold generator included with Rails 2.0.2.

Stage was born out of a desire to remove as much code as possible from the view. I didn't want to create a new markup, I wanted to use the existing architecture more efficiently.

To use Stage, you first need to grab it:
sudo gem install stage

Then you use stage just like scaffold:
./script/generate stage author first_name:string last_name:string
exists app/models/
exists app/controllers/
exists app/helpers/
create app/views/authors
exists app/views/layouts/
create app/views/data/
create app/views/authors/index.html.erb
create app/views/authors/show.html.erb
create app/views/authors/new.html.erb
create app/views/authors/edit.html.erb
create app/views/authors/_form.html.erb
create app/views/authors/_data.html.erb
dependency model
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/author.rb
create test/unit/author_test.rb
create test/fixtures/authors.yml
create db/migrate
create db/migrate/001_create_authors.rb
create app/controllers/authors_controller.rb
create app/helpers/authors_helper.rb
route map.resources :authors


So, what's different? With the exception of the index view, the contents have changed quite a bit.

Edit:
<h1>Editing Author</h1>

<%= render :partial => "form" %>

<%= link_to 'Show', @author %> |
<%= link_to 'Back', authors_path %>

New:
<h1>New Author</h1>

<%= render :partial => "form" %>

<%= link_to 'Back', authors_path %>

Show:
<%= render :partial => "data" %>
<%= link_to 'Edit', edit_author_path(@author) %> |
<%= link_to 'Back', authors_path %>


And theres a new partial called form. As you can see this is called by edit and new:
<%
submit_label = "Update"
submit_label = "Create" if @author.new_record?
-%>

<%= error_messages_for :author %>

<% form_for(@author) do |f| %>
<%= render :partial => "data" %>
<p> <%= f.submit submit_label %> </p>
<% end %>

We've now removed the duplication in new, edit and show by having them all reference the data partial.

I've had this construct for a while, but the data partial always ended up with too much code. If I'm in edit mode, show the input tag, else show the text, etc..., etc...

It could get rather ugly. So, I decided to really start using the helpers. I have used them before when there would have been "too much" code in the view. I guess I've changed my definition of "too much".

Here's the data partial:

<p>
<b>First name</b><br/>
<%= author_first_name_value %>
</p>
<p>
<b>Last name</b><br/>
<%= author_last_name_value %>
</p>

As you can see I am calling a <model>_<attribute>_value method which will give me back the correct form or show value. This construct is used to avoid naming collisions. I had initially designed it without namespacing the methods with the model name, but quickly discovered that was a bad idea.


The @action_name instance variable determines how I'm to render the information. Luckily both Merb and Rails have this same variable.

These methods are defined in the Helper:

module AuthorsHelper
def author_first_name_value
if @action_name == "show"
h @author.first_name
else
text_field_tag "author[first_name]", @author.first_name
end
end

def author_last_name_value
if @action_name == "show"
h @author.last_name
else
text_field_tag "author[last_name]", @author.last_name
end
end
end

I opted on creating a method for each attribute and a clearly outlined "if" block to make modifications easier.

I hope you find this useful and it fits your idea of view structure.

Btw, thanks to Greg Houston for his source formatting tool. It made this post look a lot better than it would have without it.

4 comments:

Donald H. said...

cool.

Jamie Macey said...

This is similar to what I'm doing in my own projects. The main differences are that I name _data.html.erb _author.html.erb, in such a way that it can be used as a partial in other actions (like Article). Of course, that precludes using your "smart" helpers that will just wind up being a maintenance pain as the data model evolves over time.

Jamie Macey said...

It occurs to me that meta-defining the helper methods would go a long way to making it more maintainable, without a significant runtime penalty as one might get with a method_missing hook. Apologies for the crappy formatting, blogger's a bit draconic about pre/code tags.

module AuthorsHelper
%w(first_name last_name).each do |field|
eval <<-DEF
def author_#{field}_value
if @action_name == "show"
h @author.#{field}
else
text_field_tag "author[#{field}]", @author.#{field}
end
end
DEF
end
end

Andrew Stone said...

Thanks for the comments Jamie. I don't think having _data as opposed to _author should prevent any code sharing. You would need to qualify the paths anyway. Am I missing something there?

Other people have mentioned method_missing and constructs similar to your array usage. It's cool and eliminates a bunch of repetitive code, but I'm just not a fan of this sort of code architecture. As soon as one of the fields differ (password, size, style, etc..) you have to do more work to make a change than you would with the current construct.

Basically this construct is there with the assumption that something is going to change. So, I think it's more maintainable this way...interesting how we have different takes on maintainability.

However, it's just not mine anymore now is it? :)

If others express the same wants I don't see why I couldn't add a flag in to the generator to provide different templating formats.

I know it needs to be smarter than it is now and will be working on that in the near future.

Thanks again for the comments. They are very much appreciated :)