This history should be edited, and inserted somewhere in the docs. I apologize for the rather stilted 3rd person narrative, hopefully, once it's integrated into the docs this will read a bit less narcissistically.
Please update if I have missed any significant people or events.
In February of 2015, David Chang (@zetachang) started building a DSL wrapper for React.js using the Opal Ruby to JS transpiler. At this time the gem was called React.rb
Back in 2014 Mitch VanDuyn (@catmando) started experimenting with Opal as a way to simplify client-side development, by focusing on a single language. Some successful initial improvements to the CatPrint.com website were done in straight Opal, but it was clear that some kind of framework would be useful.
After experiments with other solutions, @catmando stumbled on React.rb and started replacing some of the straight Opal code with React components written in Ruby. Here is one piece of code that remains close to its original form on the catprint website (click on the Chat button to see it in action.)
module Components
class Chat < React::Component
POLL_INTERVAL = 5 * 60
param online_text: nil
param offline_text: nil
param inner_elements: nil
param name: ''
param email: ''
state :online, scope: :shared, reader: true
after_mount do
Chat.start_polling
end
class << self
def start_polling
unless @started
check_status
every(POLL_INTERVAL) { check_status(false) }
@started = true
end
end
def open(opts = {}, &failure)
check_status do |current_online_state|
if current_online_state
chat_params = []
unless opts[:email].strip.blank?
chat_params << "interaction[email]=#{opts.delete(:email)}"
end
unless opts[:name].strip.blank?
chat_params << "interaction[name]=#{opts.delete(:name)}"
end
url =
"http://support.catprint.com:80/customer/widget/chats/new?#{chat_params.join('&')}"
opts = { resizable: 1, status: 0, toolbar: 0, width: 640, height: 700 }.merge opts
opts_string = opts.collect { |k, v| "#{k}=#{v}" }.join(', ')
`window.open(#{url}, 'assistly_chat', #{opts_string});`
elsif failure
yield failure
end
end
end
def check_status(force_update = true)
HTTP.get("#{configuration[:desk_online_agents_url]}#{'&force_update=true' if force_update}")
.then do |response|
new_online_state =
JSON.parse(response.body[1..-2])[:routing_agents].to_i > 0 rescue nil
yield new_online_state if block_given?
Chat.online! new_online_state if Chat.online ^ new_online_state
end
end
end
render do
if Chat.online
A do
SPAN { params.online_text }
if params.inner_elements
SPAN(class: params.inner_elements[:classes]) { params.inner_elements[:text] }
end
end.on(:click) do
Chat.open(email: params.email, name: params.name) { alert('no chat right now') }
end
elsif params.offline_text
A(class: 'chat-disabled') do
SPAN { params.offline_text }
if params.inner_elements
SPAN(class: params.inner_elements[:classes]) { params.inner_elements[:text] }
end
end
end
end
end
end
In React.js there is no built-in way to for components to be notified of global event changes. Typically React based frameworks develop a subscription-notification mechanism, where components subscribe to a central store's data change event and uses the notification to update internal state in the component. Based on @ryanstout's Voltcomputations @catmando added in 2015 a central state manager that allows components to automatically and dynamically subscribe and unsubscribe to each other's state changes. Because this concept is so fundamental to what makes Hyperloop special, its worth a bit of detailed explanation:
class Clicker < Hyperloop::Component
state clicks: 0, scope: :shared, reader: true
# there is a single click state shared by all instances, that
# is initialized to zero, and has a class level reader method (called clicks)
state :maxed_out, scope: :shared, reader: true
# same store for maxed_out, but its initial value is the default - nil.
MAXCLICKS = 5
def click
mutate.clicks state.clicks+1
mutate.maxed_out true if state.clicks >= Clicker::MAXCLICKS
end
render do
if state.maxed_out
DIV { "No More Clicks For You" }
else
BUTTON { "Click Me" }.on(:click, &:click)
end
end
end
class DisplayClicks < Hyperloop::Component
render do
if Clicker.maxed_out # read the maxed_out state, and remember that we care
"#{Clicker::MAXCLICKS} Clicks"
elsif Clicker.clicks.zero? # if execution gets to here we will also care about clicks
"No clicks yet"
elsif Clicker.clicks == 1
"1 Click"
else
"#{Clicker.clicks} Clicks"
end
end
end
This example while contrived does show that as DisplayClicks renders it always interrogates Clicker's maxed_out state. If maxed_out is falsly then Clicker's clicks state is also read. When a "global" state like this is read the Hyperloop state manager remembers that the component's rendering is dependent on that state's value, and should it change the component must be re-rendered. Each time through the rendering process a new set of dependencies will be recorded. The result is a component automatically and dynamically subscribes to state changes as needed.
Meanwhile, the big problem that Catprint.com faced was a huge legacy base of AR models, that interacted with the user via a combination of classic page loads, and ad-hoc ajax code. In order to rewrite the UI in React.rb, a complete set of API's, client-side stores, and some kind of flux loop would have to be built.
Out of this was born the HyperModel concept of compiling the AR models into client-side proxies using Opal, and directly accessing them in the React Components. Hence the react component acts very much like a rails view, where the view code accesses AR data directly and renders it to the view. Using the automatic subscription notification components could be updated as data in the AR models arrived from the server or were updated by local actions on the client.
The first iteration of this concept was called ReactiveRecord. Later when the ability to handle push synchronization from the server was added, the name was changed to Synchromesh. Synchromesh uses a combination of Channels and Policies to describe what data will get pushed to who. Channels are simply a class or individual user like objects. For example a User, a Team, all Administrators, etc. Each client will be subscribed to a number of channels based on the current logged in User. Then each Model describes what data changes a user may see based on Policies.
Besides work on ReactiveRecord and Synchromesh during this period there were a number of other significant contributors especially Adam Jahn (@ajjahn) who did a lot of code clean up. @zetachang had to drop out of the project for 2 years for personal reasons, and @catmando became the lead. React.rb went through a number of name changes, before settling on Hyperloop, a name proposed by Barrie Hadfield (@barriehadfield). Synchromesh was then renamed to Hypermesh, and React.rb ended up becoming HyperComponent.
BTW currently many of these gem names are simply wrappers around the original Gem names, as we have just not had time to do the full rename of all the internal code. So you will see names like Synchromesh still floating around.
Meanwhile, the Catprint team was working very hard on their V4 website upgrade, which was to be done entirely in Opal using React.rb. As bugs or features were found, these were added the base set of Hyperloop gems.
@barriehadfield introduced the idea of Trailblazer operations and during 2016 there was a lot of discussion on how to correctly incorporate these concepts and use ideas like the Flux loop. It was realized that by adding a broadcast capability to Operations, they became a perfect way to encode the Flux Action and Dispatcher concepts. Then it was realized that there was no reason an Operation could not run (if needed) on the Server, but the broadcast is made to all clients. So Operations became both a way to structure application code on the client or server, across client and server, and also became the underlying primitive for all hyperloop client-server communication. Hypermesh was refactored so it used Operations, and was renamed to HyperModel.
Using Operations the above Chat component could be simplified and clarified. In the current legacy implementation, the server side code is opaque and is hidden behind an API endpoint, with Operations we would simply write the methods we needed executed on the server, and call them in the Chat Component.
Having global states (like shown in the above Clicker example) did not provide a good separation of concerns, and that is why in 2017 state management was moved to a separate HyperStore gem so that states could be defined separately from components.
In 2016 @loicboutet built the hyperloop rails generator that would install hyperloop config files into a rails app, and provide a generator for a skeleton component. This made building a new Hyperloop App or integrating in a brownfield rails app became a lot easier.
Starting in late 2016 @barriehadfield and @fzingg have worked tirelessly on pulling together the documentation site.
Throughout 2016 @catmando experimented with how to test Hyperloop code. Hyperloop had now evolved to be a Full Stack Single Language framework. It encompassed both client and server code and made the client-server barrier as transparent as possible. In order to test a component it seemed that you needed full stack test helpers that would allow a single test spec on the server to test the component as it would behave within the stack. A set of ad-hoc spec helpers were developed both for testing the CatPrint application and the evolving set of gems, tutorials and examples. For instance here is a spec for a Bleed component that displays the value of the Bleed Option (aka Print to Edge) on the CatPrint website.
require 'spec_helper'
describe 'Bleed component', js: true do
before(:each) do
client_option layout: 'test'
size_window(:large)
end
it 'displays No bleed option selected (in danger-text) if no bleed is selected' do
mount 'Bleed', bleed: nil
page.should have_selector('.danger-text', text: 'No bleed option selected')
end
it 'displays Full bleed if Bleed Option is Yes' do
mount 'Bleed', bleed: FactoryGirl.create(:full_bleed_option, name: 'Yes')
page.should_not have_content('No bleed option selected', wait: 0)
page.should have_content('Full Bleed', wait: 0)
end
it 'displays No bleed if Bleed Option is No' do
mount 'Bleed', bleed: FactoryGirl.create(:full_bleed_option, name: 'No')
page.should_not have_content('No bleed option selected', wait: 0)
page.should have_content('No Bleed', wait: 0)
end
it 'adds cursor pointer to danger text if No bleed option selected' do
mount 'Bleed', bleed: nil
page.find('.danger-text')['outerHTML'].should include('cursor: pointer')
end
it 'raises the clear error event if the user clicks on danger text' do
mount 'Bleed', bleed: nil
page.find('.danger-text').click
event_history_for(:clear_error).length.should eq(1)
end
end
The mount method is one of a couple of specialized helpers that work across the client and server. Thus allowing the test to run isomorphically. A criticism of this approach made by @ajjahn was that it does not allow white-box testing of the components, so mount was extended to allow blocks of client code to be inserted during the test. Here is an example of the Chat dialog component:
it 'switches from offline to online' do
mount('Chat', online_text: 'ONLINE', offline_text: 'OFFLINE') do
module Components
class Chat < React::Component::Base
POLL_INTERVAL = 1 # override default of 5 minutes
end
end
end
page.should have_content('OFFLINE')
wait_for_ajax
@agents_online = 2
Rails.cache.delete('desk_agents_online')
page.should have_content('ONLINE')
end
Any code in the mount block is re-compiled via Opal and attached to the client code, thus allowing the internal behavior of client side code to be modified.
@barriehadfield has provided a lot of focus on how to integrate with Webpack. At the moment with a little configuration setup, you can use NPM / webpack to seamlessly manage native JS assets, and rails sprockets to manage ruby code (including hyperloop assets.)
In the middle of 2017 the CatPrint team launched the new V4 website written entirely in Ruby using Hyperloop. Instead of a 70K line mix of JS, HTML, ERB, Coffeescript, and some HAML, the much more functional website is written in 25K of Ruby.
During 2017 a number of breaking API changes primarily to HyperComponents with the intent to normalize the syntax, and eliminate common errors. To accommodate models, components, operations and stores, the location of all hyperloop code was moved to be under the app/hyperloop directory. Our goal is to lock the API as it exists now for the Hyperloop 1.0 release.
Also during 2017 other gems were added or consolidated out of various snippets like HyperConsole, and HyperSpec. Also, @adamcreekroad wrapped the latest React-Router in a Ruby DSL.
In late 2017 @janbiedermann has been working through a number of performance iand configuration issues.
Another key contributor that deserves mention is @fkchang (no relation to David) who was an early supporter of Hyperloop and has incorporated it into his Opal Playground.
The current structure of the Hyperloop team is just a small gang of core contributors. Pull Requests are welcome from all, and if someone shows sufficient interest in hyperloop via PRs, they are invited the core team.
We try to keep the code in code shape through an extensive set of tests (just under 1000 at last count) and hopefully with the release of 1.0 everything will be running through a CI service.
In general, except for very sensitive issues, the Core Team has decided to keep all discussions in the public Gitter forum. Lengthy discussions are moved off the forum and onto a specific issue.
Currently, we are working towards a 1.0 release with a number of release candidates (called laps for hopefully obvious reasons.) Our goal is to give the world a nice Christmas present, although we may be on the old Russian calendar
This history should be edited, and inserted somewhere in the docs. I apologize for the rather stilted 3rd person narrative, hopefully, once it's integrated into the docs this will read a bit less narcissistically.
Please update if I have missed any significant people or events.
In February of 2015, David Chang (@zetachang) started building a DSL wrapper for React.js using the Opal Ruby to JS transpiler. At this time the gem was called React.rb
Back in 2014 Mitch VanDuyn (@catmando) started experimenting with Opal as a way to simplify client-side development, by focusing on a single language. Some successful initial improvements to the CatPrint.com website were done in straight Opal, but it was clear that some kind of framework would be useful.
After experiments with other solutions, @catmando stumbled on React.rb and started replacing some of the straight Opal code with React components written in Ruby. Here is one piece of code that remains close to its original form on the catprint website (click on the Chat button to see it in action.)
In React.js there is no built-in way to for components to be notified of global event changes. Typically React based frameworks develop a subscription-notification mechanism, where components subscribe to a central store's data change event and uses the notification to update internal state in the component. Based on @ryanstout's Volt computations @catmando added in 2015 a central state manager that allows components to automatically and dynamically subscribe and unsubscribe to each other's state changes. Because this concept is so fundamental to what makes Hyperloop special, its worth a bit of detailed explanation:
This example while contrived does show that as
DisplayClicks
renders it always interrogates Clicker'smaxed_out
state. Ifmaxed_out
is falsly then Clicker'sclicks
state is also read. When a "global" state like this is read the Hyperloop state manager remembers that the component's rendering is dependent on that state's value, and should it change the component must be re-rendered. Each time through the rendering process a new set of dependencies will be recorded. The result is a component automatically and dynamically subscribes to state changes as needed.Meanwhile, the big problem that Catprint.com faced was a huge legacy base of AR models, that interacted with the user via a combination of classic page loads, and ad-hoc ajax code. In order to rewrite the UI in React.rb, a complete set of API's, client-side stores, and some kind of flux loop would have to be built.
Out of this was born the HyperModel concept of compiling the AR models into client-side proxies using Opal, and directly accessing them in the React Components. Hence the react component acts very much like a rails view, where the view code accesses AR data directly and renders it to the view. Using the automatic subscription notification components could be updated as data in the AR models arrived from the server or were updated by local actions on the client.
The first iteration of this concept was called ReactiveRecord. Later when the ability to handle push synchronization from the server was added, the name was changed to Synchromesh. Synchromesh uses a combination of Channels and Policies to describe what data will get pushed to who. Channels are simply a class or individual user like objects. For example a User, a Team, all Administrators, etc. Each client will be subscribed to a number of channels based on the current logged in User. Then each Model describes what data changes a user may see based on Policies.
Besides work on ReactiveRecord and Synchromesh during this period there were a number of other significant contributors especially Adam Jahn (@ajjahn) who did a lot of code clean up. @zetachang had to drop out of the project for 2 years for personal reasons, and @catmando became the lead. React.rb went through a number of name changes, before settling on Hyperloop, a name proposed by Barrie Hadfield (@barriehadfield). Synchromesh was then renamed to Hypermesh, and React.rb ended up becoming HyperComponent.
BTW currently many of these gem names are simply wrappers around the original Gem names, as we have just not had time to do the full rename of all the internal code. So you will see names like Synchromesh still floating around.
Meanwhile, the Catprint team was working very hard on their V4 website upgrade, which was to be done entirely in Opal using React.rb. As bugs or features were found, these were added the base set of Hyperloop gems.
@barriehadfield introduced the idea of Trailblazer operations and during 2016 there was a lot of discussion on how to correctly incorporate these concepts and use ideas like the Flux loop. It was realized that by adding a broadcast capability to Operations, they became a perfect way to encode the Flux Action and Dispatcher concepts. Then it was realized that there was no reason an Operation could not run (if needed) on the Server, but the broadcast is made to all clients. So Operations became both a way to structure application code on the client or server, across client and server, and also became the underlying primitive for all hyperloop client-server communication. Hypermesh was refactored so it used Operations, and was renamed to HyperModel.
Using Operations the above Chat component could be simplified and clarified. In the current legacy implementation, the server side code is opaque and is hidden behind an API endpoint, with Operations we would simply write the methods we needed executed on the server, and call them in the Chat Component.
Having global states (like shown in the above
Clicker
example) did not provide a good separation of concerns, and that is why in 2017 state management was moved to a separate HyperStore gem so that states could be defined separately from components.In 2016 @loicboutet built the hyperloop rails generator that would install hyperloop config files into a rails app, and provide a generator for a skeleton component. This made building a new Hyperloop App or integrating in a brownfield rails app became a lot easier.
Starting in late 2016 @barriehadfield and @fzingg have worked tirelessly on pulling together the documentation site.
Throughout 2016 @catmando experimented with how to test Hyperloop code. Hyperloop had now evolved to be a Full Stack Single Language framework. It encompassed both client and server code and made the client-server barrier as transparent as possible. In order to test a component it seemed that you needed full stack test helpers that would allow a single test spec on the server to test the component as it would behave within the stack. A set of ad-hoc spec helpers were developed both for testing the CatPrint application and the evolving set of gems, tutorials and examples. For instance here is a spec for a
Bleed
component that displays the value of the Bleed Option (aka Print to Edge) on the CatPrint website.The
mount
method is one of a couple of specialized helpers that work across the client and server. Thus allowing the test to run isomorphically. A criticism of this approach made by @ajjahn was that it does not allow white-box testing of the components, somount
was extended to allow blocks of client code to be inserted during the test. Here is an example of the Chat dialog component:Any code in the
mount
block is re-compiled via Opal and attached to the client code, thus allowing the internal behavior of client side code to be modified.@barriehadfield has provided a lot of focus on how to integrate with Webpack. At the moment with a little configuration setup, you can use NPM / webpack to seamlessly manage native JS assets, and rails sprockets to manage ruby code (including hyperloop assets.)
In the middle of 2017 the CatPrint team launched the new V4 website written entirely in Ruby using Hyperloop. Instead of a 70K line mix of JS, HTML, ERB, Coffeescript, and some HAML, the much more functional website is written in 25K of Ruby.
During 2017 a number of breaking API changes primarily to HyperComponents with the intent to normalize the syntax, and eliminate common errors. To accommodate models, components, operations and stores, the location of all hyperloop code was moved to be under the app/hyperloop directory. Our goal is to lock the API as it exists now for the Hyperloop 1.0 release.
Also during 2017 other gems were added or consolidated out of various snippets like HyperConsole, and HyperSpec. Also, @adamcreekroad wrapped the latest React-Router in a Ruby DSL.
In late 2017 @janbiedermann has been working through a number of performance iand configuration issues.
Another key contributor that deserves mention is @fkchang (no relation to David) who was an early supporter of Hyperloop and has incorporated it into his Opal Playground.
The current structure of the Hyperloop team is just a small gang of core contributors. Pull Requests are welcome from all, and if someone shows sufficient interest in hyperloop via PRs, they are invited the core team.
We try to keep the code in code shape through an extensive set of tests (just under 1000 at last count) and hopefully with the release of 1.0 everything will be running through a CI service.
In general, except for very sensitive issues, the Core Team has decided to keep all discussions in the public Gitter forum. Lengthy discussions are moved off the forum and onto a specific issue.
Currently, we are working towards a 1.0 release with a number of release candidates (called laps for hopefully obvious reasons.) Our goal is to give the world a nice Christmas present, although we may be on the old Russian calendar