Rails 5.1 Consulting und Schulung vom Autor:
www.wintermeyer-consulting.de/rails/

3.3. Dynamisch mit erb erzeugtes HTML

Kennen Sie PHP (ohne Frameworks)? Dann wird Ihnen der Inhalt einer erb-Datei sehr bekannt vorkommen. Es ist eine Mischung aus (beispielsweise) HTML und Ruby-Code. (erb steht für embedded Ruby, also eingebettetes Ruby.) Allerdings können wir eine solche erb-Webseite nicht einfach in das Verzeichnis public legen, da dort abgelegte Seiten 1:1 ausgeliefert werden und nicht durch einen erb-Parser gehen. Dummerweise müssen wir dafür jetzt direkt mit dem MVC-Modell [20]anrücken. Wir brauchen einen Controller. Den können wir mit dem Befehl rails generate controller anlegen. Schauen wir uns mal die Hilfe an:
MacBook:testproject xyz$ rails generate controller
Usage:
  rails generate controller NAME [action action] [options]

Options:
      [--skip-namespace]        # Skip namespace (affects only isolated applications)
      [--old-style-hash]        # Force using old style hash (:foo => 'bar') on Ruby >= 1.9
  -e, [--template-engine=NAME]  # Template engine to be invoked
                                # Default: erb
  -t, [--test-framework=NAME]   # Test framework to be invoked
                                # Default: test_unit
      [--helper]                # Indicates when to generate helper
                                # Default: true
      [--assets]                # Indicates when to generate assets
                                # Default: true

Runtime options:
  -f, [--force]    # Overwrite files that already exist
  -p, [--pretend]  # Run but do not make any changes
  -q, [--quiet]    # Supress status output
  -s, [--skip]     # Skip files that already exist

Description:
    Stubs out a new controller and its views. Pass the controller name, either
    CamelCased or under_scored, and a list of views as arguments.

    To create a controller within a module, specify the controller name as a
    path like 'parent_module/controller_name'.

    This generates a controller class in app/controllers and invokes helper,
    template engine and test framework generators.

Example:
    `rails generate controller CreditCard open debit credit close`

    Credit card controller with URLs like /credit_card/debit.
        Controller:      app/controllers/credit_card_controller.rb
        Functional Test: test/functional/credit_card_controller_test.rb
        Views:           app/views/credit_card/debit.html.erb [...]
        Helper:          app/helpers/credit_card_helper.rb
MacBook:testproject xyz$
Aha! Unten ist freundlicherweise direkt ein Beispiel angegeben:
rails generate controller CreditCard open debit credit close
Passt aber nicht direkt für unseren Fall.
Ich bin mutig und schlage vor, dass wir einfach Folgendes ausprobieren:
MacBook:testproject xyz$ rails generate controller Example test
      create  app/controllers/example_controller.rb
       route  get "example/test"
      invoke  erb
      create    app/views/example
      create    app/views/example/test.html.erb
      invoke  test_unit
      create    test/functional/example_controller_test.rb
      invoke  helper
      create    app/helpers/example_helper.rb
      invoke    test_unit
      create      test/unit/helpers/example_helper_test.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/example.js.coffee
      invoke    scss
      create      app/assets/stylesheets/example.css.scss
MacBook:testproject xyz$
Puhhh …, da wird ja direkt 'ne ganze Menge erstellt. Unter anderem eine Datei app/views/example/test.html.erb. Schauen wir uns diese nachfolgend an:
MacBook:testproject xyz$ cat app/views/example/test.html.erb 
<h1>Example#test</h1>
<p>Find me in app/views/example/test.html.erb</p>
MacBook:testproject xyz$
Ist HTML, aber für eine valide HTML-Seite fehlt oben und unten etwas (der fehlende HTML-"Rest" wird in „Layouts“ erklärt). Zum Testen starten wir den Webserver
MacBook:testproject xyz$ rails server
=> Booting WEBrick
=> Rails 3.2.3 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
[2012-04-24 10:13:48] INFO  WEBrick 1.3.1
[2012-04-24 10:13:48] INFO  ruby 1.9.3 (2012-04-20) [x86_64-darwin11.3.0]
[2012-04-24 10:13:48] INFO  WEBrick::HTTPServer#start: pid=57898 port=3000
und schauen uns die Webseite unter der URL http://0.0.0.0:3000/example/test im Browser an:
test.html.erb-Webseite
Im Log log/development.log finden wir den folgenden Eintrag:
Started GET "/example/test" for 127.0.0.1 at 2012-04-24 10:14:18 +0200
Processing by ExampleController#test as HTML
  Rendered example/test.html.erb within layouts/application (1.9ms)
Compiled example.css  (9ms)  (pid 57898)
Compiled application.css  (18ms)  (pid 57898)
Compiled jquery.js  (4ms)  (pid 57898)
Compiled jquery_ujs.js  (0ms)  (pid 57898)
Compiled example.js  (160ms)  (pid 57898)
Compiled application.js  (198ms)  (pid 57898)
Completed 200 OK in 305ms (Views: 304.2ms | ActiveRecord: 0.0ms)
Das ist fast schon verständlich geschrieben. Es kam also vom localhost (127.0.0.1) ein HTTP-GET-Request für die URI /example/test rein. Die wurde dann anscheinend vom Controller ExampleController mit der Methode test als HTML gerendert. Zusätzlich wurde noch ein Satz von CSS- und JavaScript-Dateien kompiliert (darauf gehen wir später im Buch ein). Das Ganze hat hier ungefähr 305 ms gedauert.
Jetzt müssen wir nur noch den Controller finden. Aber Sie haben Glück … ich weiß es nämlich. ;-) Alle Controller befinden sich im Verzeichnis app/controllers, und siehe da, dort ist auch tatsächlich die entsprechende Datei app/controllers/example_controller.rb.
MacBook:testproject xyz$ ls -l app/controllers/
total 16
-rw-r--r--  1 xyz  staff  80 Apr 24 09:43 application_controller.rb
-rw-r--r--  1 xyz  staff  69 Apr 24 10:11 example_controller.rb
MacBook:testproject xyz$
Öffnen Sie die Datei bitte mit Ihrem Lieblingseditor:
class ExampleController < ApplicationController
  def test
  end
end
Das ist sehr übersichtlich. Der Controller ExampleController stammt vom ApplicationController ab und enthält aktuell genau eine Methode namens test. Und diese Methode hat keinen Inhalt.
Sie werden sich fragen, woher Rails weiß, dass bei dem URL-Pfad /example/test der Controller ExampleController und die Methode test abzuarbeiten sind. Das wird nämlich nicht durch eine magische Logik, sondern durch eine einfache Routing-Konfiguration gesteuert. Diese finden Sie in der Datei config/routes.rb in der zweiten Zeile:
MacBook:testproject xyz$ cat config/routes.rb | grep example
  get "example/test"
MacBook:testproject xyz$ 
Diese Zeile wurde vom Befehl rails generate controller automatisch eingefügt. In der Routing-Datei können Sie auch beliebiges Mapping vornehmen. Aber dazu später mehr. Aktuell sehen unsere Routen sehr einfach aus. Mit dem Befehl rake routes können wir diese abfragen:
MacBook:testproject xyz$ rake routes
example_test GET /example/test(.:format) example#test
MacBook:testproject xyz$
Wir kümmern uns später noch genauer um die Routen (Kapitel 6, Routen (routes)). Ich wollte es an dieser Stelle nur nicht gänzlich überspringen.

Wichtig

Eine statische Datei im Verzeichnis public hat immer eine höhere Priorität als eine Route in der config/routes.rb! Wenn wir also eine statische Datei public/example/test abspeichern würden, würde die Route nicht mehr greifen.

Programmieren in einer erb-Datei

Erb-Seiten können Ruby-Code enthalten. Damit kann programmiert werden, und damit können diese Seiten dynamischen Inhalt bekommen.
Fangen wir mit etwas ganz Einfachem an: der Addition von 1 und 1. Als Erstes probieren wir den Code im irb aus:
MacBook:testproject xyz$ irb
1.9.3p194 :001 > 1 + 1
 => 2 
1.9.3p194 :002 > exit
MacBook:testproject xyz$
Das war einfach. Die erb-Datei app/views/example/test.html.erb füllen wir wie folgt:
<h1>Erste Versuche mit erb</h1>
<p>Addition:
<%= 1 + 1 %>
</p>
Danach mit rails server den Webserver starten (falls noch nicht getan) und per Browser auf die Seite gehen:
Einfache Addition
Ruby-Code, dessen Ergebnis ausgegeben werden soll, wird von einem <%= und einem %> eingeschlossen. Es können nur Strings ausgegeben werden.
Jetzt werden Sie sich vielleicht fragen: Wie kann denn das Ergebnis einer Addition von zwei Fixnums als Text angezeigt werden? Schauen wir erst mal im irb nach, ob es wirklich ein Fixnum ist:
MacBook:testproject xyz$ irb
1.9.3p194 :001 > 1.class
 => Fixnum 
1.9.3p194 :002 > (1 + 1).class
 => Fixnum 
1.9.3p194 :003 > exit
MacBook:testproject xyz$
Ja, sowohl die Zahl 1 also auch das Ergebnis von 1 + 1 ist ein Fixnum. Was ist passiert? Rails ist so intelligent, alle Objekte in einem View (das ist die Datei test.html.erb), die nicht bereits ein String sind, automatisch mit der Methode .to_s aufzurufen, welche per Konvention immer den Inhalt des Objektes in einen String konvertiert. Noch mal kurz ins irb:
MacBook:testproject xyz$ irb
1.9.3p194 :001 > (1 + 1)
 => 2 
1.9.3p194 :002 > (1 + 1).class
 => Fixnum 
1.9.3p194 :003 > (1 + 1).to_s
 => "2" 
1.9.3p194 :004 > (1 + 1).to_s.class
 => String 
1.9.3p194 :005 > exit
MacBook:testproject xyz$
Das mit dem Ruby-Code schauen wir uns jetzt genauer an. In einer .html.erb-Datei gibt es zusätzlich zu den HTML-Elementen zwei Arten von Ruby-Code-Anweisungen:
  • <% %>
    Führt den enthaltenen Ruby-Code aus, aber gibt nichts aus (außer Sie verwenden explizit so etwas wie print oder puts).
  • <%= %>
    Führt den enthaltenen Ruby-Code aus und gibt das Ergebnis als Text aus. Dabei werden seit Rails 3.0 automatisch bestimmte Zeichen escapt. Falls Sie einmal nicht escapten Text ausgeben möchten, so müssen Sie das mit raw(string) realisieren.
    Sofern also ein Objekt eine Methode .to_s hat oder das Objekt selber schon ein String ist, kann man es als Ergebnis im View innerhalb einer <%= %> Kapselung ausgeben.
Um ganz sicher zu sein, noch ein Beispiel. Wir ändern die app/views/example/test.html.erb wie folgt:
<p>Schleife von 0 bis 5:
<% (0..5).each do |i| %>
<%= "#{i}, " %>
<% end %>
</p>
Das sieht dann im Browser so aus:
Einfache Addition
Schauen wir uns nachfolgend den HTML-Source-Code (-Quelltext) im Browser an:
<!DOCTYPE html>
<html>
<head>
  <title>Testproject</title>
  <link href="/assets/application.css?body=1" media="all" rel="stylesheet" type="text/css" />
<link href="/assets/example.css?body=1" media="all" rel="stylesheet" type="text/css" />
  <script src="/assets/jquery.js?body=1" type="text/javascript"></script>
<script src="/assets/jquery_ujs.js?body=1" type="text/javascript"></script>
<script src="/assets/example.js?body=1" type="text/javascript"></script>
<script src="/assets/application.js?body=1" type="text/javascript"></script>
  <meta content="authenticity_token" name="csrf-param" />
<meta content="TDIallkBmGrMzsL6TC8Chet4r/X1yLK0tthFmIig4+E=" name="csrf-token" />
</head>
<body>

<p>Schleife von 0 bis 5:
0, 
1, 
2, 
3, 
4, 
5, 
</p>


</body>
</html>
Alles klar? Es gibt zwei mögliche offene Fragen:

F:

Ich verstehe gar nichts. Mit dem Ruby-Code komme ich nicht zurecht. Können Sie das noch mal erklären?

A:

Kann es sein, dass Sie Kapitel 2, Ruby-Grundlagen nicht komplett durchgearbeitet haben? Bitte nehmen Sie sich die Zeit dafür. Sonst macht hier das alles keinen Sinn.

F:

Ich verstehe den Ruby-Code und die HTML-Ausgabe. Allerdings verstehe ich nicht, warum drum herum noch HTML-Code gerendert wurde, den ich gar nicht geschrieben habe. Woher kommt der, und kann ich ihn beeinflussen?

A:

Sehr gute Frage! Dazu kommen wir sofort (siehe „Layouts“).
Die Feinheiten von erb werden Sie jetzt Stück für Stück erlernen. Es handelt sich dabei nicht um Zauberei.

Layouts

Die erb-Datei im Verzeichnis app/views/example/ bildet nur den Kern der späteren HTML-Seite. Per Default wird immer eine automatisch generierte app/views/layouts/application.html.erb drum herum gerendert. Schauen wir uns die mal an:
MacBook:testproject xyz$ cat app/views/layouts/application.html.erb 
<!DOCTYPE html>
<html>
<head>
  <title>Testproject</title>
  <%= stylesheet_link_tag    "application", :media => "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

</body>
</html>
MacBook:testproject xyz$
Ich löse das Rätsel auf: Interessant ist die Zeile:
<%= yield %>
Mit <%= yield %> wird hier die View-Datei inkludiert. Die drei Zeilen mit den Stylesheets und dem JavaScript lassen wir erst mal so, wie sie sind. Damit werden default CSS- und JavaScript-Dateien eingebaut.
Die Datei app/views/layouts/application.html.erb bietet Ihnen die Möglichkeit, das Grund-Layout für die gesamte Rails-Applikation festzulegen. Wenn Sie als Header für jede Seite ein <hr> und darüber einen Text eintragen wollen, dann können Sie das zwischen dem <%= yield %>- und dem <body>-Tag machen:
<!DOCTYPE html>
<html>
<head>
  <title>Testproject</title>
  <%= stylesheet_link_tag    "application", :media => "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
</head>
<body>

<p>Ein Test.</p>
<hr>

<%= yield %>

</body>
</html>
MacBook:testproject xyz$
Sie können aber auch im Verzeichnis app/views/layouts/ noch andere Layouts anlegen und diese je nach Situation anwenden, aber lassen wir das erst mal. Wichtig ist, dass Sie die Grundidee verstehen.

Instanz-Variablen vom Controller zum View übergeben

Einer der Todsünden im MVC-Modell [21]ist ja bekanntlich, zu viel Programm-Logik im View unterzubringen. Quasi so, wie man früher oft in PHP programmiert hat. Bei MVC ist eins der Ziele, dass jeder beliebige HTML-Designer einen View erstellen kann, ohne sich über die Programmierung Gedanken machen zu müssen. Ja, ja, … wenn das immer so einfach wäre. Trotzdem gehen wir das mal gedanklich weiter: Wenn ich im Controller einen Wert habe, den ich im View anzeigen will, so benötige ich dafür einen Mechanismus. Dieser heißt Instanz-Variable (instance variable) und fängt immer mit einem @ an. Wer sich nicht mehr 100 % sicher ist, welche Variable welchen Geltungsbereich (Scope) hat, der sollte ganz fix noch mal einen Blick in „Gültigkeitsbereich (Scope) von Variablen“ werfen.
Im folgenden Beispiel fügen wir im Controller eine Instanz-Variable für die aktuelle Uhrzeit ein und fügen diese dann im View ein. Wir nehmen also Programmier-Intelligenz aus dem View in den Controller.
Die Controller Datei app/controllers/example_controller.rb sieht so aus:
class ExampleController < ApplicationController
  def test
    @current_time = Time.now
  end

end
In der View-Datei app/views/example/test.html.erb können wir dann auf diese Instance-Variable zurückgreifen:
<p>
Die aktuelle Uhrzeit ist 
<%= @current_time %>
</p>
Wir haben jetzt eine klare Trennung von Programmierlogik und Darstellungslogik mit dem Controller und dem View. So können wir im Controller die Uhrzeit je nach Zeitzone des Anwenders automatisch anpassen, ohne dass sich der Designer der Seite darum kümmern muss. Wie immer wird im View automatisch die Methode to_s angewendet.
Mir ist klar, dass jetzt keiner aufspringen und schreien wird: Danke für die Erleuchtung! Ich werde nur noch sauber nach MVC programmieren. Das obige Beispiel ist der erste kleine Schritt in die Richtung und zeigt, wie wir einfach mit Instanz-Variablen Werte aus dem Controller in den View bringen können.

Partials

Selbst bei kleinen Webprojekten gibt es oft wiederkehrende Elemente. Das kann zum Beispiel ein Footer der Seite mit den Kontaktdaten sein oder ein Menü. Rails gibt uns die Möglichkeit, diesen HTML-Code in sogenannte Partials abzuspeichern und dann innerhalb eines Views einzubinden. Ein Partial wird ebenfalls im app/views/example/ Verzeichnis abgespeichert. Allerdings muss der Dateiname mit einem Unterstrich (Underscore = _) anfangen.

Anmerkung

Das englische Adjektiv partial heißt so viel wie Teil-…, partiell oder unvollständig. Partials sind also so etwas wie Teile, Stückchen oder Vorlagen-Schnipsel.
Als Beispiel fügen wir unserer Seite jetzt einen Mini-Footer in einem eigenen Partial hinzu. Dafür schreiben wir in die neue Datei app/views/example/_footer.html.erb den folgenden Inhalt:
<hr>
<p>
Copyright 2009 - <%= Date.today.year %> beim Osterhasen
</p>
Die Datei app/views/example/test.html.erb verändern wir wie folgt und fügen mit dem Befehl render das Partial ein:
<p>Schleife von 0 bis 5:
<% (0..5).each do |i| %>
<%= "#{i}, " %>
<% end %>
</p>

<%= render "footer" %>
Es sind also jetzt folgende Dateien im Verzeichnis app/views/example:
MacBook:testproject xyz$ ls -l app/views/example 
total 8
-rw-r--r--  1 xyz  staff   0 Apr 24 11:03 _footer.html.erb
-rw-r--r--  1 xyz  staff  81 Apr 24 10:33 test.html.erb
MacBook:testproject xyz$
Die neue Webseite sieht nun so aus:
Seite mit Footer als Partial

Wichtig

Der Name eines Partials im Code wird immer ohne den Unterstrich (_) am Anfang und ohne die .erb- und .html-Endung angegeben. Aber die wirkliche Datei muss im Dateinamen mit einem Unterstrich anfangen und auch am Ende mit der .erb- und .html-Endung aufhören.
Partials können auch aus anderen Bereichen des Unterverzeichnisses app/views eingebunden werden. So können Sie für wiederkehrende und übergreifende Inhalte beispielsweise ein Verzeichnis app/views/shared einrichten und dort eine Datei _footer.html.erb anlegen. Das Einbetten im erb-Code würde dann mit folgender Zeile erfolgen:
<%= render "shared/footer" %>

Anmerkung

Das Footer-Problem würde man – je nach Programmiergeschmack – in einem richtigen Projekt nicht mit einem Partial lösen, das überall lokal aufgerufen wird, sondern eher zentral in der app/views/layouts/application.html.erb.

Variablen an ein Partial übergeben

Partials sind im DRY (Don't Repeat Yourself)-Gedanken sehr gut. Aber was sie erst richtig praktisch macht, ist die Möglichkeit, Variablen zu übergeben. Bleiben wir bei unserem Copyright-Beispiel. Wenn wir das Startjahr als Wert übergeben möchten, so können wir das mit folgender Erweiterung in der Datei app/views/example/_footer.html.erb einbauen:
<hr />
<p>
Copyright <%= start_year %> - <%= Date.today.year %> beim Osterhasen
</p>
Ändern wir dazu die app/views/example/test.html.erb wie folgt:
<p>Schleife von 0 bis 5:
<% (0..5).each do |i| %>
<%= "#{i}, " %>
<% end %>
</p>

<%= render 'footer', :start_year => '2000' %>
Wenn wir jetzt die URL http://0.0.0.0:3000/example/test aufrufen, so sehen wir die 2000:
Partial mit local start_year
Manchmal braucht man ein Partial, das teilweise eine Local-Variable benutzt und an anderer Stelle braucht man das gleiche Partial, aber ohne Local-Variable. Das können wir im Partial selber mit einer if-Abfrage abfangen:
<hr />
<p>
Copyright 
<% if defined? start_year %>
<%= start_year %>
 - 
<% end %>
<%= Date.today.year %> beim Osterhasen
</p>

Anmerkung

Mit defined? wird in Ruby überprüft, ob eine Expression definiert ist.
Dieses Partial könnte man mit <%= render 'footer', :start_year => '2000' %> und mit <%= render 'footer' %> aufrufen.
Sie sehen ebenfalls, dass ich beim Einbinden eines einfaches Partials eine kürzere Schreibweise benutzen kann, als bei der Version mit locals.

Andere Schreibweise

In „Variablen an ein Partial übergeben“ benutzen wir nur die Kurzform, um Partials zu rendern. Oft sehen Sie auch diese Langform:
<%= render :partial => "footer", :locals => { :start_year => '2000' } %>

Weitere Dokumentation zum Thema Partials

Wir haben hier wirklich nur die Oberfläche angekratzt. Partials sind sehr mächtige Werkzeuge. Unter http://guides.rubyonrails.org/layouts_and_rendering.html#using-partials finden Sie die Doku von Ruby on Rails zum Thema Partials.