Getting Started

This is a tutorial of making an Ovto app. We create a static app (.html + .js) here, but you can embed Ovto apps into a Rails or Sinatra app (See ./examples/*).

This is the final Ruby code.

require 'ovto'

class MyApp < Ovto::App
  class State < Ovto::State
    item :celsius, default: 0

    def fahrenheit
      (celsius * 9 / 5.0) + 32
    end
  end

  class Actions < Ovto::Actions
    def set_celsius(value:)
      return {celsius: value}
    end

    def set_fahrenheit(value:)
      new_celsius = (value - 32) * 5 / 9.0
      return {celsius: new_celsius}
    end
  end

  class MainComponent < Ovto::Component
    def render
      o 'div' do
        o 'span', 'Celcius:'
        o 'input', {
          type: 'text',
          onchange: ->(e){ actions.set_celsius(value: e.target.value.to_i) },
          value: state.celsius
        }
        o 'span', 'Fahrenheit:'
        o 'input', {
          type: 'text',
          onchange: ->(e){ actions.set_fahrenheit(value: e.target.value.to_i) },
          value: state.fahrenheit
        }
      end
    end
  end
end

MyApp.run(id: 'ovto')

Let's take a look step-by-step.

Prerequisites

  • Ruby
  • Bundler (gem install bundler)

Setup

Make a Gemfile:

source "https://rubygems.org"
gem "ovto", github: 'yhara/ovto'  # Use git master because ovto gem is not released yet
gem 'rake'

Run bundle install.

HTML

Make an index.html:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <script type='text/javascript' src='app.js'></script>
  </head>
  <body>
    <div id='ovto'></div>
    <div id='ovto-debug'></div>
  </body>
</html>

Write code

app.rb:

require 'ovto'

class MyApp < Ovto::App
  class State < Ovto::State
  end

  class Actions < Ovto::Actions
  end

  class MainComponent < Ovto::Component
    def render           # Don't miss the `:`. This is not a typo but
      o 'div' do         # a "mandatory keyword argument".
        o 'h1', "HELLO"  # All of the Ovto methods take keyword arguments.
      end
    end
  end
end

MyApp.run(id: 'ovto')
  • The name MyApp is arbitrary.
  • The id ovto corresponds to the div tag in index.html.

Compile

Generate app.js from app.rb.

$ bundle exec opal -c -g ovto app.rb > app.js

(Compile will fail if there is a syntax error in your app.rb.)

Now you can run your app by opening index.html in your browser.

Trouble shooting

If you see HELLO, the setup is done. Otherwise, check the developer console and you should see some error messages there.

For example if you misspelled class State to class Stat, you will see:

app.js:5022 Uncaught $NameErrorĀ {name: "State", message: "uninitialized constant MyApp::State", stack: "State: uninitialized constant MyApp::State"}

because an Ovto app must have a State class in its namespace.

(Tips: auto-compile)

If you get tired to run bundle exec opal manually, try ifchanged gem:

  1. Add gem "ifchanged" to Gemfile
  2. bundle install
  3. bundle exec ifchanged ./app.rb --do 'bundle exec opal -c -g ovto app.rb > app.js'

Now you just edit and save app.rb and it runs opal -c for you.

Add some state

In this tutorial, we make an app that convers Celsius and Fahrenheit degrees to each other. First, add an item to MyApp::State.

  class State < Ovto::State
    item :celsius, default: 0
  end

Now an item celsius is added to the global app state. Its value is 0 when the app starts. You can read this value by state.celsius. Let's display the value with MyApp::MainComponent.

  class MainComponent < Ovto::Component
    def render
      o 'div' do
        o 'span', 'Celcius:'
        o 'input', type: 'text', value: state.celsius
      end
    end
  end

Now you should see Celsius: [0 ] in the browser.

Add a method to State

Next, we want to know what degree is it in Fahrenheit. Let's add a method to convert.

  class State < Ovto::State
    item :celsius, default: 0

    def fahrenheit
      (celsius * 9 / 5.0) + 32
    end
  end

Now you can know the value by state.fahrenheit. Update MainComponent to show the value too.

  class MainComponent < Ovto::Component
    def render
      o 'div' do
        o 'span', 'Celcius:'
        o 'input', type: 'text', value: state.celsius
        o 'span', 'Fahrenheit:'
        o 'input', type: 'text', value: state.fahrenheit
      end
    end
  end

Add an action

Now we know 0 degrees Celsius is 32 degrees Fahrenheit. But how about 10 degrees or 100 degrees Celsius? Let's update the app to we can specify a Celsius value.

You may think that you can change the value with state.celsius = 100, but this is prohibited. In Ovto, you can only modify app state with Actions.

Our first action looks like this. An action is a method defined in MyApp::Actions. It takes an old state (and its own parameters) and returns a Hash that describes the updates to the state. This return value is merged into the global app state.

  class Actions < Ovto::Actions
    def set_celsius(value:)
      return {celsius: value}
    end
  end

This action can be called by actions.set_celsius from MainComponent. Replace the first input tag with this:

        o 'input', {
          type: 'text',
          onchange: ->(e){ actions.set_celsius(value: e.target.value.to_i) },
          value: state.celsius
        }

onchange: is a special attribute that takes an event handler as its value. The argument e is an instance of Opal::Native and wraps the event object of JavaScript. In this case you can get the input string by e.target.value.

Now reload your browser and input 100 to the left input box. Next, press Tab key (or click somewhere in the page) to commit the value. Then you should see 212 in the right input box. 100 degrees Celsius is 212 degrees Fahrenheit!

What has happend

In case you are curious, here is what happens when you give 100 to the input box.

  1. JavaScript's onchange event is executed.
  2. Ovto calls the event handler.
  3. It calls actions.set_celsius. actions is an instance of Ovto::WiredActions. It is a proxy to the MyApp::Actions. It has the same methods as those in MyApp::Actions but does some more:
  • It passes state to the user-defined action.
  • It merges the result to the global app state.
  • It schedules re-rendering the view to represent the new state.

Reverse conversion

It is easy to update the app to support Fahrenheit-to-Celsius conversion. The second input should be updated to:

        o 'input', {
          type: 'text',
          onchange: ->(e){ actions.set_fahrenheit(value: e.target.value.to_i) },
          value: state.fahrenheit
        }

Then add an action set_fahrenheit to MyApp::Actions. This action convers the Fahrenheit degree into Celsius and set it to the global state.

    def set_fahrenheit(value:)
      new_celsius = (value - 32) * 5 / 9.0
      return {celsius: new_celsius}
    end

Now your app should react to the change of the Fahrenheit value too.

Install

Use Ovto with static files

$ gem i ovto
$ ovto new myapp --static

Use Ovto with Sinatra

$ gem i ovto
$ ovto new myapp --sinatra

Install Ovto into Rails apps

Edit Gemfile

gem 'opal-rails'
gem 'ovto'

Run bundle install

Remove app/assets/javascripts/application.js

Create app/assets/javascripts/application.js.rb

require 'opal'
require 'rails-ujs'
require 'activestorage'
require 'turbolinks'
require_tree '.'

Create app/assets/javascripts/foo.js.rb (file name is arbitrary)

require 'ovto'

class Foo < Ovto::App
  class State < Ovto::State
  end

  class Actions < Ovto::Actions
  end

  class MainComponent < Ovto::Component
    def render(state:)
      o 'h1', "HELLO"
    end
  end
end

Edit app/views/<some controller/<some view>.html.erb

<div id='foo-app'></div>
<%= opal_tag do %>
  Foo.run(id: 'foo-app')
<% end %>

This should render HELLO in the browser.

You also need to edit config/environments/production.rb like this before deploy it to production.

    #config.assets.js_compressor = :uglifier
    config.assets.js_compressor = Uglifier.new(harmony: true)

Ovto::App

First of all, you need to define a subclass of Ovto::App and define class State, class Actions and class MainComponent in it.

Example

This is a smallest Ovto app.

require 'opal'
require 'ovto'

class MyApp < Ovto::App
  class State < Ovto::State
  end

  class Actions < Ovto::Actions
  end

  class MainComponent < Ovto::Component
    def render
      o 'input', type: 'button', value: 'Hello'
    end
  end
end

MyApp.run(id: 'ovto')

It renders a button and does nothing else. Let's have some fun:

require 'opal'
require 'ovto'

class MyApp < Ovto::App
  COLORS = ["red", "blue", "green"]

  class State < Ovto::State
    item :color_idx, default: 0
  end

  class Actions < Ovto::Actions
    def update_color
      new_idx = (state.color_idx + 1) % COLORS.length
      return {color_idx: new_idx}
    end
  end

  class MainComponent < Ovto::Component
    def render
      o 'input', {
        type: 'button',
        value: 'Hello',
        style: {background: COLORS[state.color_idx]},
        onclick: ->{ actions.update_color },
      }
    end
  end
end

MyApp.run(id: 'ovto')

Here we added color_idx to app state and update_color action to change it. The button is updated to have the color indicated by color_idx and now has onclick event handler which executes the action.

Calling actions on startup

To invoke certain actions on app startup, define MyApp#setup and use MyApp#actions.

Example:

class MyApp < Ovto::App
  def setup
    actions.fetch_data()
  end

  ...
end

MyApp.run(id: 'ovto')

Ovto::State

Ovto::State is like a hash, but members are accessible with name rather than [].

Example

class State < Ovto::State
  item :foo
  item :bar
end

state = State.new(foo: 1, bar: 2)
state.foo  #=> 1
state.bar  #=> 2

Default value

class State < Ovto::State
  item :foo, default: 1
  item :bar, default: 2
end

state = State.new
state.foo  #=> 1
state.bar  #=> 2

Immutable

State objects are immutable. i.e. you cannot update value of a key. Instead, use State#merge.

state = State.new(foo: 1, bar: 2)
new_state = state.merge(bar: 3)
new_state.foo  #=> 1
new_state.bar  #=> 3

Nesting state

For practical apps, you can nest State like this.

class Book < Ovto::State
  item :title
  item :author
end

class State < Ovto::State
  item :books, []
end

book = Book.new('Hello world', 'taro')
state = State.new(books: [book])

Defining instance methods of state

You can define instance methods of state.

class Book < Ovto::State
  item :title
  item :author

  def to_text
    "#{self.title} (#{self.author})"
  end
end

book = Book.new('Hello world', 'taro')
book.to_text  #=> "Hello world (taro)"

Defining class methods of state

Ovto does not have a class like StateList. Just use Array to represent a list of state.

You can define class methods to manipulate a list of state.

class Book < Ovto::State
  item :title
  item :author

  def self.of_author(books, author)
    books.select{|x| x.author == author}
  end
end

# Example
taro_books = Book.of_author(books, "taro")

Ovto::Actions

Actions are the only way to change the state. Actions must be defined as methods of the Actions class. Here is some more conventions:

  • You must use keyword arguments
  • You must return state updates as a Hash. It will be merged into the app state.
  • You can get the current state by state method

Example:

require 'opal'
require 'ovto'

class MyApp < Ovto::App
  class State < Ovto::State
    item :count, default: 0
  end

  class Actions < Ovto::Actions
    def increment(by:)
      return {count: state.count + by}
    end
  end

  class MainComponent < Ovto::Component
    def render
      o 'span', state.count
      o 'button', onclick: ->{ actions.increment(by: 1) }
    end
  end
end

MyApp.run(id: 'ovto')

Calling actions

Actions can be called from components via actions method. This is an instance of Ovto::WiredActions and has methods of the same name as your Actions class.

  o 'button', onclick: ->{ actions.increment(by: 1) }

Arguments are almost the same as the original but you don't need to provide state; it is automatically passed by Ovto::WiredActions class. It also updates the app state with the return value of the action, and schedules rendering the view.

Skipping state update

An action may return nil when no app state changes are needed.

Promises are also special values which does not cause state changes (see the next section).

Async actions

When calling server apis, you cannot tell how the app state will change until the server responds. In such cases, you can call another action via actions to tell Ovto to reflect the api result to the app state.

Example:

  class Actions < Ovto::Actions
    def fetch_tasks
      Ovto.fetch('/tasks.json').then {|tasks_json|
        actions.receive_tasks(tasks: tasks_json)
      }.fail {|e|
        console.log("failed:", e)
      }
    end

    def receive_tasks(tasks_json:)
      tasks = tasks_json.map{|item| Task.new(**item)}
      return {tasks: tasks}
    end
  end

Ovto::Component

An Ovto app must have MainComponent class, a subclass of Ovto::Component.

'render' method

render is the only method you need to define in the MainComponent class. You can get the global app state by calling state method.

  class MainComponent < Ovto::Component
    def render
      o 'div' do
        o 'h1', 'Your todos'
        o 'ul' do
          state.todos.each do |todo|
            o 'li', todo.title
          end
        end
      end
    end
  end

MoreThanOneNode error

If you missed the surrounding 'div' tag, Ovto raises an MoreThanOneNode error. render must create a single DOM node.

    def render
      o 'h1', 'Your todos'
      o 'ul' do
        state.todos.each do |todo|
          o 'li', todo.title
        end
      end
    end

#=> $MoreThanOneNodeĀ {name: "MoreThanOneNode", ...}

The 'o' method

Ovto::Component#o describes your app's view. For example:

o 'div'
#=> <div></div>

o 'div', 'Hello.'
#=> <div>Hello.</div>

You can pass attributes with a Hash.

o 'div', class: 'main', 'Hello.'
#=> <div class='main'>Hello.</div>

There are shorthand notations for classes and ids.

o 'div.main'
#=> <div class='main'>Hello.</div>

o 'div#main'
#=> <div id='main'>Hello.</div>

You can also give a block to specify its content.

o 'div' do
  'Hello.'
end
#=> <div>Hello.</div>

o 'div' do
  o 'h1', 'Hello.'
end
#=> <div><h1>Hello.</h1></div>

Special attribute: style

There are some special keys for the attributes Hash. style: key takes a hash as its value and specifies styles of the tag.

o 'div', style: {color: 'red'}, 'Hello.'
#=> <div style='color: red;'>Hello.</div>

Special attribute: onxx

An attribute starts with "on" specifies an event handler.

For example:

o 'input', {
  type: 'button',
  onclick: ->(e){ p e.target.value },
  value: 'Hello.'
}

The argument e is an instance of Opal::Native and wraps the JavaScript event object. You can get the input value with e.target.value here.

Lifecycle events

There are special events oncreate, onupdate, onremove, ondestroy.

https://github.com/hyperapp/hyperapp#lifecycle-events

Special attribute: key

https://github.com/hyperapp/hyperapp#keys

Sub components

o can take another component class to render.

  # Sub component
  class TodoList < Ovto::Component
    def render(todos:)
      o 'ul' do
        todos.each do |todo|
          o 'li', todo.title
        end
      end
    end
  end

  # Main component
  class MainComponent < Ovto::Component
    def render
      o 'div' do
        o 'h1', 'Your todos'
        o TodoList, todos: state.todos
      end
    end
  end

Text node

Sometimes you may want to create a text node.

#=> <div>Age: <span class='age'>12</a></div>
#        ~~~~~
#          |
#          +--Raw text (not enclosed by an inner tag)

o generates a text node when 'text' is specified as tag name. The above HTML could be described like this.

o 'div' do
  o 'text', 'Age:'
  o 'span', '12'
end

Ovto::PureComponent

It almost the same as Ovto::Component, but it caches the render method calling with arguments of the method.

When to use PureComponent?

Use it when your app is slow and need more speed.

Cache strategy

It compares render method arguments and the previous arguments.

def render
  o 'div' do
    o Pure, foo: state.foo
    o NotPure bar: state.bar
  end
end

In this case, NotPure component's render method is called even if state.foo is changed. Whereas Pure component's render method is called only if state.foo is changed.

State

state method is not available in PureComponent, because PureComponent does not treat state as the cache key. If you'd like to use state in PureComponent, pass the state from the parent component.

Ovto::Middleware

When you are making a big app with Ovto, you may want to extract certain parts which are independent from the app. Ovto::Middleware is for such cases.

  • A middleware has its own namespace of state and actions
    • that is, you don't need to add prefixes to the names of states and actions of a middleware.

Example

# 1. Middleware name must be valid as a method name in Ruby
class MyMiddleware < Ovto::Middleware("my_middleware")
  def setup
    # Called on app startup
  end

  # 2. Make a subclass of MyMiddleware's State
  class State < MyMiddleware::State
    item :count, default: 0
  end

  # 3. Make a subclass of MyMiddleware's Actions
  class Actions < MyMiddleware::Actions
    def increment(by:)
      return {count: state.count + by}
    end
  end

  # 4. Make a subclass of MyMiddleware's Component
  class SomeComponent < MyMiddleware::Component
    def render
      o 'div' do
        o 'span', state.count
        o 'button', onclick: ->{ actions.increment(by: 1) }
      end
    end
  end
end

class MyApp < Ovto::App
  # 5. Declare middlewares to use
  use MyMiddleware

  class State < Ovto::State; end
  class Actions < Ovto::Actions; end

  class MainComponent < Ovto::Component
    def render
      o 'div.counter' do
        o MyMiddleware::SomeComponent
      end
    end
  end
end

Advanced

Getting middlware state from app

class MyApp < Ovto::App
  def MainComponent < Ovto::Component
    def render
      o 'span', state._middlewares.middleware1.some_state
    end
  end
end

Calling middlware action from app

class MyApp < Ovto::App
  # From actions
  def Actions < Ovto::Actions
    def some_action
      actions.middleware1.some_action()
    end
  end

  # From component
  def MainComponent < Ovto::Component
    def render
      o 'button', onclick: ->{ actions.middleware1.some_action() }
    end
  end
end

Using a middleware from another middleware

class Middleware1 < Ovto::Middleware("middleware1")
  use Middleware2
end

Ovto.fetch

Ovto provides wrapper of Fetch API, for convenience of calling typical server-side APIs (eg. those generated by rails scaffold command.)

Ovto.fetch returns Opal's Promise object that calls the API with the specified parameters.

Examples

GET

Ovto.fetch('/api/tasks').then{|json_data|
  p json_data
}.fail{|e|  # Network error, 404 Not Found, JSON parse error, etc.
  p e
}

POST

Ovto.fetch('/api/new_task', 'POST', {title: "do something"}).then{|json_data|
  p json_data
}.fail{|e|  # Network error, 404 Not Found, JSON parse error, etc.
  p e
}

PUT

Ovto.fetch('/api/tasks/1', 'PUT', {title: "do something"}).then{|json_data|
  p json_data
}.fail{|e|  # Network error, 404 Not Found, JSON parse error, etc.
  p e
}

CSRF tokens

You don't need to care about CSRF tokens if the server is a Rails app because Ovto.fetch automatically send X-CSRF-Token header if the page contains a meta tag like <meta name='csrf-token' content='....' />.

Debugging Ovto app

console.log

In an Ovto app, you can print any object to developer console by console.log like in JavaScript.

console.log(state: State.new)

This is mostly equal to p state: State.new but console.log supports JavaScript objects too.

(Note: this is not an official feature of Opal. You can do this setup by this:)

  require 'console'; def console; $console; end

ovto-debug

If the page has a tag with id='ovto-debug', exception is shown in the tag.

Ovto.debug_trace

If Ovto.debug_trace is set to true, some diagnostic messages are shown in the browser console.

Ovto.debug_trace = true
MyApp.run(id: 'ovto')

Development notes

How to run unit test

  1. git clone
  2. bundle install
  3. bundle exec rake

How to rebuild docs

bundle exec doc:build