Neues in ActionPack 1.12.0

RJS-Templates

Analog zu rxml und rhtml gibt es jetzt die Fusion von Ruby- und JavaScript-Code.

In RJS-Dateien steht Ruby-Code, der JavaScript erzeugt, ähnlich den rxml-Templates. Dieser Code wird als Ergebnis eines AJAX-Aufrufs an den Browser gesendet und dort ausgeführt. Kurz: RJS-Templates sind Browser-Scripte.

Es klingt kompliziert, aber damit ist AJAX-Entwicklung bedeutend einfacher geworden.

Hintergrund (englisch):

Beispiel

Solche Dinge können in einer .rjs-Datei stehen:
# Der erste Kauf lässt "cart" erscheinen, folgende lassen ihn blinken
page[:cart].visual_effect(@cart.size == 1 ? :appear : :highlight)

# Ersetze "cart" mit dem frischen Inhalt aus dem Partial "cart"
page[:cart].replace_html :partial => "cart" 

# Alle DOM-Elemente der Klasse "product" blinken
page.select(".product").each do |element|
  element.visual_effect :highlight
end

# Rufe die JavaScript-Funktion AddressBook.cancel() auf
page.address_book.cancel

# Setze in 4 Sekunden den font-style aller spans auf "normal"
page.delay(4) do
  page.select("td span.company").each do |column| 
    column.set_style :fontStyle => "normal" 
  end
end

page

Das page-Objekt ist eine Instanz von JavaScriptGenerator und bietet viele Möglichkeiten, die Seite zu verändern:

  • öffne einen alert()-Dialog: page.alert 'nachricht'
  • simuliere einen Redirect mit window.location.href: page.redirect_to ziel (url_for-Syntax)
  • führe eine JavaScript-Funktion aus: page.call funktion, argumente
  • setze einen Wert für eine JavaScript-Variable: page.assign variable, wert
  • ersetze ein komplettes Element: page.replace 'person_45', :partial => 'person', :object => @person
  • HTML in ein Element einfügen: page.insert_html(:bottom, 'list', '<li>Last item</li>')
  • starte einen visuellen Effekt für ein bestimmtes Element: page.visual_effect :highlight, 'list'
  • etwas sichtbar/unsichtbar machen: page.show id / page.hide id
  • ein Element referenzieren: page['id'].show #-> $('id').show();
  • referenziere ein Element anhand der CSS-Hierachie: page.select 'p'
  • page.select('p.welcome b').first #-> $$('p.welcome b').first();
  • page.select('p.welcome b').first.hide #-> $$('p.welcome b').first().hide();
  • füge puren JavaScript-Code ein: page << 'alert(0)'
  • ein Element draggable machen: page.draggable id (oder sortable)
  • page.drop_receiving 'wastebasket', :url => { :action => 'delete' }
  • page.sortable 'todolist', :url => { action => 'change_order' }
  • führe einen Funktion verzögert aus: page.delay(20) { page.visual_effect :fade, 'notice' }

Inline-RJS

Zusätzlich zu den .rjs Dateien im Ordner app/views gibt es auch die Möglichkeit, Inline-RJS zu benutzen:

class UserController < ApplicationController
  def refresh
    render :update do |page|
      page.replace_html 'user_list', :partial => 'user', :collection => @users)
      page.visual_effect :highlight, 'user_list'
    end
  end
end

Hilfsmodule

Natürlich kann man RJS-Hilfsmodule schreiben und sie mit dem page-Objekt benutzen:

module ApplicationHelper
  def update_time
    page.replace_html 'time', Time.now.to_s(:db)
    page.visual_effect :highlight, 'time'
  end
end

class UserController < ApplicationController
  def poll
    render :update { |page| page.update_time }
  end
end

respond_to

Mit respond_to kann eine Aktion anhand des HTTP-Headers das optimale Ausgabeformat erkennen und dementsprechend reagieren:

class WeblogController < ActionController::Base
  def index
    @posts = Post.find :all
    respond_to do |wants|
      wants.html
        # Normales Template in app/views/weblog/index.rhtml
      wants.xml  { render :xml => @posts.to_xml }
        # es wird XML generiert und mit dem korrekten MIME-Typ gesendet
      wants.js
        # nutzt das RJS-Template in app/views/weblog/index.rjs
    end
  end
end

Damit ist es möglich, ganz nebenbei zur Applikationsentwicklung für den Browser auch noch einen WebService zu erstellen.

Mehr dazu findet man bei Jamis Buck: Web services, Rails-style.

Integration Tests

Integration Tests sind eine weitere Sorte von Tests.

Sie erlauben es, innerhalb einer Rails-Applikation das Zusammenspiel von mehreren Controllern und Aktionen zu testen. Für diese Art von Tests existiert seit Rails 1.1 der Ordner test/integration. Ein einfacher Integrationstest unterscheidet sich kaum von einem funktionalen Test:

require "#{File.dirname(__FILE__)}/test_helper"
require "integration_test"

class ExampleTest < ActionController::IntegrationTest

  fixtures :people

  def test_login
    # fordere die Login Seite an
    get "/login"
    assert_equal 200, status
    # sende die Logindaten per POST-Request und folge der Applikation zur Homepage
    post "/login", :username => people(:jamis).username,
      :password => people(:jamis).password
    follow_redirect!
    assert_equal 200, status
    assert_equal "/home", path
  end

end

Mit Integrations-Tests besitzt man weiterhin die Möglichkeit, mehrere Sessions pro Test zu erzeugen und eine Interaktion dieser Instanzen zu testen. Zum Beispiel verwendet 37Signals für Campfire die Tests so:

def test_login_and_speak
  jamis, david = login(:jamis), login(:david)
  room = rooms :office 
  jamis.enter room 
  jamis.speak room, "jemand da?"
  david.enter room 
  david.speak room, "hallo!"
end

module CustomAssertions
  def enter room
    # benutze eine benannte Route, um die interne Konsistenz zu prüfen
    get room_url(:id => room.id)
    assert ...
  end
  def speak room, message
    xml_http_request "/say/#{room.id}", :message => message
    assert ...
  end
end

def login who
  open_session do |sess|
    sess.extend CustomAssertions
    who = people who
    sess.post "/login", :username => who.username,
      :password => who.password
    assert ...
  end
end

Wie man sieht, kann mit geringem Aufwand eine sehr mächtige Test-Sprache (DSL) erstellt werden, die beliebig erweiterbar ist.

Auch hier hat Jamis eine Einführung geschrieben: Jamis Buck: Integration Testing in Rails 1.1

render

:xml

render :xml => '<quiek><foo>bar</foo></quiek>'

...funktioniert wie render :text => ..., aber der Text wird als application/xml / UTF-8 gesendet.

content-type

render hat jetzt eine neue Option, um den Inhaltstyp der Antwort anzugeben, sollte sie von text/html abweichen:
render 'atom.rxml', :content_type => Mime::ATOM
render :text => mein_hund.to_yaml, :content_type => 'text/yaml'

Helper

select :selected => auswahl

Die neue Option :selected erlaubt es, einen beliebigen Eintrag in einem <select>-Feld auszuwählen. :select => nil lässt die Auswahl leer.

auto_link mit Block

Jeder gefundene Link wird durch den Block geschickt, bevor er ausgegeben wird:
auto_link post_body { |link| truncate link, 10 }

Neue Form-Helfer

fields_for erlaubt es, Form-Helfer einfacher aufzurufen und das Objekt, zu dem die Felder gehören, zentral anzugeben.

form_for und remote_form_for (oder form_remote_for, wie man mag) bauen auf fields_for auf.

Beispiel: Statt
<%= form_tag :action => "update" %>
  Vorname: <%= text_field 'person', :first_name %>
  Nachname: <%= text_field 'person', :last_name %>
  Administrator?: <%= check_box 'person', :admin %>
<%= end_form_tag %>
schreibt man
<% form_for :person, @person, :url => { :action => "update" } do |p| -%>
  Vorname: <%= p.text_field :first_name %>
  Nachname: <%= p.text_field :last_name %>
  Administrator?: <%= p.check_box :admin %>
<% end -%>

Bitte die verschiedenen ERb-Modi beachten: form_for ist ein Block und wird evaluiert, während form_tag einen String zurückgibt und keinen Block benutzt.

Die englische API-Doku hierzu ist inzwischen sehr informativ.

JavaScript-Helfer

Aus dem JavaScriptHelper wurden die meisten Methoden in die neuen Module PrototypeHelper und ScritaculousHelper ausgelagert.

Einige Methoden bieten neue Möglichkeiten:

  • button_to_function als Variante von link_to_function.
  • visual_effect kennt jetzt auch die Effekte :toggle_appear, :toggle_slide und :toggle_blind.
  • visual_effect hat jetzt eine Option :queue => 'mein_scope' für Scoped Queues; siehe railsdevelopment.com.
  • auto_complete_field hat eine select-Option, die alle Textelemente als Vorschläge in die Liste aufnimmt, die der CSS-Selektor auswählt. Zum Beispiel könnte man sämtliche code-Tags aus diesem Artikel auswählen.
  • observe_field id, :on => :focus ruft den Observer nur bei einem onfocus-Ereignis auf.

Kleinere Änderungen

  • content_for und capture sind jetzt in jedem Template-Typ verfügbar.
  • submit_tag ändert mit disable_with => '[Aus]' den Text des Buttons, wenn er abgeschaltet wurde.

Sonstiges

param_parsers

Mit der neuen automatisierten Behandlung Request-Parameter ist das Erstellen moderner REST-WebServices ein Kinderspiel.

  • POST- und PUT-Anfragen mit dem Typ application/xml werden als XmlSimple-Objekt in die params eingefügt, wobei der Name des Root-Elementes als Schlüssel dient.
  • Anfragen des Typs application/x-yaml werden mit YAML.load geladen und einfach in die params hineingemischt.
Eigene Parser registriert man so:
ini = Mime::Type.new 'application/ini', :ini
ActionController::Base.param_parsers[ini] =
  proc do |data|
    ini = IniFile.new data
    { 'ini' => ini }
  end

Caching

Action- und FragmentCaching ist nun problemlos möglich, die Daten werden im Verzeichnis tmp/cache gespeichert; siehe hierzu die Liste erzeugter Dateien und Ordner.

Bugfixes

  • Unbenutzte Helper-Dateien können gelöscht werden, ohne Fehler zu erzeugen.
  • link_to_function beachtet nun bestehende :onclick-Definitionen.
  • Probleme mit SCGI wurden gelöst.
  • Layout-Templates mit eigenen Endungen (wie .mab) werden automatisch erkannt.
  • Scaffolds sind nun nicht mehr gefährlich durch zerstörerische GET-Links (damit haben Spider wie wget oder Google alle Daten aus der DB gelöscht.)