-
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 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 web site:
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, type: WorldClock
render(DIV) do
"The time in #{params.clock.name} is #{params.clock.current_time}"
end
end
class App < React::Component::Base
render(DIV) do
DisplayTime clock: WorldClock.new('New York', 40.7128, -74.0059, 5.hours)
DisplayTime clock: WorldClock.new('London', 51.5074, -0.1278, 0)
end
end
It is easy to bind user input events in Components. For example, let's allow the user to select from different cities and add those WorldClocks to the UI dynamically.
First, we will need a simple static class to return a sample list of locations:
class Location
LOCATIONS = {
'New York' => [40.7128, -74.0059, 5.hours],
'London' => [51.5074, -0.1278, 0],
'Mumbai' => [19.0760, 72.8777, 5.hours+30.minutes],
'Bangkok' => [13.7563, 100.5018, 7.hours],
'Sydney' => [-33.8688, 151.2093, 11.hours],
'San Francisco' => [37.7749, 122.4194, -8],
'Buenos Aires' => [-34.6037, -58.3816, -3]
]
def self.all
LOCATIONS
end
end
Now let's define our application:
class App < React::Component::Base
def remaining_locations
Location.all.keys.select do |location|
WordClock.all.detect { |clock| clock.name == location }
end
end
def add_clock(name)
WorldClock.new name, *Locations.all[name]
end
def build_select
SELECT do
remaining_locations do |name|
OPTION(value: name) { name }
end
end.on(:change) { |e| add_clock e.value }
end
render(DIV)
build_select unless @locations.empty?
WorldClock.all.each { |clock| DisplayTime clock: clock }
end
end
When we generate the SELECT we bind the change event to the block
{ |e| add_clock e.value }
which will remove the selected clock from the @locations
list, and create a new clock. WorldClock's all
method will be updated causing a re-render with the new clock being displayed.
Notice how are component is tangled up massaging the city data into a WorldClock. We will be able to abstract a lot of that work out to an Operation. You will often see event handlers invoking a single Operation that exists separate from both the component and the store.