-
Notifications
You must be signed in to change notification settings - Fork 18
Component Anatomy
At the core of a Hyperloop application are components. These are your views, and instead of being written in ERB, HAML, JSX, Mustache, Dirty Underwear or some other templating language, they are written as Ruby classes which build React components.
For example here is a simple component that displays a user with their avatar and a link to their website:
class User < React::Component::Base
param :name
param :avatar
param :website
def url(s)
s =~ /^http:/ ? s : "http://#{s}"
end
render(DIV) do
IMG(src: url(params.avatar))
A(class: :username, target: :_blank, href: url(params.website)) { params.name }
end
end
To display a user we would say:
User(name: "Hyperloop", avatar: "goo.gl/epMF6e", website: "ruby-hyperloop.io")
The DSL defines methods like DIV
, IMG
, and A
for all the HTML tags, and then your app defines other components (like User
) and helper methods (like url
.) You put this all together in a render block which can use if
, each
and any other Ruby construct to build your HTML.
Note that its easy to read the DSL: All uppercase names are built-in tags, CamelCase names are application defined components, and snake_case names are methods and ruby constructs.
Using a Store in your component is simple Ruby code. We will use our WorldClock store in a couple of components to display the current time for London and New York.
class DisplayTime < React::Component::Base
param :clock
param :format # time format string
render(DIV) do
"The time in #{params.clock.name} is "\
"#{params.clock.current_time % params.format}"
end
end
class App < React::Component::Base
FORMATS = {
12 => "%a, %e %b %Y %l:%M:%S %p",
24 => "%a, %e %b %Y %H:%M:%S"
}
CLOCKS = [
WorldClock.new('New York', 40.7128, -74.0059, 5.hours),
WorldClock.new('London', 51.5074, -0.1278, 0)
]
render(DIV) do
CLOCKS.each do |clock|
DisplayTime format: FORMATS[12], clock: clock
end
end
end
Our component is very happy because we have full access to all of Ruby's great style. We put our clocks into a constant array and then iterate through that array. We are not sure whether we should display the clock in 12 or 24-hour time, so we set up both formats and for now hard code our app to use the 12-hour (AM/PM) format.
It is easy to bind user input events in Components. We will provide a button to change the time format from 12 to 24-hour clock.
We want to present a SELECT drop down with the choices 12 and 24. When the user changes the SELECT we want to change the format.
Any time we respond to a user event we are going to change state somewhere in the system. For now, we are going to keep a private state called format
in our App component. Like Stores, Components can also declare and use state variables. Like any state variable, as it changes any components depending on the current state of the variable will be updated.
class App < React::Component::Base
FORMATS = {
12 => "%a, %e %b %Y %I:%M:%S %p",
24 => "%a, %e %b %Y %H:%M:%S"
}
CLOCKS = [
WorldClock.new('New York', 40.7128, -74.0059, 5.hours),
WorldClock.new('London', 51.5074, -0.1278, 0)
]
private_state format: FORMATS[12] # initial format to 12-hour
render(DIV) do
CLOCKS.each do |clock|
DisplayTime format: state.format, clock: clock
end
SELECT do
FORMATS.each { |name, format| OPTION(value: format) { value } }
end.on(:change) { |e| state.format! = e.value }
end
end
Whenever the SELECT changes the
{ |e| state.format! = e.value }
block is run. This updates the format state variable which will cause a re-render of both clock components.
Again the key to understanding user events is that they always update state.