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:
- streetcar-elements (Frontend Library)
- ride_the_streetcar (Ruby Backend)
- ride-the-streetcar-js (JavaScript/Node Backend)