-
Notifications
You must be signed in to change notification settings - Fork 18
The Structure of a Hyperloop Application
unless you want it to be.
Hyperloop is a collection of Ruby classes packaged in several gems so that you can quickly (and we mean quickly) create great applications. These classes allow you to structure your code so that it's reusable, maintainable, and best of all short and sweet. If you want to call that a framework, that is okay, but its very useful also think about how each of the Hyperloop elements works as an independent part of the application. That way you can pick and choose how and when to use each piece of Hyperloop to your best advantage.
So without further ado, let's get stuck into the Structure of a Hyperloop Application.
Sitting at the base of your Application are one or more Stores. This is the same "Store" that Flux talks about. In Hyperloop, Stores are created by subclassing the HyperStore
base class.
A HyperStore is just like any other Ruby class but with a very special feature: reactive instance variables or state variables, or simply state.
The state literally holds the state of your application. State variables work just like regular Ruby instance variables, except that they intelligently inform any of your Components (we will get to them next) that they will need to rerender when the state changes.
Here is a very simple Store which keeps a list of Words:
class Word < HyperStore::Base
private_state list: [], scope: :class
def self.add!(word)
state.list! << word
end
def self.all
state.list
end
end
The declaration private_state list: [], scope: :class
creates a state variable called list, and initializes it to an empty array. The scope: :class
option indicates that the state will be associated with the class (instead of having a separate state variable for every instance of the class.)
The class method add!
will push a new word onto the list. We tell HyperStore that we are acting on the list by appending the exclamation to the state name. Likewise, so users of our Word store will know that the add
method is an action we by convention add an exclamation to its name as well.
To see our words we have the all
method. Notice that in this case we are only reading the state so there is no !
either on our method name, or when we access the state.
Besides private_state
you can also declare a state_reader
which will build the reader method using the states name. For example if we could have said state_reader all...
and would automatically get the all
method declared. However, we would have to then change our code to use state.all
(instead of state.list
), and I wouldn't have had this nice teaching moment with you.
Except for the receives
method which will get to later under Actions
that is all there is to know about HyperStore. The rest is plain old Ruby code. You can create Stores as simple as the one above or as complex as one that keeps a pool of random users available for display like this example.
Now that we have some state tucked away we will want to display it. In Hyperloop you create the UI with Components which are Ruby classes wrapping React.js components. Let's make a simple component to display our Words, and allow the user to add more
class ShowWords < React::Component::Base
render(DIV) do
INPUT(placeholder: 'enter a new word, and hit enter', style: {width: 200})
.on(:key_down) do |evt|
if evt.key_code == 13
Word.add! evt.target.value
evt.target.value = ''
end
end
UL do
Word.all.each { |word| LI { word } }
end
end
end
Our component definition is straight forward. The ShowWords
component renders a DIV
which contains an input box, and a list of words.
The input box has a handler for the key_down
event. If the key pressed is the enter key, we send the new word to Word's add!
action method, and clear the input.
To show the list of words we use Word's all
method and display each word in the list.
As new words are added, our component will rerender because the contents of all
will have changed.
Hyperloop Components have all the features of normal React components and more. For the complete description of the DSL (domain specific language) and other details see the documentation.
So far in our example, we have defined the add!
action directly in the Store. It is sometimes useful to structure the actions as separate objects.
- Actions defined as separate objects allow Components to be completely separate from the Store being updated, which can facilitate reuse.
- Actions defined as separate objects can access multiple stores, and do more complex tasks.
In the classic Flux pattern, all actions are defined as separate entities. Hyperloop is much less opinionated. In cases where the action is clearly associated with a specific Store, then go right ahead and define it as an action method in the store. If on the other hand the Action has a general meaning which can be described without reference to a particular store or state, then it probably should be created as a separate entity.
Let's add a ClearAll
action to our growing application. The meaning is for any store to reset itself to its initial state.
First we define the Action:
class ClearAll < HyperAction
end
Then let's have our Word store listen for it and empty the list:
class Word < HyperStore::Base
receives ClearAll { state.list! [] }
end
Finally, we will create a new component that uses our ShowWords component and adds a 'Reset' button:
class App < React::Component::Base
render(DIV) do
ShowWords()
BUTTON { 'Reset' }.on(:click) { ClearAll() }
end
end
As I hope is clear, the ClearAll
(no pun intended) Action breaks the coupling between the App component and the Word store. If we added another store that needed to respond to the ClearAll
action only that store's code would change.
Actions can have parameters which are defined using the param
macro. For example if we wanted to define our Word.add!
action as an Action class we would say:
class AddWord < HyperAction
param :word
end
and send it saying:
AddWord(word: 'hello')
and receive it like this:
class Word < HyperStore::Base receives AddWord { |word| state.list! << word } end
Every Action has an execute method. By default this method dispatches the action to all the registered receivers. You can add your own logic by redefining the execute method in the subclass:
class ClearAll < HyperAction
def execute # add some console debugging before we dispatch
puts "ClearAll called at #{Time.now}"
super # call super to run the base dispatcher
end
end
As necessary the Action's execute method can orchestrate the business logic and external communications that occur between events and the stores. This concept is taken directly from the Trailblazer Operation class
For example:
class AddRandomWord < HyperAction
def execute
HTTP.get("http://randomword.setgetgo.com/get.php", dataType: :jsonp) do |response|
AddWord(word: response.json[:Word])
end
end
end