Using blocks for callbacks
Since Ruby code is generally synchronous in nature, we don't have the same need for callbacks as we do in event-driven languages like JavaScript. However, it is still quite common to design callback-like APIs in Ruby and use blocks to implement them. Some examples include:
OptionParser's on method, which allows us to register blocks to be called when specific command-line options are encountered:
require "optparse"
options = {}
OptionParser.new do |op|
op.on("-n", "--name NAME", "Your name") do |name|
options[:name] = name
end
op.on("-a", "--age AGE", "Your age") do |age|
options[:age] = age.to_i
end
end.parse!
RSpec's before, after and around hooks, which allow us to register blocks to be called before, after or around each example:
RSpec.describe MyApp do
before do
# This block will be called before each example
setup_test_data
end
after do
# This block will be called after each example
cleanup_test_data
end
around do |example|
# This block will be called around each example
MyApp.silence_logs do
example.run
end
end
it "does something" do
# ...
end
it "does something else" do
# ...
end
end
The Glimmer DSL for building desktop applications, which uses blocks both to define the structure and input events of the UI:
require "glimmer-dsl-libui"
include Glimmer
window("Callbacks", 320, 220) {
vertical_box {
horizontal_box {
stretchy false
@status = label("Move or type in the blue area")
button("Click me") {
stretchy false
on_clicked { @status.text = "button clicked" }
}
}
area {
on_draw do |p|
rectangle(0, 0, p[:area_width], p[:area_height]) {
fill r: 225, g: 240, b: 255
}
end
on_mouse_moved { |e| @status.text = "moved to #{e[:x]}, #{e[:y]}" }
on_mouse_down { |e| @status.text = "mouse down: #{e[:button]}" }
on_mouse_up { |e| @status.text = "mouse up: #{e[:button]}" }
on_key_down { |e| @status.text = "key down: #{e[:key]}" }
on_key_up { |e| @status.text = "key up: #{e[:key]}" }
}
}
}.show
Feel free to gem install glimmer-dsl-libui and run the above code to see it in action BTW!
Make your own
To implement this kind of API, we can receive the blocks as arguments via &block and store them in instance variables to be called later when the relevant event occurs. For example, we could implement a simple API allowing us to register before hooks like this:
class TestSuite
def initialize
@before_hooks = []
end
def before(&block)
@before_hooks << block
end
def run_example(example)
@before_hooks.each(&:call)
example.call
end
end
Easy peasy!