Ik was vanavond aan het stoeien met een complex formulier voor een applicatie waarmee ik bezig ben voor mijn werk. Ik moest aan een ‘post’ meerdere code snippets, tags en bestanden toevoegen. Dit deed ik op de manier uit Railscast afleveringen #73, #74 en #75. Dit werkte in het begin erg goed, maar ik kwam er al gauw achter dat er een aantal problemen waren. Als ik bijvoorbeeld nieuwe bestanden, tags of code snippets wilde toevoegen in de edit actie, gebeurde er allemaal rare dingen. Sommige velden werden gewoon genegeerd, waardoor ze uit de database werden verwijderd. Bestanden die werden geüpload waren 0kb in grootte, wat natuurlijk niet goed is. Ik werd er helemaal gek van en ben maar gaan zoeken naar mogelijke oplossingen. Ik stuitte op een geweldige oplossing in de vorm van een plugin: acts_as_virtual_attribute.

Ik installeerde de plugin en verving al mijn code met 1 regel: acts_as_virtual_attribute. Tot mijn verbazing werkte dit helemaal perfect. Alle problemen waren verdwenen als sneeuw voor de zon! De plugin heeft ervoor gezorgd dat ik deze avond niet uren heb moeten debuggen en daar ben ik de maker ervan heel dankbaar voor. Hiernaast is heel veel code verdwenen, waardoor alles een stuk overzichtelijker is. Als bedankje aan de maker zal ik een post wijden aan het gebruik van deze plugin.

UPDATE: Blijkbaar is er sinds Rails 2.3.0 een functie binnen Rails die alles doet wat deze plugin ook doet. Deze functie heet accepts_nested_attributes_for en stop je ook gewoon in de model. Het werkte wel aardig, maar toen ik bestanden ermee probeerde te uploaden had ik weer hetzelfde probleem als wat ik eerst had. Alle bestanden waren 0kb bestanden, waardoor ik toch maar ben gebleven bij deze plugin. Het werkt gewoon goed en je kunt het ook gemakkelijker de code aanpassen. Met dat ‘accepts_nested_attributes_for’ gaat alles via het Rails framework, waardoor je niet al te veel kan customizen. Als je er toch meer over wilt lezen raad ik je aan het volgende artikel te lezen: http://ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes.

Ik ga als voorbeeld mijn applicatie gebruiken. Ik ben namelijk bezig met een interne applicatie voor 45north waarin alle programmeurs code kunnen delen in een soort community. Je kunt snippets plaatsen, je code laten verbeteren, vragen stellen, plugins posten en nog veel meer. De meeste van deze onderdelen worden in de ‘posts’ tabel opgeslagen en worden van elkaar onderscheiden met een category_id. Een post kan ’snippets’, ‘tags’ en ‘assets’ aan zich gekoppeld hebben. Dit is een has_many relatie en in elk formulier moet je dus meerdere snippets, tags en assets kunnen toevoegen. Een behoorlijk complex formulier dus, maar gelukkig is er een simpele oplossing.

Het eerste wat je moet doen is de plugin installeren. Ga in de Terminal naar de root van jouw applicatie en voer het volgende commando uit.

script/plugin install git://github.com/sixty4bit/acts_as_virtual_attribute.git

De plugin is nu geïnstalleerd en we kunnen gaan beginnen. De new, create, edit en update acties in de PostsController zijn volgens de standaard CRUD en hebben geen speciale dingen. Voor het geval je niet weet hoe ze eruit moeten zien laat ik ze hier even zien.

# /controllers/posts_controller.rb
class PostsController < ApplicationController
    def new
        @post = Post.new
    end

    def create
        @post = Post.new(params[:post])
        if @post.save
          flash[:notice] = "Post has been added."
          redirect_to posts_path(@post)
        else
          render :action => "new"
        end
    end

    def edit
        @post = Post.find(params[:id])
    end

    def update
        @post = Post.find(params[:id])
        if @post.update_attributes(params[:post])
          flash[:notice] = "Post has been updated."
          redirect_to post_path(@post)
        else
          render :action => "edit"
        end
    end
end

Redelijk logisch dus. Natuurlijk moet je wel in /config/routes.rb nog even ‘map.resources :posts’ stoppen om dit te laten werken. De views zijn ook redelijk gewoontjes.

# /views/posts/new.html.erb
<h2>New post</h2>
<div>
	<%= render :partial => "form" %>
</div>

# /views/posts/edit.html.erb
<h2>Edit post</h2>
<div>
	<%= render :partial => "form" %>
</div>

# /views/posts/_form.html.erb
<% form_for @post, :html => { :multipart => true } do |f| %>
    <%= f.error_messages %>
    <p>
        <%= f.label :title %><br />
        <%= f.text_field :title %>
    </p>
    <p>
        <%= f.label :body %><br />
        <%= f.text_area :body %>
    </p>
     <%= render :partial => "collection" %>
     <p><%= f.submit %></p>
<% end %>

Zoals je ziet renderen we een partial genaamd ‘collection’ in _form.html.erb. Hierin komt alles voor de extra velden die je wilt toevoegen. Laten we dit bestand toevoegen.

# /views/posts/_collection.html.erb
<div id="snippets">
	<%= render :partial => "snippet", :collection => @post.snippets %>
</div>
<p><%= add_object_link "New snippet", :id => "snippets", :partial => "snippet", :object => Snippet.new %></p>

<div id="tags">
	<%= render :partial => "tag", :collection => @post.tags %>
</div>
<p><%= add_object_link "New tag", :id => "tags", :partial => "tag", :object => Tag.new %></p>

<div id="assets">
	<%= render :partial => "asset", :collection => @post.assets %>
</div>
<p><%= add_object_link "New asset", :id => "assets", :partial => "asset", :object => Asset.new %></p>

We hebben een aantal <div>’jes waarin we een partial renderen. We geven een :collection mee om aan te geven wat er in de partials moet komen. Hiernaast zie je op een aantal plaatsen ‘add_object_link’ staan. Dit is een helper die ik zelf heb gemaakt die een Javascript link maakt om nieuwe velden toe te voegen. We geven het label mee, de :id van de <div>, de :partial naam en het :object wat we willen gebruiken. De helper zelf ziet er als volgt uit.

# /helpers/posts_helper.rb
def add_object_link(name, options={})
    link_to_function name do |page|
      page.insert_html :bottom, options[:id], :partial => options[:partial], :object => options[:object]
    end
end

Het is niet de perfecte helper, maar is goed genoeg voor nu en doet zijn ding. Het volgende wat we moeten doen is die drie partials aanmaken. We hebben dus ’snippet’, ‘tag’ en ‘asset’.

# /views/posts/_snippet.html.erb
<div class="snippet">
<% fields_for_snippet snippet do |snippet_form| %>
	<%= snippet_form.text_field :title %><br />
	<%= snippet_form.text_area :body %><br />
	<%= snippet_form.file_field :file %><br />
	<%= link_to_remove_snippet "remove", ".snippet", snippet_form %>
<% end %>
</div>

# /views/posts/_tag.html.erb
<div class="tag">
<% fields_for_tag tag do |tag_form| %>
	<%= tag_form.text_field :tag %>
	<%= link_to_remove_tag "remove", ".tag", tag_form %>
<% end %>
</div>

# /views/posts/_asset.html.erb
<div class="asset">
<% fields_for_asset asset do |asset_form| %>
	<%= asset_form.file_field :file %>
	<%= link_to_remove_asset "remove", ".asset", asset_form %>
<% end %>
</div>

Dit zijn dus de bestanden waarin de extra velden zitten. We hebben text_area’s, text_fields en file_fields. Het uploaden van bestanden gebeurt via de desbetreffende models en gaan we hier niet behandelen. Je ziet een aantal helpers die worden gebruikt in de partials. Zo worden de fields_for en link_to_remove helpers die je ziet gemaakt door de plugin. Hier hoef je jezelf dus geen zorgen over te maken. Wat ons alleen nog resteert is de code in de models stoppen.

# /models/post.rb
class Post < ActiveRecord::Base
  has_many :tags, :dependent => :destroy
  has_many :assets, :dependent => :destroy
  has_many :snippets, :dependent => :destroy

  acts_as_virtual_attribute :snippets
  acts_as_virtual_attribute :assets
  acts_as_virtual_attribute :tags
end

# /models/snippet.rb
class Snippet < ActiveRecord::Base
  belongs_to :post
end

# /models/tag.rb
class Tag < ActiveRecord::Base
  belongs_to :post
end

# /models/asset.rb
class Asset < ActiveRecord::Base
  belongs_to :post
end

En dat is alles wat je in de models moet stoppen. Helaas kan je niet alle attributen in één acts_as_virtual_attribute stoppen. Misschien handig voor een toekomstige update.

Als je dus nu naar het formulier gaat kun je op 'Add snippet' klikken om de velden uit het '_snippet.html.erb' bestand te halen. Als je op 'Add tag' klikt krijg je alle velden uit '_tag.html.erb' en als je op 'Add asset' klikt uit '_asset.html.erb'. Als je hierna op submit klikt wordt alles opgeslagen en worden alle relaties automatisch voor je gelegd. Alles gaat via de models en via de plugin, waardoor je een stuk minder code hoeft te schrijven. Ook als je naar het edit formulier gaat en bepaalde velden verwijderd, worden ze ook echt uit de database verwijderd als je op de submit knop klikt. Handig toch?

En dat was het dan. Je hebt nu een complex formulier gemaakt! Hopelijk heb je wat aan deze tutorial gehad en kun je de plugin goed gebruiken in een toekomstig project. Voor vragen kun je altijd terecht in de comments.