Skip to content

Instantly share code, notes, and snippets.

@radanskoric
Created November 28, 2023 10:25
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save radanskoric/9bdaa8f64289b00b3cfb1d35cd889196 to your computer and use it in GitHub Desktop.
Save radanskoric/9bdaa8f64289b00b3cfb1d35cd889196 to your computer and use it in GitHub Desktop.
Turbo Frames and Streams on Sinatra
# This is a little experiment in using Turbo Frames and Streams without Rails.
# Built using just plain Sinatra as the web server.
#
# Make sure that you have sinatra and puma (or some other server) installed:
# gem install sinatra
# gem install puma
#
# You can then run the app with:
# ruby app.rb
require 'sinatra'
# If you're unfamiliar with Sinatra, it is an extremely simple ruby web application
# framework and I recommend you get familiar with its basics: https://sinatrarb.com/intro.html
# If you don't want to do that you can probably just read on and guess what the code is doing,
# it's really that simple and I have not used any advanced features at all.
# We will not use a real database but instead imitate an in memory database with a plain hash:
TASKS = {
1 => "Write the blog post",
2 => "Edit the blog post",
3 => "Publish the blog post",
}
def render_task(id)
<<~HTML
<turbo-frame id="task_#{id}">
<h2>Task #{id}</h2>
<p>#{TASKS[id]}</p>
<a href="/tasks/#{id}/edit">Edit</a>
<form method="delete" action="/tasks/#{id}">
<button type="submit">Delete</button>
</form>
</turbo-frame>
HTML
end
def render_new_form
# In Rails you would do:
# ```ruby
# <%= link_to "Add task", new_task_path, data: {turbo_frame: :new_task_frame} %>
# <%= turbo_frame_tag :new_task_frame %>
# ```
# Notice how close that is to the actual HTML, Rails provides a minimal wrapper around it.
<<~HTML
<a data-turbo-frame="new_task_frame" href="/tasks/new">Add task</a>
<turbo-frame id="new_task_frame"></turbo-frame>
HTML
end
get '/' do
<<~HTML
<html>
<head>
<title>Turbo Frames and Streams without Rails</title>
<script type="module">
// We will be loading the full turbo javascript library directly from the CDN
// but we won't be using anything on the backend. We'll do all the work
// directly, without helpers.
// I locked it to 7.3.0 here because there were some build issues with latest
// at the moment of writing this, but it should work fine with latest version as well.
import hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo@7.3.0';
</script>
</head>
<body>
<h1>Task List</h1>
#{ render_new_form }
<turbo-frame id="tasks">
#{ TASKS.keys.map(&method(:render_task)).join("\n") }
</turbo-frame>
</body>
</html>
HTML
end
get '/tasks/new' do
# The only important thing here is to note that we are using the same id as
# in the placeholer for the new form: "new_task_frame"
<<~HTML
<turbo-frame id="new_task_frame">
<h2>New task</h2>
<form action="/tasks" method="post">
Task: <input type="text" name="description"/>
<input type="submit" value="Create Task"/>
</form>
</turbo-frame>
HTML
end
post '/tasks' do
new_id = TASKS.keys.max + 1 # ensuring a unique id
TASKS[new_id] = params[:description]
# The content type important is. Without this, Turbo will NOT attempt to execute any stream
# instructions. Sinatra will respond with the default content-type of "text/html" and Turbo
# will look for a turbo-frame tag and then fail when it doesn't find it.
content_type 'text/vnd.turbo-stream.html'
# Pay attention to the target, it has to match the id of the turbo-frame tag.
<<~TURBO
<turbo-stream action="append" target="tasks">
<template>
#{ render_task(new_id) }
</template>
</turbo-stream>
<turbo-stream action="update" target="new_task_frame"><template></template></turbo-stream>
TURBO
end
def check_task_id(params)
id = params[:id].to_i
# This will just get Sinatra to render its default 404 page.
raise Sinatra::NotFound unless TASKS.key?(id)
id
end
get '/tasks/:id/edit' do
id = check_task_id(params)
<<~HTML
<turbo-frame id="task_#{id}">
<h2>Edit task</h2>
<form action="/tasks/#{id}" method="post">
Task: <input type="text" value="#{TASKS[id]}" name="description" />
<input type="submit" value="Update Task" />
</form>
</turbo-frame>
HTML
end
post '/tasks/:id' do
id = check_task_id(params)
TASKS[id] = params[:description]
render_task(id)
end
delete '/tasks/:id' do
id = check_task_id(params)
TASKS.delete(id)
# As above, we're responding with turbo-stream instructions so we must change the content-type.
content_type 'text/vnd.turbo-stream.html'
# Again, the important thing is that the target matches the turbo-frame id. This is why
# Rails providing a consistent system for those ids is so valuable. A lot of complexity
# goes away because the code is making this assumption on ids being consistently generated.
<<~TURBO
<turbo-stream action="remove" target="task_#{id}"></turbo-stream>
TURBO
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment