Created
November 28, 2023 10:25
-
-
Save radanskoric/9bdaa8f64289b00b3cfb1d35cd889196 to your computer and use it in GitHub Desktop.
Turbo Frames and Streams on Sinatra
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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