Archive for the ‘Webhunter Tutorial’ Category

How to catch a mouse with Apotomo

Thursday, October 15th, 2009

Jaded and with half-closed eyes you look at your screen, dozens of windows of all sizes floating around in front of an army of desktop icons, and right in this digital maze a small black mouse, whizzing between menus, windows and folder symbols.

With dreamy look you follow the mouse cursor, fixing your eyes on this small speedy fellow who jumps easily through the virtual chaos. Man, it would be fun to go on an exiting mouse-hunting, like a real hunter, with javelin and traps, having thrilling adventures with the small savage gnawer.

Too tempting is the mouse-hunting. The actual finger exercise for your poor man’s browser game would be not using JavaScript but implementing with Apotomo, a widget layer for Ruby on Rails.

Apotomo?

Apotomo provides components – widgets – that behave like small Rails controllers, whereas you can use as many widgets as you need on one page. Their event-driven nature makes them perfect for rich client apps, or for your browser game.

Dozily you create a new Rails project

projects$ rails webhunter

with the growling name webhunter, after that you install the Apotomo plugin and Cells, which is the base for Apotomo.

webhunter$ script/plugin install git://github.com/apotonick/cells.git
webhunter$ script/plugin install git://github.com/apotonick/apotomo.git

A widget acts as bear trap

Your first concern is having a trap since this type of hunting seems to be the more suceeding than a spear assault. A widget in the form of an oversized bear trap should be your mate when hunting the mouse cursor, the unforgivingly snapping steel claws fascinated you ever since Rocky I.

webhunter$ script/generate widget BearTrap charged snapped
      exists  app/cells/ 
      create  app/cells/bear_trap 
      create  app/cells/bear_trap_cell.rb 
      create  app/cells/bear_trap/charged.html.erb 
      create  app/cells/bear_trap/snapped.html.erb 
      create  test/functional/test_bear_trap_cell.rb

After a courageous stroke on Enter you get your widget class and early view stubs, that all looks very similar to a conventional controller with views. Speaking about controllers: that’s another thing you would need to plug-in your trap into the page.

webhunter$ script/generate controller forest undergrowth
      exists  app/controllers/ 
      create  app/controllers/forest_controller.rb
      ...

To escape from your daily office routine you put up a dark ForestController with an action undergrowth where you insidiously place your trap. By now you can hear the cracking sound of animal paws in the untergrowth, mysterious songbirds chirp their lonly melodies, the trees get shrouded in light mist.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ForestController < ApplicationController 
  include Apotomo::ControllerMethods 
 
  def undergrowth 
    use_widgets do |root| 
      trap = cell(:bear_trap, :charged, 'evil_trap') 
      root << trap 
    end 
 
    @trap = render_widget('evil_trap') 
 
    render 
  end 
end
Your dark forest, still innocent.

Your dark forest, still innocent.

Real Widgets in Rails

To come to the magic power of Apotomo you pull the module Apotomo::ControllerMethods in your controller (line 2). Man, that’s exciting, just yet you’re feeling like the guy from Predator.

5
6
7
8
    use_widgets do |root| 
      trap = cell(:bear_trap, :charged, 'evil_trap') 
      root << trap 
    end

First of all you create the widget evil_trap in your controller action (line 6). The #cell method assists you when passing the name of the widget class, the start state and a unique ID, therefore :bear_trap would be BearTrapCell, :charged is the method to be executed the first time the widget is rendered and evil_trap is the unique name of your evil trap.

After you created the widget object, you attach it to an ominous root widget (line 7), which is kindly provided by the #use_widgets method (line 5).
With these lines you brought a unique trap widget into being, the hunt is on!

A bear trap on rails

For the sake of completeness you glance at your BearTrapCell to assure the state :charged isn’t doing something unreasonable, like blowing a fire or some other outrage in the bush.

app/cells/bear_trap_cell.rb

1
2
3
4
5
class BearTrapCell < Apotomo::StatefulWidget 
  def charged 
    render 
  end 
end

A call to #render_widget in the controller passing the evil_trap widget seems to do nothing more than invoking the state method #charged of a certain BearTrapCell instance, where the method itself just renders its view (line 3).

10
@trap = render_widget('evil_trap')

To place the trap in the undergrowth you render your nasty widget to an instance variable of the controller (line 10).

Holding the rendered markup in your hand you can situate it in any location in the undergrowth.html.erb view.

/app/views/forest/undergrowth.html.erb

1
2
3
<div id="forest"> 
  <%= @trap %> 
</div>

When peeking from your deerstand in the forest you see the rendered standard view of the trap widget in your undergrowth action.

A harmless trap widget in the dark forest.

A harmless trap widget in the dark forest.

So, Apotomo helps you building small mini-controller, or widgets, and embedding them in real existing controllers. In so doing, the widgets expose a behaviour similar to controllers, they have actions („states“) and can render views.

An interactive trap

A dark forest, the smell of firs and moss in your nose, in the distance you can hear a pecker getting his job done. You inhale deeply the humid air, you already scent it, the small gnawer, which is out for trouble in the undergrowth. Anyway you should instantly exchange the boring standard view of your trap with the likeness of a frightening bear trap, otherwise your victim will break out into roaring laughter at this sight.

app/cells/bear_trap/charged.html.erb

1
<%= image_tag "bear_trap_charged.png" %>
This looks like trouble .

This looks like trouble.

Yeah, bare steel.

OnMouseover – and the trap snaps shut!

Your mouse trap looks awesome now, anyway it lacks a triggering mechanism and a respective state for the bear trap widget when the trap has snapped.

Well, for what we got the :onMouseover event? That’s exactly what your trap needs. You extend the charged view.

app/cells/bear_trap/charged.html.erb

1
<%= image_tag "bear_trap_charged.png", :onMouseover => trigger_event(:mouseAlarm) %>

If the mouse steps into the trap this will trigger a :mouseAlarm typed event, the helper method #trigger_event surely worries about that.

The event is delivered via AJAX to Rails and it is Apotomo taking care of the event bubbling up from the triggering widget, the bear trap, up to the very highest root widget. Your mouse- or bear trap is directly attached at the root widget, so the bubbling won’t be that spectacular, anyway, you like the bubbling.

Firebug and nobody else is catching events right now.

Firebug and nobody else is catching events right now.


After reloading the page you move the mouse over the trap, and something happens… not. Indeed there is some AJAX request triggered, but the page remains the same.

Stop the mouse, eh, the event!

If you roll the mouse over the trap, seeking for a sudden death of this poor little animal, then Apotomo admittedly triggers an event and it bubbles up to the top. The bubble bursts at the top in the root widget, nothing more happens. Even worse, the bubble doesn’t burst, it is simply ignored.
Something is missing. An event hunter manifesting interest for this event, and acting uncompromising when it sees the event. An event hunter to snap the trap.

app/controllers/forest_controller.rb

5
6
7
    use_widgets do |root| 
      trap = cell(:bear_trap, :charged, 'evil_trap') 
      trap.respond_to_event(:mouseAlarm, :with => :snapped)

In your extended version of the ForestController you take the bear trap widget and stick such a watcher to it using #respond_to_event (line 7). Knowing that #trigger_event will fire an event that bubbles up from evil_trap to root, we also know the event will surely pass this watcher. This one will „see“ the event and will snap the trap by sending the evil_trap to a new state :snapped.

Widgets are deadly state machines

Excited you roll the mouse once more over the trap, again, nothing happens. What the heck is wrong here? Ouh, well, you forgot to allow this state transition in the bear trap. Without having defined transitions the widget will stay in its start state that was specified in the creating #cell(...) call.

app/cells/bear_trap_cell.rb

1
2
3
class BearTrapCell < Apotomo::StatefulWidget 
 
  transition :from => :charged, :to => :snapped

The class method ::transition and two of its parameters :from and :to define the transition from state :charged to :snapped. Your murderous intend in mind you also extend the state method :snapped. It should output a JavaScript alert, telling us that it would have snapped.

app/cells/bear_trap_cell.rb

1
2
3
4
5
6
7
8
9
10
11
class BearTrapCell < Apotomo::StatefulWidget 
 
  transition :from => :charged, :to => :snapped 
 
  def charged 
    render 
  end 
 
  def snapped 
    render :js => 'alert("gotcha!")' 
  end

So if there really is a transition happening from :charged to :snapped the state method #snapped is invoked. The method itself injects an alarm message with render :js into the page. If that will work? Once again you let the mouse step into the diabolical bear trap, and… it works!

A talking trap. Is there really such a thing?

A talking trap. Is there really such a thing?

Well, a ridiculous message doesn’t catch a mouse. A snapping claw clinging its bag with a bare-knuckle metallic „clack“, that’s what you want.

In place of harmless JavaScript your #snapped method would have to render a snapped trap.

app/cells/bear_trap_cell.rb

9
10
11
  def snapped 
    render 
  end

This #render call looks for a view snapped.html.erb, which is set up out of hand.

app/cells/bear_trap/snapped.html.erb

1
  <%= image_tag 'bear_trap_snapped.png' %>
Clack!

Clack!


As expected the trap snaps as you roll the mouse over it. Clack!

Summing up

Alright. You embedded your widget into a controller action utilizing #render_widget. A widget looks like a controller, has methods and respective views. In the view you used #trigger_event to report an event to rails, or better: Apotomo.

To react upon the event you used #respond_to_event. If it encounters the event your trap is sent to another state. The new state is a plain method again and renders a view. The new view automatically replaces the view of the former state on the page. That’s… awesome.

Cheese in the trap: composed widgets

For hours you’ve been lying in ambush. With bated breath you constantly keep staring at the trap, your necks starts to hurt. Darkness is falling around you.

For some reasons you didn’t catch any mouse, yet. And suddenly the scales fall from your hair: Mice do love cheese!

You decide to put a piece of cheese in the form of a separate widget into the trap widget. To hide knowledge about cheese from the trap the encapsulation of the cheese as autonomous widget makes sense, and traps usually shouldn’t be interested in the type of bait. I mean, have you ever met a customary trap that prefers gentle melting chocolate in favour of smelling cheese?

The cheese widget should consist of exactly one state, in particular it should be the state that displays a yummy piece of cheese.

webhunter$ script/generate widget cheese smelling 
      create  app/cells/cheese_cell.rb 
      create  app/cells/cheese/smelling.html.erb

Sometimes generators in rails have a right to exist, yet. You create a CheeseCell widget having the solely state :smelling, which represents the bait now.

app/cells/cheese/smelling.html.erb

1
<%= image_tag "cheese.png" %>

That’s incredibly exciting.

Again you open the ForestController and plug the piece of cheese into the trap by extending the widget tree.

4
5
6
7
def undergrowth 
    use_widgets do |root| 
      trap = cell(:bear_trap, :charged, 'evil_trap') 
        trap << cell(:cheese, :smelling, 'piece_of_cheese')

The assignment in line 7 adds a child to the evil_trap widget.

Yeah, that’s the way it goes!

Cheese!

Of course, the child is the CheeseCell widget with start state :smelling, as a unique ID you pass piece_of_cheese.

The last thing you gotta do is extending the view of your trap so it displays the bait.

app/cells/bear_trap/charged.html.erb

1
2
<%= image_tag "bear_trap_charged.png", :onMouseover => trigger_event(:mouseAlarm) %>
<%= rendered_children['piece_of_cheese'] %>

That looks appealing .

That looks appealing.

Delighted you notice that your trap looks very attractive once it contains a piece of cheese.

Getting already rendered children seems to work by accessing @rendered_children rendered_children. That’s how you can place children in the views of their parents.

For completeness you have to extend the view of the snapped trap, too. For example, it could happen that a bear would actually trigger the trap, a bear that wouldn’t eat the cheese and thus be stuck in the trap together with the cheese.

app/cells/bear_trap/snapped.html.erb

1
2
<%= image_tag 'bear_trap_snapped.png' %>
<%= rendered_children['piece_of_cheese'] %>

Assault of the Hungry Mouse

So there is cheese in the charged trap. As soon as the mouse toddles into the trap it snaps shut, but the cheese remains in the trap. „What a non-sense!“ you think, shouldn’t the cheese disappear, and the trap stay charged? Considering the fly weight a mouse wouldn’t trigger a bear trap.

The cheese-eating logic neither belongs in the cheese widget, nor in the trap. You need a new widget, a mouse widget.

$ script/generate widget mouse lurking eating
      exists  app/cells/
      create  app/cells/mouse
      create  app/cells/mouse_cell.rb
      create  app/cells/mouse/lurking.html.erb
      create  app/cells/mouse/eating.html.erb
      create  test/functional/test_mouse_cell.rb

To have the mouse placed in the forest you append it to root, as you did with the trap. In order to do that you have to return to #use_widgets in the controller to call the mouse to life.

app/controllers/forest_controller.rb

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  def undergrowth
    use_widgets do |root|
      trap = cell(:bear_trap, :charged, 'evil_trap')
        trap << cell(:cheese, :smelling, 'piece_of_cheese')
      #trap.respond_to_event(:mouseAlarm, :with => :charged)
      root << trap
 
      root << cell(:mouse, :lurking, 'hungry_mouse')
    end
 
    @trap   = render_widget('evil_trap')
    @mouse  = render_widget('hungry_mouse')
 
    render
  end
That's how your current widget tree looks like .

That's how your current widget tree looks like.

First of all you uncomment the watcher that snaps the trap (line 8). You’re gonna think about that later, and moreover you don’t want to hurt the sweet mouse.

Next, you add the new mouse widget to the widget tree, namely in the start state :lurking (line 11). To place the mouse in the controller view you render it using #render_widget (line 15).

app/cells/mouse/lurking.html.erb

1
<%= image_tag "mouse.png" %>

After you polished the view of the :lurking state you got the two duellists mouse and trap standing face to face.

EOne on One .

One on One.

Bubbling events

To steal the cheese from the trap the mouse has to catch the :mouseAlarm event and eat the cheese. Again you extend the #use_widgets block in the controller.

app/controllers/forest_controller.rb

4
5
6
7
8
9
10
11
12
  def undergrowth
    use_widgets do |root|
      trap = cell(:bear_trap, :charged, 'evil_trap')
        trap << cell(:cheese, :smelling, 'piece_of_cheese')
      root << trap
 
      root << cell(:mouse, :lurking, 'hungry_mouse')
      root.respond_to_event(:mouseAlarm, :with => :eating, :on => 'hungry_mouse')
    end

You append a new watcher to root (line 11) and read out aloud that line.

Root, if you catch an event of type :mouseAlarm respond to it by invoking the state :eating on the hungry_mouse widget.“

You cannot add this watcher directly to the mouse widget. Why? Well, the :mouseAlarm event is triggered by the trap and it bubbles up to root. On its way to the top it doesn’t pass the mouse at all, so it’s root who does the job.

Finally: Statefulness!

Instead of making the cheese disappear you decide to count the pieces of eaten cheese in the mouse widget. That widget class already got a bunch of lines.

app/cells/mouse_cell.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MouseCell < Apotomo::StatefulWidget
  transition :from => :lurking, :to => :eating
  transition :in => :eating
 
  def lurking
    @cheese_count = 0
    render
  end
 
  def eating
    @cheese_count += 1    
    render :view => :lurking
  end
end

Hey, wouldn’t it be fun if the mouse would get fatter with every piece of cheese it eats? You implement that instantaneously by enhancing the view of :lurking.

app/cells/mouse/lurking.html.erb

1
<%= image_tag "mouse.png", :width => 90 * (@cheese_count+1)  %>

The image width is recalculated every time the widget renders, making the mouse fatter and fatter with every piece.

Fat mouse .

Fat mouse.

Ok, what’s going on here?

From state to state

When sending the mouse in the trap this triggers the well-known :mouseAlarm event. That one is catched and sends the hungry_mouse widget to the state :eating. As this is a state transition initiated from outside (namely by the #respond_to_event watcher) this transition has to be allowed (line 2).

After that, the mouse keeps eating in the state :eating. Another event would send the widget from :eating to :eating. And that must be allowed as well (line 3).

Counting cheese

You initialize the cheese counter in the start state :lurking and set it to zero, so the mouse has an empty stomach (line 6).

app/cells/mouse_cell.rb

5
6
7
8
9
10
11
12
13
  def lurking
    @cheese_count = 0
    render
  end
 
  def eating
    @cheese_count += 1    
    render :view => :lurking
  end

As you already defined the mouse stays in :eating state, its state method is executed again and again with every event. The method worries about incrementing the cheese counter (line 11).
When it’s up-to-date the view of the :lurking state is rendered, and a growing mouse is displayed (line 12).

Statefulness and automatic updates

You note happily that the instance variable @cheese_count keeps actually established although there are multiple requests between the events. That’s the stateful widgets everybody’s on.

Useless to say that every widget, even our fat mouse, keeps itself updated on the page via AJAX. Or did you write one line of JavaScript yet, since you develop this trailblazing mouse-game?

Talking widgets

In fact it would be consequent to make the trap snap shut as the mouse grows and reaches a sufficient weight to trigger.

Somehow the trap has to detect if the mouse is heavy enough. Maybe you should bring the trap watcher back into play.

app/controllers/forest_controller.rb

4
5
6
7
8
9
def undergrowth
    use_widgets do |root|
      trap = cell(:bear_trap, :charged, 'evil_trap')
        trap << cell(:cheese, :smelling, 'piece_of_cheese')
      trap.respond_to_event(:mouseAlarm, :with => :snapped)
      ...

Not only the mouse but also the trap are watching the :mouseAlarm event now.

Yes- it’s absolutely ok to have multiple watchers for one event.

That's not fat enough!

That's not fat enough!

The bear traps’ state :snapped should decide if it’s worth snapping or if we’re better of waiting for fatter booties.

At the moment the trap snaps and the mouse gets a bit fatter when moving the cursor over the trap. However, the mouse should be really fat to trigger the trap, at least 3 pieces of cheese have to be eaten, you decide.

app/cells/bear_trap_cell.rb

1
2
3
4
5
6
7
8
9
10
11
class BearTrapCell < Apotomo::StatefulWidget
  transition :from => :charged, :to => :snapped
 
  def charged
    render
  end
 
  def snapped
    jump_to_state :charged unless root.find_by_id('hungry_mouse').cheese_count >= 3
    render
  end

The bear trap is sent to :snapped at each and every :mouseAlarm. However, it will stay in this state only if the cheese counter of the mouse is really greater than 3 (line 9).

If not, it calls the method #jump_to_state and jumps back to :charged and waits for bigger booties.

Obviously this does work properly only if the mouse answers to #cheese_count.

app/cells/mouse_cell.rb

1
2
3
4
5
class MouseCell < Apotomo::StatefulWidget
  transition :from => :lurking, :to => :eating
  transition :in => :eating
 
  attr_reader :cheese_count

Not only the mouse gets bigger now when stealing cheese from the trap, also the trap’s in and queries the mouse if it is heavy enough by chance.

It's getting bigger and bigger!

It's getting bigger and bigger!


And, wow, that works! The fourth time rolling over the trap snaps shut.

That’s enough for today, folks!

It would be nice if both cheese and mouse would disappear, since the cheese is eaten and the fat mouse slopes off as the trap snaps with a loud „clack!“. Or have you ever seen a mouse that gets caught in a bear trap?

Also the dependency annoys you that you created in the bear trap – at the moment it is aware of the hungry_mouse and asks the mouse widget in the #snapped state method, that shouldn’t be the case with independent components.

„Well, I bet this can be done even better with Apotomo, but for today I’m done!“ you say to yourself, lean back and enjoy the fact that no mouse has been harmed in your game, yet.



The running example code can be found at github.

Auf Mäusejagd mit Apotomo!

Tuesday, October 13th, 2009

Erschöpft und mit halbgeschlossenen Augen blicken Sie auf Ihren Monitor, dutzende Fenster verschiedenster Grössen tummeln sich vor einem Heer an Icons, und inmitten dieses digitalen Labyrints eine kleine schwarze Maus, die flink zwischen Menüs, Fenstern und Ordner-Symbolen herumflitzt.

Mit verträumtem Blick folgen Sie dem Maus-Cursor, Ihre Augen lassen nicht mehr ab von diesem kleinen flinken Racker, der mühelos durch dieses virtuelle Chaos springt. Wie schön wäre es, auf eine spannende Mäusejagd zu gehen, wie ein echter Jäger, mit Speerspitze und Fallen, und aufregenden Abenteuern mit gefährlichen kleinen Nagetieren…

Die Mäusejagd ist zu verlockend, als dass Sie davon ablassen könnten. Die eigentliche Fingerübung für Ihr popliges Browser-Spiel soll darin bestehen, es nicht mit reinem JavaScript zu implementieren, sondern mit Apotomo, einer Widget-Schicht für Ruby on Rails.

Apotomo?

Apotomo stellt Ihnen Komponenten – Widgets – bereit, die sich wie kleine Rails Controller verhalten, aber von denen beliebig viele auf einer Seite verwendet werden können und diese zudem noch per Events steuerbar sind. Perfekt also für eine Rich Client App, oder eben ein Browserspiel.
Ein dunkler Wald, und eine Bärenfalle.

Noch im Halbschlaf erstellen Sie ein neues Rails Projekt

projects$ rails webhunter

mit dem knurrenden Namen webhunter, dann installieren Sie das Apotomo Plugin und Cells, auf welchem Apotomo aufsetzt.

webhunter$ script/plugin install git://github.com/apotonick/cells.git
webhunter$ script/plugin install git://github.com/apotonick/apotomo.git

Eine Bärenfalle als Widget

Als erstes muss eine Falle her, da diese Jagdform, im Gegensatz zum aktiven Angriff mit Speer, wohl eher von Erfolg gekrönt sein wird. Ein Widget in Form einer überdimensionierte Bärenfalle soll auf der Jagd nach dem Mauscursor Ihr Gefährte sein, die unerbittlich zuschnappenden Stahlklauen haben Sie schon seit Rocky I fasziniert.

webhunter$ script/generate widget BearTrap charged snapped
      exists  app/cells/ 
      create  app/cells/bear_trap 
      create  app/cells/bear_trap_cell.rb 
      create  app/cells/bear_trap/charged.html.erb 
      create  app/cells/bear_trap/snapped.html.erb 
      create  test/functional/test_bear_trap_cell.rb

Nach einem beherzten Hieb auf die Enter-Taste bekommen Sie die Widgetklasse und erste View-Stubs serviert, alles sieht sehr nach einem üblichen Controller mit Views aus. A propos Controller: den brauchen Sie natürlich auch noch, um Ihre Falle in eine Seite einzuhängen.

webhunter$ script/generate controller forest undergrowth
      exists  app/controllers/ 
      create  app/controllers/forest_controller.rb
      ...

Um dem grauen Büroalltag zu entkommen, bauen Sie sich einen dunklen ForestController, der eine Action undergrowth (Unterholz) hat, in dem Sie heimtückisch Ihre Falle aufstellen. Schon jetzt hören Sie das Knacken von Tierpfoten im Unterholz, geheimnisvolle Vögel zwitschern ihr einsames Lied, leichter Nebel kommt auf.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ForestController < ApplicationController 
  include Apotomo::ControllerMethods 
 
  def undergrowth 
    use_widgets do |root| 
      trap = cell(:bear_trap, :charged, 'evil_trap') 
      root << trap 
    end 
 
    @trap = render_widget('evil_trap') 
 
    render 
  end 
end
Ihr Dunkler-Wald Controller, noch unbefleckt und ohne Fallen.

Ihr Dunkler-Wald Controller, noch unbefleckt und ohne Fallen.

Echte Widgets in Rails

Um an die Apotomo-Zauberkräfte zu gelangen, ziehen Sie das Modul Apotomo::ControllerMethods in Ihren Controller (Zeile 2). Man ist das aufregend, Sie fühlen sich schon jetzt fast wie der Jäger aus Predator.

5
6
7
8
    use_widgets do |root| 
      trap = cell(:bear_trap, :charged, 'evil_trap') 
      root << trap 
    end

Zuallererst erstellen Sie das Widget evil_trap in der Controller Action (Zeile +2). Die #cell Methode hilft Ihnen, wenn Sie ihr den Namen der Widgetklasse, den Startzustand und eine eindeutige ID übergeben, :bear_trap würde also BearTrapCell bedeuten, :charged ist die Methode, die beim ersten Aufruf gerendert würde und evil_trap ist der eindeutige Name Ihrer bösen Falle.

Nachdem Sie das Widgetobjekt erstellt haben, hängen Sie es an ein ominöses root Widget (Zeile 7), dass Ihnen freundlicherweise durch die #use_widgets Methode bereitgestellt wurde (Zeile 5).
Hiermit haben Sie ein persistentes und einzigartiges Fallen-Widget geschaffen, die Jagd ist somit eröffnet!

Eine Bärenfalle auf Schienen

Der Vollständigkeit halber werfen Sie noch einen Blick in Ihre BearTrapCell und versichern sich, dass der State :charged nichts Unvernünftiges anstellt, wie z.B. ein Feuer zu entzünden oder sonst irgendwelche Freveleien im Gebüsch anstellt.

app/cells/bear_trap_cell.rb

1
2
3
4
5
class BearTrapCell < Apotomo::StatefulWidget 
  def charged 
    render 
  end 
end

Ein Aufruf an #render_widget im Controller auf das Widget evil_trap macht also nichts weiter, als die State Methode #charged einer bestimmten BearTrapCell Instanz auszuführen, die Methode ihrerseits rendert einfach nur ihr View (Zeile 3).

10
@trap = render_widget('evil_trap')

Um die Falle nun noch im Unterholz zu platzieren, rendern Sie Ihr fieses Widget in eine Instanzvariable des Controllers (Zeile 10).

Da Sie nun das gerenderte Markup in der Hand haben, können Sie es an einen beliebigen Ort im undergrowth.html.erb View setzen.

/app/views/forest/undergrowth.html.erb

1
2
3
<div id="forest"> 
  <%= @trap %> 
</div>

Als Sie nun von Ihrem Hochsitz in den Wald spähen, sehen Sie das gerenderte Standard-View des Fallen-Widgets in Ihrer undergrowth Action.

Ein etwas komisch anmutendes Fallen-Widget im dunklen Wald.

Ein etwas komisch anmutendes Fallen-Widget im dunklen Wald.

Mit Apotomo können Sie also kleine Mini-Controller, oder Widgets, bauen und diese in bestehende echte Controller einbetten. Hierbei legen die Widgets ein an Controller angelehntes Verhalten an den Tag, sie haben Actions („states“) und können Views rendern.

Eine interaktive Falle

Ein dunkler Wald, der Geruch von Tannen und Moos liegt Ihnen in der Nase, in der Ferne klopft ein Specht. Tief atmen Sie die Luft ein, Sie können es bereits wittern, das kleine Nagetier, das dort im Unterholz sein Unwesen treibt. Doch sollten Sie schleunigst das langweilige Standard-View Ihrer Falle gegen das Konterfei einer furchteinflössenden Bärenfalle eintauschen, sonst bricht Ihr Opfer beim dessen Anblick allenfalls in schallendes Gelächter aus.

app/cells/bear_trap/charged.html.erb

1
<%= image_tag "bear_trap_charged.png" %>
Das sieht nach Ärger aus.

Das sieht nach Ärger aus.

Ja, blanker Stahl.

OnMouseover – und die Falle schnappt zu!

Ihre Mausefalle sieht jetzt zwar toll aus, es fehlt aber ein Auslösemechanismus und ein entsprechender Zustand für das Bärenfallen Widget, wenn die Falle zugeschnappt ist.

Nun, wofür wurde der :onMouseover Event erfunden? Das ist doch genau das, was Ihre Falle braucht. Sie erweitern also das charged View.

app/cells/bear_trap/charged.html.erb

1
<%= image_tag "bear_trap_charged.png", :onMouseover => trigger_event(:mouseAlarm) %>

Sollte die Maus die Falle betreten, löst das einen Event vom Typ :mouseAlarm aus, dafür sorgt die Helper Methode #trigger_event.

Der Event wird per AJAX an Rails gemeldet und Apotomo sorgt dafür, dass der Event vom auslösenden Widget, der Bärenfalle, bis ganz hoch zum obersten root Widget blubbert. Ihre Mause- oder Bärenfalle hängt direkt am root Widget, also ist das nicht wirklich spektakulär, dennoch freuen Sie sich über das Blubbern.

Ausser Firebug fängt hier noch niemand was.

Ausser Firebug fängt hier noch niemand was.


Als Sie jetzt die Seite neu laden und mit der Maus über die Falle fahren, da passiert …nichts. Zwar wird der AJAX-Request ausgelöst, aber die Seite bleibt gleich.

Fang die Maus, äh, den Event!

Wenn Sie jetzt also mit der Maus über die Falle fahren und danach trächtigen, das arme kleine Tierchen in sein grausame Verderben zu schicken, dann triggert Apotomo zwar einen Event und dieser blubbert wie eine Blase nach oben, aber ausser oben im root Widget zu zerplatzen (eigentlich wird er einfach nur ignoriert, aber das ist fast schlimmer), geschieht nicht wirklich viel. Es fehlt natürlich ein Event Jäger, der sein Interesse für diesen Event bekundet und im Bedarfsfall knallhart handelt – und die Falle zuschnappen lässt.

app/controllers/forest_controller.rb

5
6
7
    use_widgets do |root| 
      trap = cell(:bear_trap, :charged, 'evil_trap') 
      trap.respond_to_event(:mouseAlarm, :with => :snapped)

In Ihrer erweiterten Version des ForestControllers nehmen Sie das Bärenfallen Widget und hängen solch einen Beobachter mit Hilfe #respond_to_event daran an (Zeile 7). Da der von #trigger_event ausgelöste Event aus dem Browser von der evil_trap hoch zu root blubbert, kommt er auf jeden Fall an diesem Beobachter vorbei. Dieser „sieht“ den Event und lässt die Falle zuschnappen, indem er die evil_trap in einen neuen Zustand :snapped schickt.

Widgets sind tödliche Zustandsmaschinen

Gespannt fahren Sie ein weiteres Mal mit der flinken Maus über die Falle, doch wieder passiert nichts. Was ist denn nun schon wieder falsch? Ach natürlich, Sie haben noch vergessen, der Bärenfalle diesen Zustandsübergang zu erlauben. Ohne definierte Übergänge bleibt das Widget nämlich immer in seinem Startzustand, der in #cell(...) festgelegt wurde.

app/cells/bear_trap_cell.rb

1
2
3
class BearTrapCell < Apotomo::StatefulWidget 
 
  transition :from => :charged, :to => :snapped

Sie definieren mit der Klassenmethode #transition und den beiden Parametern :from und :to einen Übergang vom Zustand :charged in :snapped. Ihr mörderisches Vorhaben verfolgend erweitern Sie im gleichen Atemzug noch die Statemethode #snapped. Diese soll erstmal nur per JavaScript eine Meldung darüber ausgeben, dass sie jetzt eigentlich zugeschnappt wäre.

app/cells/bear_trap_cell.rb

1
2
3
4
5
6
7
8
9
10
11
class BearTrapCell < Apotomo::StatefulWidget 
 
  transition :from => :charged, :to => :snapped 
 
  def charged 
    render 
  end 
 
  def snapped 
    render :js => 'alert("gotcha!")' 
  end

Findet also ein Übergang von :charged nach :snapped statt, wird die Statemethode #snapped ausgeführt, die dann per render :js eine Alarmmeldung in die Seite injiziert. Na ob das klappt? Ein weiteres Mal schicken Sie die arme Maus in die teuflische Bärenfalle, und sehen: es klappt!

Eine sprechende Falle. Wo gibt’s denn sowas?

Eine sprechende Falle. Wo gibt’s denn sowas?

Nun, mit einer lächerlichen Textmeldung ist noch lange keine Maus gefangen. Eine zuklaffende Kralle, die mit einem harten metallischen „Klack!“ gnadenlos Ihre Beute festhält, das ist das was Sie haben wollen.

Statt ungefährlichem JavaScript muss Ihre #snapped Methode also eine zugeschnappte Falle rendern.

app/cells/bear_trap_cell.rb

9
10
11
  def snapped 
    render 
  end

Diese #render Anweisung sucht nach einem View snapped.html.erb, welches Sie kurzerhand anlegen.

app/cells/bear_trap/snapped.html.erb

1
  <%= image_tag 'bear_trap_snapped.png' %>
Klack!

Klack!


Erwartungsgemäss schnappt die Falle jetzt zu, als Sie mit der Maus darüberfahren. Klack!

Nochmal zum mitschreiben

Also: Sie haben mit #render_widget Ihr Widget in eine Controller Action eingebettet. Ein Widget sieht genauso aus wie ein Controller, mit Methoden und dazugehörigen Views. Im View haben sie #trigger_event verwendet, um einen Event an Rails bzw. Apotomo zu vermelden.

Eine Reaktion auf den Event haben Sie mit #respond_to_event eingerichtet. Wird der Event erwischt, senden Sie Ihre Falle in einen weiteren Zustand. Der neue Zustand ist wieder einfach nur eine Methode, die ein View rendert. Das neue View ersetzt dann automatisch das View des vorherigen Zustands in der Seite. Das ist… cool.

Käse in die Falle: zusammengesetzte Widgets

Seit Stunden liegen Sie auf der Lauer. Mit flachem Atem blicken Sie unentwegt in Richtung Falle, eine leichte Halsstarre macht sich bereits bemerkbar. Um Sie herum wird es dunkel.

Irgendwie will Ihnen keine Maus in die Falle gehen. Und da fällt es Ihnen wie Schuppen aus den Haaren: Mäuse lieben Käse!

Sie beschliessen, ein Stück Käse in Form eines eigenständigen Widgets in das Fallen Widget zu legen. Durch das Kapseln als eigenes Widget nehmen Sie möglichst viel Wissen über Käse aus der Falle, diese soll ja eigentlich auch nur wissen, dass irgendein Köder in ihr liegt. Ob dieser nach Käse riecht oder es sich dabei um ein Stück zart schmelzender Schokolade handelt, ist einer handelsüblichen Bärenfalle normalerweise ziemlich egal.

Das Käse Widget soll aus einem Zustand bestehen, nämlich genau dem, der ein leckeres Stück Käse darstellt.

webhunter$ script/generate widget cheese smelling 
      create  app/cells/cheese_cell.rb 
      create  app/cells/cheese/smelling.html.erb

Manchmal haben Generatoren in Rails doch eine Daseinsberechtigung. Sie erstellen sich ein CheeseCell Widget, dessen einziger Zustand :smelling besitzt ein View, das nur den Köder darstellt.

app/cells/cheese/smelling.html.erb

1
<%= image_tag "cheese.png" %>

Unglaublich spannend.

Nun öffnen Sie wieder den ForestController und stecken das Stück Käse in die Falle, indem Sie den Widgetbaum erweitern.

4
5
6
7
def undergrowth 
    use_widgets do |root| 
      trap = cell(:bear_trap, :charged, 'evil_trap') 
        trap << cell(:cheese, :smelling, 'piece_of_cheese')

Durch die Zuweisung in Zeile 7 haben Sie dem evil_trap Widget ein Kind angehängt.

Ja, so schnell kann das gehen!

So ein Käse

Das Kind ist natürlich das CheeseCell Widget mit dem Startzustand :smelling, als eindeutige ID nennen Sie es piece_of_cheese.

Nun müssen Sie nur noch das View der Falle erweitern, damit dieses auch den Köder darstellt.

app/cells/bear_trap/charged.html.erb

1
2
<%= image_tag "bear_trap_charged.png", :onMouseover => trigger_event(:mouseAlarm) %>
<%= rendered_children['piece_of_cheese'] %>

Das sieht doch verlockend aus.

Das sieht doch verlockend aus.

Entzückt stellen Sie fest, dass sich Ihre Falle als äussert attraktiv herausstellt, wenn erst mal ein Stück Käse darinliegt.

Sie können also über @rendered_children rendered_children auf bereits gerenderte Kinderwidgets zugreifen, und somit Kinder in den Views der Eltern platzieren.

Der Vollständigkeit halber muss natürlich auch das View der zugeschnappten Falle erweitert werden – es könnte ja zum Beispiel sein, das tatsächlich ein Bär die Falle auslöst, dieser aber keinen Käse mag und dann zusammen mit einem Stück Käse in der Falle steckt.

app/cells/bear_trap/snapped.html.erb

1
2
<%= image_tag 'bear_trap_snapped.png' %>
<%= rendered_children['piece_of_cheese'] %>

Angriff der hungrigen Maus

Es liegt nun also Käse in der aufgestellen Falle. Sobald die Maus in die Falle tappt, schnappt diese zu, der Käse bleibt aber darin liegen. „Was für ein Unsinn!“ denken Sie sich, eigentlich müsste ja der Käse verschwinden, die Falle aber offenbleiben, denn eine Maus wird mit ihrem Fliegengewicht wohl kaum eine Bärenfalle auslösen

Die Käse-Wegfress Logik gehört weder in das Käsewidget, noch in die Falle. Ein neues Widget, ein Mauswidget, muss her.

$ script/generate widget mouse lurking eating
      exists  app/cells/
      create  app/cells/mouse
      create  app/cells/mouse_cell.rb
      create  app/cells/mouse/lurking.html.erb
      create  app/cells/mouse/eating.html.erb
      create  test/functional/test_mouse_cell.rb

Die Maus soll, wie die Falle auch, an root hängen, also direkt im Wald. Hierzu müssen Sie wieder an den #use_widgets Block im Controller ran, um die Maus in ihrem natürlichen Lebensraum zuzuführen.

app/controllers/forest_controller.rb

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  def undergrowth
    use_widgets do |root|
      trap = cell(:bear_trap, :charged, 'evil_trap')
        trap << cell(:cheese, :smelling, 'piece_of_cheese')
      #trap.respond_to_event(:mouseAlarm, :with => :charged)
      root << trap
 
      root << cell(:mouse, :lurking, 'hungry_mouse')
    end
 
    @trap   = render_widget('evil_trap')
    @mouse  = render_widget('hungry_mouse')
 
    render
  end
So sieht Ihr momentaner Widget Baum aus.

So sieht Ihr momentaner Widget Baum aus.

Zuallererst haben Sie den Beobachter auskommentiert, der die Falle zuschnappen lässt (Zeile 8). Darum wollen Sie sich später kümmern, ausserdem haben Sie auf einmal Angst, Sie könnten die süsse Maus verletzen.

Dann hängen Sie das neue Mauswidget in den Widget Baum, und zwar im Startzustand :lurking (Zeile 11). Um die Maus noch in das Controllerview einzubauen, rendern Sie es mit #render_widget (Zeile 15).

app/cells/mouse/lurking.html.erb

1
<%= image_tag "mouse.png" %>

Nachdem Sie das View des :lurking Zustandes noch etwas verschönert haben, stehen sich jetzt tatsächlich die Duellisten Maus und Falle gegenüber.

Einer gegen Einen.

Einer gegen Einen.

Aufsteigende Events

Um jetzt den Käse aus der Falle zu stehlen, muss die Maus nur noch den :mouseAlarm Event abfangen und den Käse fressen. Hierzu erweitern Sie wieder den #use_widgets Block im Controller.

app/controllers/forest_controller.rb

4
5
6
7
8
9
10
11
12
  def undergrowth
    use_widgets do |root|
      trap = cell(:bear_trap, :charged, 'evil_trap')
        trap << cell(:cheese, :smelling, 'piece_of_cheese')
      root << trap
 
      root << cell(:mouse, :lurking, 'hungry_mouse')
      root.respond_to_event(:mouseAlarm, :with => :eating, :on => 'hungry_mouse')
    end

Sie hängen also einen neuen Beobachter an root (Zeile 11) und lesen sich diese Zeile einfach laut vor.

Root, falls Du einen Event vom Typ :mouseAlarm siehst, dann reagiere darauf, indem Du das Widget hungry_mouse in den :eating Zustand versetzt.“.

Sie konnten diesen Beobachter nicht direkt ans Mauswidget hängen. Warum? Na weil der :mouseAlarm Event ja von der Falle ausgelöst wird, und dann hoch zu root blubbert. Auf seinem Weg nach oben kommt er aber gar nicht bei der Maus vorbei, also muss root aushelfen.

Und endlich: Statefulness!

Sie beschliessen, statt den Käse verschwinden zu lassen, lieber die Anzahl der gefressenen Käsestücke im Mauswidget zu zählen, das mittlerweile schon ganz schön viele Zeilen Code enthält.

app/cells/mouse_cell.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MouseCell < Apotomo::StatefulWidget
  transition :from => :lurking, :to => :eating
  transition :in => :eating
 
  def lurking
    @cheese_count = 0
    render
  end
 
  def eating
    @cheese_count += 1    
    render :view => :lurking
  end
end

Es wäre doch lustig, wenn die Maus mit jedem Stückchen gefressenen Käse dicker werden würde! Dies implementieren Sie, indem Sie das View von :lurking erweitern.

app/cells/mouse/lurking.html.erb

1
<%= image_tag "mouse.png", :width => 90 * (@cheese_count+1)  %>

Die Bildbreite wird also bei jedem Rendern neu berechnet, und mit jedem gefressenen Käse wird die Maus fetter.

Fette Maus.

Fette Maus.

Also, was geht hier genau vor?

Von Zustand zu Zustand

Wenn Sie die Maus in die Falle schicken, löst das den altbekannten :mouseAlarm Event aus. Dieser wird abgefangen und schickt das hungry_mouse Widget in den :eating Zustand. Da dies ein Zustandsübergang ist, der von aussen angestossen wird (nämlich eben durch den #respond_to_event Beobachter), muss dieser Übergang auch erlaubt werden (Zeile 2).

Danach bleibt die Maus am Fressen, im Zustand :eating. Ein weiterer Event schickt das Widget also von :eating in :eating, auch das muss erlaubt sein (Zeile 3).

Käse zählen

Der Käsezähler wird im Startzustand :lurking erstmal initialisiert und auf Null gesetzt, schliesslich hat Ihr Mäuschen noch einen leeren Magen (Zeile 6).

app/cells/mouse_cell.rb

5
6
7
8
9
10
11
12
13
  def lurking
    @cheese_count = 0
    render
  end
 
  def eating
    @cheese_count += 1    
    render :view => :lurking
  end

Wie Sie schon definiert haben, bleibt die Maus danach im :eating Zustand, und dessen Zustandsmethode wird bei jedem Event erneut ausgeführt. Die Methode kümmert sich um das Erhöhen des Käsezählers (Zeile 11).

Ist dieser auf dem neusten Stand, wird die View des :lurking Zustands gerendert und eine immer fetter werdende Maus dargestellt (Zeile 12).

Statefulness und automatisches Updaten

Entzückt bemerken Sie, dass die Instanzvariable @cheese_count tatsächlich erhalten bleibt, obwohl zwischen den eigentlichen Events mehrere Requests liegen. Das sind demnach die stateful widgets, von denen dauernd die Sprache ist.

Es erübrigt sich, zu Erwähnen, dass sich jedes Widget, also auch unsere fettsüchtige Maus, automatisch selbst auf der Seite per AJAX updatet. Oder haben Sie schon eine Zeile JavaScript geschrieben, seit Sie dieses bahnbrechende Maus-Spiel entwickeln?

Miteinander kommunizierende Widgets

Eigentlich wäre es nur konsequent, dass eine fette, schwere Maus auch irgendwann mal die Falle zuschnappen lässt, da ihr Gewicht zum Zeitpunkt X ausreichend ist, den Auslöser auch wirklich zu betätigen.

Die Falle muss also irgendwie herauskriegen, ob die Maus schwer genug ist. Sie sollten den Beobachter der Falle wieder ins Spiel bringen.

app/controllers/forest_controller.rb

4
5
6
7
8
9
def undergrowth
    use_widgets do |root|
      trap = cell(:bear_trap, :charged, 'evil_trap')
        trap << cell(:cheese, :smelling, 'piece_of_cheese')
      trap.respond_to_event(:mouseAlarm, :with => :snapped)
      ...

Nicht nur die Maus, sondern nun auch noch die Falle beobachten jetzt den :mouseAlarm Event.

Ja- es ist absolut in Ordnung, mehrere Beobachter für einen Event zu haben.

Die ist noch nicht fett genug!

Die ist noch nicht fett genug!

Der Zustand :snapped der Bärenfalle sollte jetzt darüber entscheiden, ob es sich lohnt, zuzuschnappen oder doch lieber auf fettere Beute zu warten.

Wenn Sie momentan nämlich den Zeiger über die Falle bewegen, schnappt diese zu, und die Maus wird ein bisschen dicker. Die Maus soll aber richtig fett sein, damit die Falle ausgelöst wird, mindestens 3 Käsestücke muss Sie gefressen haben, entscheiden Sie.

app/cells/mouse_cell.rb

1
2
3
4
5
6
7
8
9
10
11
class BearTrapCell < Apotomo::StatefulWidget
  transition :from => :charged, :to => :snapped
 
  def charged
    render
  end
 
  def snapped
    jump_to_state :charged unless root.find_by_id('hungry_mouse').cheese_count >= 3
    render
  end

Die Bärenfalle wird also bei jedem :mouseAlarm in :snapped geschickt. Sie bleibt allerdings nur in diesem Zustand, wenn der Käsezähler der Maus wirklich grösser als 3 ist (Zeile 9).

Ansonsten springt sie mit der Methode #jump_to_state zurück nach :charged und bleibt offen für fette Beute.

Natürlich funktioniert das so noch nicht ganz, die Maus muss noch auf #cheese_count antworten.

app/cells/mouse_cell.rb

1
2
3
4
5
class MouseCell < Apotomo::StatefulWidget
  transition :from => :lurking, :to => :eating
  transition :in => :eating
 
  attr_reader :cheese_count

Jetzt wird nicht nur die Maus fetter, wenn Sie Käse aus der Falle stiehlt, auch die Falle regt sich und fragt bei der Maus nach, ob diese vielleicht zufällig schwer genug ist.

Die wird ja immer fetter!

Die wird ja immer fetter!


Und tatsächlich, es funktioniert! Beim vierten Mal schnappt die Falle zu.

Für heute reicht’s!

Schön wäre es noch, wenn der Käse und die Maus verschwinden würden. Denn der Käse ist gefressen, und die fette Maus macht sich aus dem Staube, wenn die Bärenfalle mit einem lauten „Klack!“ zuschnappt, oder haben Sie schonmal ‘ne Maus gesehen, die in eine Bärenfalle passt?

Auch stört Sie ein bisschen die Abhängigkeit, die Sie in der Bärenfalle geschaffen haben – diese weiss ja jetzt Bescheid über die hungry_mouse und befragt das Mauswidget in der #snapped Zustandsmethode, das sollte bei unabhängigen Komponenten nicht der Fall sein.

„Aber das kann man mit Apotomo bestimmt auch anders machen, doch für heute reicht es!“, sagen Sie sich lehnen sich zurück und freuen sich darüber, dass in Ihrem Spiel noch keine einzige Maus verletzt wurde.