Revolving Hearts Logo. Heartml Search ⌘K

Ruby Backend

Ride The Streetcar is framework-agnostic, meaning that you can use it with any Ruby server architecture. As long as you can return an HTML string response, you can use Streetcar!

Installation & Usage #

Within your Ruby project, run:

bundle add ride_the_streetcar

Check out specific examples of integrations with popular Ruby web frameworks below.

You will also need to install the @heartml/steetcar_elements package for your frontend. See Client Elements for more information.

The easiest way to use Steetcar is to add a helper which provides the DSL object to your routes.

# in any sort of "helper" module or context
def streetcar = RideTheStreetcar.dsl

Then in your route, use the DSL to issue one or more statements, known colloquially as “riding the streetcar”:

# The output value of the `ride` method is a string containing HTML (custom elements)
streetcar.ride do
  query_selector("#btn").set_style :background_color, :red
  query_selector_all("input").set_property :value, "default"
  console.log "Ride complete!"
end

In that example, the DSL methods are available within the block context directly. If you’d rather not employ the magic of instance_exec, you can use a block variable instead:

Whoa = Data.define(:id) do
  def to_dom_selector = "#ident_#{id}"
end

whoa = Whoa.new(id: "abc123")

streetcar.ride do |sc|
  sc.q(whoa).inner_html = "<p>Whoa!</p>"
end

In that example, q is shown as an alias for query_selector (plus q_all is an alias for query_selector_all). Also shown is our convention where any object responding to to_dom_selector can be passed as a query selector instead of a string. This makes it handy when you have 1:1 mappings between database records and UI elements on-screen.

It’s possible to inform the frontend you want your statement(s) run within a View Transition by passing start_transition: true to the ride method.

Many of the DSL methods are chainable, meaning you can use the return value from one statement to invoke another:

streetcar.ride do |sc|
  query = sc["my-element"] # yet another query_selector alt
  query
    .text_content("abc 123")
    .set_attribute("foo", "bar").remove_attribute(:bad_news)
end

Ride The Streetcar doesn’t provide any data sanitation. Please use the features of your Ruby framework to ensure user-supplied content isn’t re-rendered verbatim when you aren’t expecting it. Or you can use standard DOM features like setting textContent rather than innerHTML.

Adding Custom Statements #

If you’d like to add your own statement types (assuming the corresponding custom elements are available on the frontend), you can use the RideTheStreetcar.customize method and pass a module. For example:

module Customization
  module GlobalStatements
    def redirect_to(url:, delay: 3000)
      custom :redirect_to, url:, delay:
    end
  end

  module QueryStatements
    def morph(content = nil, &block)
      contents = content ? proc { content } : block
      custom :morph, &contents
    end
  end

  def log_this(message)
    custom :log_this, message:
  end
end

RideTheStreetcar.customize Customization

In this example, redirect_to is made available at the root level, morph is made available on querySelector/querySelectorAll, and log_this is made available to both root and selector levels.

By convention, snake_case names will be converted to kebob-case and prefixed by sc-c-. (We use sc-c- instead of just sc- so user-created statement types won’t ever conflict with statement types provided by Ride The Streetcar.) Thus sc.log_this("Message") will be output as a <sc-c-log-this message="Message"> tag.

Data provided to attributes is not automatically converted to JSON (as this isn’t always needed). Use .to_json when you pass data to the custom method if need be (and then make sure in your custom element you use JSON.parse to convert the attribute back).


Integration Examples #

Sinatra #

require "ride_the_streetcar"

helpers do
  def streetcar = RideTheStreetcar.dsl
end

# in your routes:
post '/path/to/url' do
  streetcar.ride do
    ...
  end
end

Roda #

require "ride_the_streetcar"

module StreetcarHelper
  module InstanceMethods
    def streetcar = RideTheStreetcar.dsl
  end
end

Roda.plugin StreetcarHelper

# in your routes:
route do |r|
  r.post '/path/to/url' do
    streetcar.ride do
      ...
    end
  end
end

Hanami #

# config/providers/streetcar.rb
Hanami.app.register_provider(:streetcar) do
  prepare do
    require "ride_the_streetcar"
  end

  start do
    register "streetcar", RideTheStreetcar.dsl
  end
end

# in your actions:
module MyApplication
  module Actions
    module SomethingFun
      include Deps["streetcar"]

      class Riding < MyApplication::Action
        def handle(request, response)
          response.body = streetcar.ride do
            ...
          end
        end
      end
    end
  end
end

Rails #

# app/controllers/application_controller
require "ride_the_streetcar"

class ApplicationController < ActionController::Base
  def streetcar = RideTheStreetcar.dsl
end

# in your controllers
class SomethingFunController < ApplicationController
  def riding
    render(html: streetcar.ride do
      ...
    end.html_safe)
  end
end

Statement Types #

Root DSL #

These all live at the root level, e.g. sc.console.log.

console.{log,debug,warn,error}

Mirrors the API of the browser's console object.

streetcar.ride do
  console.warn("Don't do that")
  console.info("Just so you know")
end

query_selector{_all}

Selects one, or many, elements using a CSS selector(s), providing a DSL with many features similar to the Element-based APIs on the client. Many methods can be chained for quick usage. Further documentation below.

streetcar.ride do |s|
  query_selector("some-element")
    .text_content("abc 123")
    .set_attribute("foo", "bar").remove_attribute(:bad_news)
end

set_property

Will set a property on document or window (or lower down in a prop chain).

streetcar.ride do
  set_property "window.someObject.someProp", "value"
  # or a shorthand DSL via `js`:
  js.window.someObject.someProp = "value"
end

apply

Call a method on document or window (or lower down in a prop chain), optionally with arguments.

streetcar.ride do
  # keyword argument(s) get converted to a JavaScript key/val object as the last positional argument
  apply("window.someObject.someMethod", 123, "str!", [{ abc: :xyz }], kwarg: true)
end

navigate

Navigates the browser to a new URL using the configured strategy on the client (location.href= by default).

streetcar.ride do
  navigate("/off-and-away")
end

Query Selector DSL #

These all live as part of query_selector or query_selector_all to affect selected element(s), e.g. sc.query_selector("a.selector").inner_html

Note that when passing a symbol, snake_case will be converted to kebob-case (useful for web components, aka query_selector(:some_component_tag)querySelector("some-component-tag)).

inner_html

Clears the element's DOM children (if any) and adds the supplied HTML code.

streetcar.ride do
  query_selector("#ident").inner_html = "<p>All new content.</p>"
  # or you can chain:
  query_selector(...).inner_html("<html>").other_statements...
end

outer_html

Effectively replaces the element in the DOM with the supplied HTML code.

streetcar.ride do
  query_selector("#ident").outer_html = "<section>Whatever was, is no more!</section>"
  # or you can chain:
  query_selector(...).outer_html("<html>").other_statements...
end

text_content

Sets the inner text of an element as plain text (all HTML-like syntax will be escaped).

streetcar.ride do
  query_selector("#ident").text_content = "abc 123 < 456"
  # or you can chain:
  query_selector(...).text_content("hello world").other_statements...
end

class_list.{add,remove}

Add or remove CSS class names from the selected element(s).

streetcar.ride do
  query_selector("#ident").class_list.add "is-validated"
  query_selector("#ident").class_list.remove "has-errors"
end

class_list.toggle

Toggle the presence or absence of a class name. Use force argument to ensure toggle on/off matches true/false.

streetcar.ride do
  query_selector("#ident").class_list.toggle "is-visible" #, force: true
end

set_attribute

Add HTML attribute to the selected element(s). Attribute names of snake_case are converted to kebob-case (useful when passing a symbol).

streetcar.ride do
  query_selector("#ident").set_attribute "id", "another-ident"
end

remove_attribute

Remove HTML attribute from the selected element(s). Attribute names of snake_case are converted to kebob-case (useful when passing a symbol).

streetcar.ride do
  query_selector("#ident").remove_attribute "aria-invalid"
end

toggle_attribute

Toggle the presence or absence of a boolean-style attribute. Use force argument to ensure toggle on/off matches true/false. Attribute names of snake_case are converted to kebob-case (useful when passing a symbol).

streetcar.ride do
  query_selector("#ident").toggle_attribute "disabled" #, force: true
end

before

Inserts HTML code into the DOM as a sibling before the selected element. Make sure you provide only tag at the root (all other elements will be ignored).

streetcar.ride do
  query_selector("body > footer").before "<main>I'm content.</main>"
end

prepend

Inserts HTML code into the DOM as a new first child within the selected element. Make sure you provide only tag at the root (all other elements will be ignored).

streetcar.ride do
  query_selector("body > footer").prepend "<small>Copyright</small>"
end

append

Inserts HTML code into the DOM as a new last child within the selected element. Make sure you provide only tag at the root (all other elements will be ignored).

streetcar.ride do
  query_selector("main").append "<p>More content!</p>"
end

after

Inserts HTML code into the DOM as a sibling following the selected element. Make sure you provide only tag at the root (all other elements will be ignored).

streetcar.ride do
  query_selector("body > header").after "<main>I'm content.</main>"
end

remove

Removes selected element(s) from the DOM.

streetcar.ride do
  query_selector("#ident").remove
end

set_style

Uses the DOM API (.style=) to set inline styles on the selected element(s).

streetcar.ride do
  # You can use either camelCase or snake_case for property names
  query_selector(".message").set_style :background_color, :red
end

set_property

Will set a property on the selected element(s).

streetcar.ride do
  query_selector(:my_element).set_property("someProp", "value")
  # or a shorthand DSL via `js`:
  query_selector(:my_element).js.someProp = "value"
end

apply

Call a method on the selected element(s) (or lower down in a prop chain), optionally with arguments.

streetcar.ride do
  # keyword argument(s) get converted to a JavaScript key/val object as the last positional argument
  query_selector("some-element").apply("methodName", 123, "str!", [abc: :xyz], kwarg: true)
end

dispatch_event

Dispatches an event from the selected element, either Event or CustomEvent depending on the presence of detail. Additional optional arguments match the Event API.

streetcar.ride do
  query_selector("yo-dawg").dispatch_event("heard", bubbles: true, detail: { abc: 123 })
end

Community 👋😃 #

Join the Human Web Collective Discord where you can meet friendly fellow developers who love investigating and using “vanilla-adjacent” frontend APIs and ask questions about this project and beyond.

Interested in contributing to Ride The Streetcar? Bug reports, feature requests, and PRs most welcome. (Full human authorship only, please!) There are three repos to choose from depending on the issue:


Skip to content Close