JavaScript Backend
Ride The Streetcar is framework-agnostic, meaning that you can use it with any JavaScript/Node server architecture. (Other server runtimes likely work but are not “officially” supported.) As long as you can return an HTML string response, you can use Streetcar!
Installation & Usage #
Within your Node project, run:
npm install @heartml/ride-the-streetcar-js
Check out specific examples of integrations with popular JavaScript web frameworks below.
You will also need to install the @heartml/steetcar_elements package for your frontend. See Client Elements for more information.
In your route(s) file, use the Streetcar DSL to issue one or more statements, known colloquially as “riding the streetcar”:
// This could perhaps be hoisted above multiple routes
const streetcar = RideTheStreetcar.dsl()
// The output value of the `ride` method is a string containing HTML (custom elements)
const html = streetcar.ride(sc => {
sc.query_selector("#btn").setStyle("backgroundColor", "red")
sc.query_selector_all("input").setProperty("value", "default")
sc.console.log("Ride complete!")
})
The DSL provides a convention where any object responding to toDOMSelector() 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:
class SelectableObject {
toDOMSelector() {
return "#oldie"
}
}
const selectableObj = new SelectableObject()
streetcar.ride(sc => {
sc.querySelector(selectableObj).remove()
})
It’s possible to inform the frontend you want your statement(s) run within a View Transition by passing { startTransition: true } to the ride method as its second argument.
Many of the DSL methods are chainable, meaning you can use the return value from one statement to invoke another:
streetcar.ride(sc => {
const query = sc.querySelector("my-element")
query
.setTextContent("abc 123")
.setAttribute("foo", "bar").removeAttribute("bad-news")
})
Ride The Streetcar doesn’t provide any data sanitation. Please use the features of your JavaScript 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 #
Support for this feature is coming soon…
Integration Examples #
Astro #
// riding.ts
import type { APIRoute } from "astro";
import RideTheStreetcar from "@heartml/ride-the-streetcar-js";
const streetcar = RideTheStreetcar.dsl();
export const POST = (({ request }) => {
return new Response(
streetcar.ride(sc => {
...
}),
{ headers: { 'Content-Type': 'text/html; charset=utf-8' } }
)
}) satisfies APIRoute;
Fastify #
import Fastify from 'fastify'
import RideTheStreetcar from '@heartml/ride-the-streetcar-js'
const fastify = Fastify({
// ...further setup...
})
// declare a route
fastify.post('/path/to/url', function (request, reply) {
reply
.type('text/html; charset=utf-8')
.send(streetcar.ride(sc => {
...
}))
})
Express #
import express from 'express';
import RideTheStreetcar from '@heartml/ride-the-streetcar-js';
const app = express();
// ...further setup...
app.post('/path/to/url', (req, res) => {
res
.set('content-type', 'text/html; charset=utf-8')
.send(streetcar.ride(sc => {
...
}));
});
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(sc => {
sc.console.warn("Don't do that")
sc.console.info("Just so you know")
})
querySelector{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(sc => {
sc.querySelector("some-element")
.textContent("abc 123")
.setAttribute("foo", "bar").removeAttribute("bad-news")
})
setProperty
Will set a property on document or window (or lower down in a prop chain).
streetcar.ride(sc => {
sc.setProperty("window.someObject.someProp", "value")
})
apply
Call a method on document or window (or lower down in a prop chain), optionally with arguments.
streetcar.ride(sc => {
sc.apply("window.someObject.someMethod", 123, "str!", { someOption: true })
})
navigate
Navigates the browser to a new URL using the configured strategy on the client (location.href= by default).
streetcar.ride(sc => {
sc.navigate("/off-and-away")
})
Query Selector DSL #
These all live as part of querySelector or querySelector_all to affect selected element(s), e.g. sc.querySelector("a.selector").innerHTML
innerHTML
Clears the element's DOM children (if any) and adds the supplied HTML code.
streetcar.ride(sc => {
sc.querySelector("#ident").innerHTML = "<p>All new content.</p>"
// or you can chain:
sc.querySelector(...).setInnerHTML("<html>").other_statements...
})
outerHTML
Effectively replaces the element in the DOM with the supplied HTML code.
streetcar.ride(sc => {
sc.querySelector("#ident").outerHTML = "<section>Whatever was, is no more!</section>"
// or you can chain:
sc.querySelector(...).setOuterHTML("<html>").other_statements...
})
textContent
Sets the inner text of an element as plain text (all HTML-like syntax will be escaped).
streetcar.ride(sc => {
sc.querySelector("#ident").textContent = "abc 123 < 456"
// or you can chain:
sc.querySelector(...).setTextContent("<html>").other_statements...
})
classList.{add,remove}
Add or remove CSS class names from the selected element(s).
streetcar.ride(sc => {
sc.querySelector("#ident").classList.add("is-validated")
sc.querySelector("#ident").classList.remove("has-errors")
})
classList.toggle
Toggle the presence or absence of a class name. Use force argument to ensure toggle on/off matches true/false.
streetcar.ride(sc => {
sc.querySelector("#ident").classList.toggle("is-visible"/*, { force: true } */)
})
setAttribute
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(sc => {
sc.querySelector("#ident").setAttribute("id", "another-ident")
})
removeAttribute
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(sc => {
sc.querySelector("#ident").removeAttribute("aria-invalid")
})
toggleAttribute
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(sc => {
sc.querySelector("#ident").toggle("disabled"/*, { force: true } */)
})
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(sc => {
sc.querySelector("body > footer").before("<main>I'm content.</main>")
})
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(sc => {
sc.querySelector("body > footer").prepend("<small>Copyright</small>")
})
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(sc => {
sc.querySelector("main").append("<p>More content!</p>")
})
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(sc => {
sc.querySelector("body > header").after("<main>I'm content.</main>")
})
remove
Removes selected element(s) from the DOM.
streetcar.ride(sc => {
sc.querySelector("#ident").remove()
})
setStyle
Uses the DOM API (.style=) to set inline styles on the selected element(s).
streetcar.ride(sc => {
sc.querySelector(".message").setStyle("backgroundColor", "red")
})
setProperty
Will set a property on the selected element(s).
streetcar.ride(sc => {
sc.querySelector("my-element").setProperty("someProp", "value")
})
apply
Call a method on the selected element(s) (or lower down in a prop chain), optionally with arguments.
streetcar.ride(sc => {
sc.querySelector("some-element").apply("methodName", 123, "str!", { someOption: true })
})
dispatchEvent
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(sc => {
sc.querySelector("yo-dawg").dispatchEvent("heard", { bubbles: true, detail: { abc: 123 } })
})
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)