Skip to content
This repository has been archived by the owner on Oct 19, 2018. It is now read-only.

The Structure of a Hyperloop Application

Mitch VanDuyn edited this page Jan 21, 2017 · 5 revisions

Hyperloop is not a Framework...

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, using just Ruby. These classes allow you to structure your code so that it's debuggable, reusable, maintainable, and best of all short and sweet. If you want to call that a framework, that is okay, but it's also useful to 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.

Stores

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: they have reactive instance variables or state variables, or simply state.

State variables literally hold 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 re-render 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, by convention we 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.

Except for the receives method which will get to later under HyperActions that is all you have 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.

Components

Now that we have some state tucked away we will want to display it. In Hyperloop you create your views with Components which are Ruby classes that wrap React.js components. Let's make a simple component to display our Words, and allow the user to add more words to the list.

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|
      next unless evt.key_code == 13
      Word.add! evt.target.value
      evt.target.value = ''
    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. We ignore all keys except the enter key. 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 re-render 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.

HyperAction

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

I hope all this 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.

Action Parameters

Actions can have parameters which are defined using the param macro. 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

The Action execute method

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
Clone this wiki locally