Ruby on Rails

Rails Helpers: Dottify en Core extensions

Nadat ik de dutch_date_select helper voor form_for had gepost moest ik gewoon uitzoeken hoe je extra methods aan bijvoorbeeld de String class kunt toevoegen. Ik dacht meteen aan een helper die ik een keer had gemaakt om dit uit te testen: Dottify. Deze helper zorgt ervoor dat je een String kunt afknippen en puntjes erachter kunt zetten als het nodig is. Deze zag er zo uit:

def dottify(text, len)
    "#{text[0..len-1]}#{"..." if text.length > len}"
end

Je geeft dus de string en de lengte mee om er wel of niet puntjes achter te zetten. Het leek mij een stuk handiger als je dit direct op een String kunt aanroepen. Ik ben gaan zoeken hoe dit moet en het bleek behoorlijk simpel te zijn.

Het eerste wat je moet doen is een bestand aanmaken in de /lib map. Noem deze ‘core_extensions.rb’. Hierin stop je het volgende:

class String
  def dottify(len)
    "#{self[0..len-1]}#{"..." if self.length > len}"
  end
end

Je doet dus gewoon ‘class String’ en kunt hierin allerlei methods toevoegen. Je spreekt de String zelf steeds aan met ’self’. Je kunt dit dus ook met Integer e.d. doen, maar ook met Rails classes zoals die ik gebruikte in dutch_date_select. Je moet echter nog wel iets doen om dit te laten werken. Maak een bestand aan in /config/initializers en noem deze ‘load_extensions.rb’. Stop hier het volgende in:

require 'core_extensions'

Nu wordt elke keer als de server wordt opgestart ‘core_extensions.rb’ ingeladen. Let dus op: elke keer als je een aanpassing doet moet je de server opnieuw opstarten. Je kunt in ‘core_extensions.rb’ alle aanpassingen aan de verschillende classes doen. Je kunt er ook voor kiezen om dit op te delen in meerdere bestanden. Vergeet deze dan niet toe te voegen aan ‘load_extensions.rb’, zodat ze daadwerkelijk worden ingeladen.

Vrij simpel toch? Om nu Dottify te gebruiken doe je het volgende:

"Dit is een String".dottify(6)   # => "Dit is..."

Hopelijk hebben jullie er wat aan. :)

Rails Helpers: Griddify

Ik had weer zin om een Rails helper te posten. Dit keer kom ik met ‘Griddify’. Dit is een helper waarmee je gemakkelijk een aantal div’jes als grid kunt aanmaken. Er worden een aantal classes en id’s gekoppeld aan de aangemaakte div’jes, zodat je het gemakkelijk mooi kunt maken met CSS. Dit is dan de helper.

def griddify(array, cols, options = {})
	array = array.in_groups_of(cols, options[:fill_with])
	options[:rows] ||= array.length
	options[:row_id] ||= "gridRow"
	options[:row_class] ||= "gridRow"
	options[:column_id] ||= "gridColumn"
	options[:column_class] ||= "gridColumn"

	if options[:rows] > array.length && options[:fill_with] != false
		rows_array = []
 		((options[:rows] - array.length) * cols).times { rows_array.push(options[:fill_with]) }
    	array.concat(rows_array.in_groups_of(cols))
	elsif options[:rows] < array.length
   	(array.length - options[:rows]).times { array.slice!(-1) }
	end

	array.each_with_index do |row, row_index|
		concat "<div class=\"#{options[:row_class]}\" id=\"#{options[:row_id]}_#{row_index}\">"
		row.each_with_index do |column, column_index|
			concat "<div class=\"#{options[:column_class]}\" id=\"#{options[:column_id]}_#{row_index * cols + column_index}\">"
			yield column if block_given? && !column.blank?
			concat "</div>"
		end
		concat "</div>"
	end
end

Behoorlijk uitgebreid dus. Je hoeft niet per se te weten hoe het werkt, maar wel hoe je het kunt gebruiken. Gelukkig is dat behoorlijk gemakkelijk. Als we alle blogposts in de variabele @posts hebben gestopt en we willen alles laten zien in rijen van twee kolommen, dan doe je het volgende.

<% griddify(@posts, 2) do |post| %>
	<%= post.title %>
<% end %>

Er worden div'jes aangemaakt die je zo kunt stijlen dat het er als een grid uitziet. Wil je ook een limiet op het aantal rijen, dan geef je simpelweg een extra parameter mee.

<% griddify(@posts, 2, :rows => 4) do |post| %>
	<%= post.title %>
<% end %>

Nu komen er dus maximaal acht blogposts in het grid. Als er minder dan acht blogposts in de database zitten worden de div'jes toch aangemaakt, zodat het grid er blijft. Naast de :rows kun je ook andere parameters meesturen. Zo kun je elke rij een andere :row_id of :row_class meegeven en elke kolom een andere :column_id of :column_class. Deze staan standaard op 'gridRow' en 'gridColumn'.

Om te illustreren wat er gebeurt als je griddify uitvoert laat ik hier een voorbeeld zien.

<% griddify(@posts, 2, :rows => 2) do |post| %>
	<%= post.title %>
<% end %>

Dit geeft:

<div id="gridRow_1" class="gridRow">
	<div class="gridColumn" id="gridColumn_0">
		Hello world!
	</div>
	<div class="gridColumn" id="gridColumn_1">
		Once upon a time
	</div>
</div>
<div id="gridRow_2" class="gridRow">
	<div class="gridColumn" id="gridColumn_2">
		Pretty neat
	</div>
	<div class="gridColumn" id="gridColumn_3">
		Griddify is awesome!
	</div>
</div>

Veel plezier met deze helper. Het had mij een hoop tijd gekost als ik dit had gemaakt voor de website voor het Nederlands Dans Theater. Hopelijk voorkomt dit voor jullie een hoop overbodig werk.

Rails Helpers: f.dutch_date_select

Gisteren heb ik laten zien hoe je een dutch_date_select helper kunt maken. Het nadeel is echter dat je het niet met form_for kunt gebruiken. Ik heb de helper daarom aangepast zodat deze werkt in form_for. Stop het volgende in de ApplicationHelper.

module ApplicationHelper
  class DutchBuilder < ActionView::Helpers::FormBuilder
    def dutch_date_select(method, *args)
      options = args.extract_options!
      options[:order] = [:day, :month, :year]
      options[:use_month_names] = ["januari", "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober", "november", "december"]
      @template.date_select(@object_name, method, options)
    end
  end
end

We maken een eigen FormBuilder class aan die alles van ActionView::Helpers::FormBuilder erft. We stoppen @template ervoor en geven @object_name als object naam mee. Dit is de manier hoe al deze form_for velden worden gemaakt. Nu kunnen we het bijna in een form_for gebruiken. Je moet eerst nog wat toevoegen aan je form_for.

<% form_for @user, :builder => ApplicationHelper::DutchBuilder do |f| %>
...
<% end %>

We geven nu dus aan dat we de DutchBuilder willen gebruiken. Je kunt er ook voor zorgen dat de helper direct in ActionView::Helpers::FormBuilder wordt gestopt, zodat je dit niet hoeft te doen. Hoe dit moet kun je het beste zelf gaan uitzoeken. We kunnen nu de helper in de form_for aanroepen.

<% form_for @user, :builder => ApplicationHelper::DutchBuilder do |f| %>
    <%= f.dutch_date_select :birthday, :include_blank => true %>
<% end %>

Dat was dan de f.dutch_date_select helper. Veel plezier ermee.

EDIT (18 mei 2010 om 13:27)

Hier een verbeterde versie van de helper. Hij werkte namelijk niet goed met accepts_nested_attributes_for.

module ApplicationHelper
    class ActionView::Helpers::FormBuilder
	def dutch_date_select(method, options = {}, html_options = {})
            options[:order] = [:day, :month, :year]
	    options[:prompt] = {:day => 'dag', :month => 'maand', :year => 'jaar'}
            options[:use_month_names] = ["januari", "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober", "november", "december"]
            @template.date_select(@object_name, method, options.merge(:object => @object), html_options)
        end
    end
end

Rails Helpers: dutch_date_select

Ruby on Rails kent de handige helper ‘date_select’ die automatisch een aantal select tags aanmaakt om een datum te kiezen. Het probleem hiermee is dat het in het Engels is en dat de volgorde niet volgens de Nederlandse standaard is (DD-MM-YYYY). Ik heb daarom even snel een ‘dutch_date_select’ helper geschreven. Let op: gebruik alleen deze helper als je zeker weet dat je alleen Nederlandse gebruikers zult krijgen. Als de website in meerdere talen te bekijken is kun je veel beter de lokalisatie bestanden aanpassen.

def dutch_date_select(object_name, method, *args)
    options = args.extract_options!
    options[:order] = [:day, :month, :year]
    options[:use_month_names] = ["januari", "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober", "november", "december"]
    date_select(object_name, method, options)
end

Behoorlijk gemakkelijke helper dus. Als je een User model hebt met een kolom genaamd ‘birthday’, dan doe je het volgende.

<%= dutch_date_select :user, :birthday %>

Je kunt overigens alle andere parameters meesturen die je bij date_select kunt gebruiken. Bijvoorbeeld :include_blank.

<%= dutch_date_select :user, :birthday, :include_blank => true %>

Dat was dan de dutch_date_select helper. Ik hoop dat je er wat aan hebt. Voor mij is het erg handig geweest bij een project voor 45north.

Rails Helpers: Menu items aanmaken

Ik vond dat het weer tijd was voor een nieuwe Rails helper. Dit keer een helper om een menu item te maken die automatisch een class ‘current’ meegeeft als de huidige URL overeenkomt met de meegestuurde URL. Het is niet de perfecte helper en zal niet voor alle situaties helpen, maar is zeker een goed beginpunt om te customizen. Ik heb de helper ‘menu_li’ genoemd.

def menu_li(label, url="/", *args)
   options = args.extract_options!
   current_page = request.request_uri
   current_check = (url == "/" ? current_page == "/" : current_page.match(url))
   options[:class] = current_check ? "current" : nil
   content_tag(:li, link_to(label, url, :title => label), options)
end

Je kunt nu dus het volgende doen om een menu item aan te maken.

<ul>
   <%= menu_li "Home" %>
   <%= menu_li "Posts", "/posts" %>
</ul>

Als je op de root van de website bent (“/”) dan krijg je de volgende HTML.

<ul>
   <li class="current"><a href="/" title="Home">Home</a></li>
   <li><a href="/posts" title="Posts">Posts</a></li>
</ul>

Als je dus in de index van de PostsController bent krijgt de tweede <li> een class ‘current’. Je kunt ook extra argumenten meegeven, waaronder een extra class.

<ul>
   <%= menu_li "Home" %>
   <%= menu_li "Posts", "/posts", :class => "andere_class" %>
</ul>

Als je in /posts zit dan wordt de class echter niet toegevoegd aan de <li>. De ‘current’ class herschrijft dan de opgegeven class. Als je dit niet wil kun je deze aangepaste helper gebruiken.

def menu_li(label, url="/", *args)
   options = args.extract_options!
   current_page = request.request_uri
   current_check = (url == "/" ? current_page == "/" : current_page.match(url))
   options[:class] = options[:class].blank? ? (current_check ? "current" : nil) : (current_check ? "#{options[:class]} current" : options[:class])
   content_tag(:li, link_to(label, url, :title => label), options)
end

Veel plezier met deze helper. Volgende keer weer een andere helper, dus blijf me volgen.

Rails Helpers: Dynamische layout

Ik ben de laatste tijd erg druk geweest met school en werk. Ik ben hiernaast begonnen met een boek over Ruby on Rails 3.0, waardoor ik heel weinig tijd heb gehad om iets aan mijn blog te doen. Een collega van mij gaf me gisteren een goede tip om toch bezig te zijn met het boek en tegelijkertijd met mijn blog. Ik ga elke week een handige Ruby on Rails helper plaatsen op mijn blog om zo mijn boek alvast een beetje te promoten en toch bezig te zijn met mijn blog. Vandaag begin ik met de eerste Ruby on Rails helper. Ik zal proberen elke week minstens 1 helper te plaatsen en zo af en toe tijd te nemen om over andere dingen te schrijven.

Laat ik maar beginnen met mijn helper. Soms wil je vanuit de views bepalen wat er allemaal in de layout wordt getoond. Je wilt een dynamische layout. Je kunt hiervoor ‘content_for’ gebruiken die een string stuurt naar de layout die je kunt aanspreken met ‘yield’. Dit werkt als volgt.

# View
<% content_for(:title, "Dit is de title!") %>

# Layout
<head>
    <title><%= yield(:title) %></title>
</head>

Nu wordt er vanuit de view bepaald wat de titel van de pagina wordt. Erg handig natuurlijk. Het nadeel van content_for is dat er alleen een String kan worden meegegeven. Als je een Array meegeeft, dan wordt hier een String van gemaakt. Hier heb je natuurlijk weinig aan.

Wat je wel kan doen om een Array of Hash mee te sturen is een instance variabele aanmaken in de view en deze aanspreken in de layout. De view die bij de corresponderende actie hoort wordt namelijk eerst gerendert. De layout wordt pas erna gemaakt, waardoor je een instance variabele die in de view is gemaakt kunt aanspreken in de layout. Een voorbeeld.

# View
<% @menu = { :home => true, :products => true } %>

# Layout
<%= link_to "Home", "/" if @menu[:home] %>
<%= link_to "Products", "/products" if @menu[:products] %>

Dit is gelijk een stuk flexibeler. We kunnen dit echter een stuk handiger maken en ervoor zorgen dat je vanuit je view precies kan bepalen welke blokken er in de layout worden getoond. We gaan een helper maken genaamd ‘layout_render’. Hier willen we woorden aan kunnen meegeven die aangeven wat er mag worden getoond in de layout.

# ApplicationHelper
def layout_render *args
    @layout_render = args
end

We kunnen nu meerdere argumenten naar de helper sturen en de helper stopt al deze argumenten in een Array. Je zou nu het volgende in de view kunnen doen.

# De view
<% layout_render :intro, :header, :menu %>

Nu geef je dus aan in de view dat je de intro, header en menu in de layout wilt laten zien. We hebben nog een helper nodig die checkt of iets mag worden getoond in de layout.

# ApplicationHelper
def render?(option)
    @layout_render ||= []
    @layout_render.include?(option)
end

Je kunt nu in de layout het volgende doen om te kijken of de header mag worden getoond.

# Layout
<% if render?(:header) %>
    <h1>De header van de pagina</h1>
<% end %>

Als je nu dus uit de layout_render functie in de view de :header weghaalt, dan wordt de header niet getoond. Handig toch?

Ik zou mezelf kunnen voorstellen dat je ook iets als :all wilt kunnen meesturen aan de layout_render functie. Dus als je :all meegeeft, dat alle onderdelen in de layout worden getoond en dat je dus niet steeds alles moet intypen. Om dit voor elkaar te krijgen doe je een kleine aanpassing in de ‘render?’ helper.

# ApplicationHelper
def render?(option)
    @layout_render ||= []
    @layout_render.include?(option) || @layout_render.include?(:all)
end

Dus als je nu :all meegeeft, wordt gewoon alles getoond. Als je nou ook wilt dat als je layout_render in de view niet aanroept toch alles wordt getoond in de layout, dan voeg je het volgende toe aan de ‘render?’ helper.

# ApplicationHelper
def render?(option)
    @layout_render ||= [:all]
    @layout_render.include?(option) || @layout_render.include?(:all)
end

Nu wordt dus i.p.v. een lege Array een Array gemaakt met :all er al in.

Ik hoop dat jullie wat aan deze helpers hebben. Ik heb er zeker wat aan gehad bij een project voor 45north. Volgende keer dus een nieuwe helper. Tot de volgende keer!

Make_resourceful en namespaces

Welkom bij het tweede deel in de wekelijkse reeks van presentaties die bij 45north worden gegeven. Deze week ga ik het hebben over de Ruby on Rails plugin ‘make_resourceful‘ en zal ik in het kort de zogenaamde ‘namespaces’ binnen Rails behandelen. Ik heb hier samen met mijn collega Daniël Zwijnenburg een presentatie over gehouden bij 45north. We hebben uitgelegd wat make_resourceful is, waarom je het moet gebruiken en wat voor kanttekeningen er zijn. Hiernaast hebben we een hoop voorbeelden gegeven en hebben we in het kort verteld wat namespaces zijn. Ik zal dezelfde volgorde aanhouden voor deze blogpost.

Meer >

Complexe formulieren met acts_as_virtual_attribute

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.

Meer >

REST en Ruby on Rails

Bij 45north zijn we sinds kort begonnen om elke week opgedane kennis met elkaar te delen door korte presentaties te geven aan alle aanwezige collega’s. Deze presentaties gaan vooral over het gebruik van Ruby on Rails, maar kunnen ook over dingen als HTML, CSS en Actionscript gaan. Ik ben van plan om elke week samen met iemand anders een presentatie voor te bereiden en te geven. Nu leek het mij wel leuk om al deze presentaties op mijn blog te stoppen in de vorm van een post, zodat iedereen het nog een keer kan nalezen en mensen die niet bij 45north werken toch nog kunnen deelnemen aan deze presentaties.

Vorige week heb ik samen met mijn collega Mark van de Korput een presentatie gegeven over REST binnen Ruby on Rails. We hebben uitgelegd hoe het REST principe precies werkt en hoe je het kunt gebruiken binnen Ruby on Rails. In deze post zal ik alles wat we in de presentatie hebben verteld nog eens haarfijn uitleggen.

Meer >

Handige Ruby helpers

Ik heb toenet even snel een aantal helpers geschreven die je kunt gebruiken in je Ruby on Rails applicaties. Je kunt ze in de normale helpers stoppen, maar natuurlijk ook in de ApplicationController (je kunt ze dan aanspreken vanuit de views door er een helper_method van te maken). Download het bestand via de volgende link: functions.rb.

# Als de naam van de gebruiker in de 'users' tabel is opgedeeld in drieën is het handig
# om een functie te hebben die deze drie kolommen samenvoegt. Stuur het object van de
# gebruiker naar 'get_username' en je krijgt de naam van de gebruiker terug.
def get_username(user)
  return "#{user.first_name} #{user.last_name}" if user.preposition.blank?
  return "#{user.first_name} #{user.preposition} #{user.last_name}" unless user.preposition.blank?
end

# Soms wil je weten of een bepaald bestand bestaat. Dit kun je doen met 'File.exist?'.
# Het is echter misschien iets handiger om hier een simpele helper voor te hebben.
def file?(path)
  File.exist?(path)
end

# Wil je een string van een aantal willekeurige cijfers? Dan kun je natuurlijk zelf een functie schrijven.
# Ik gebruik de volgende functie om een aantal willekeurige cijfers te krijgen. Erg gemakkelijk.
def random_numbers(len)
  random = ""
  len.times { random += rand(10).to_s }
  return random
end

# En dan hier een snelle functie om een random string te maken (met letters en cijfers).
def random_string(len)
  return Array.new(len/2) { rand(256) }.pack('C*').unpack('H*').first
end

# Een hele simpele functie om een tekst een SHA1 hash te geven.
def sha1_hash(text)
  return Digest::SHA1.hexdigest(text)
end

# Een hele simpele functie om een tekst een MD5 hash te geven.
def md5_hash(text)
  return Digest::MD5.hexdigest(text)
end

En dat waren dan de helpers. Heb jij een leuke helper die je wilt delen? Laat het even achter in de comments. Verbeteringen voor deze helpers? Dan kun je dat natuurlijk ook in de comments kwijt.