Skip to content

Commit

Permalink
Moved from old repo
Browse files Browse the repository at this point in the history
  • Loading branch information
dhh committed Dec 2, 2020
0 parents commit b6e7fe8
Show file tree
Hide file tree
Showing 101 changed files with 2,199 additions and 0 deletions.
6 changes: 6 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
source 'https://rubygems.org'

gemspec

gem 'rake'
gem 'byebug'
145 changes: 145 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
PATH
remote: .
specs:
turbo (0.1)
rails (>= 6.0.0)

GEM
remote: https://rubygems.org/
specs:
actioncable (6.0.3.1)
actionpack (= 6.0.3.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.0.3.1)
actionpack (= 6.0.3.1)
activejob (= 6.0.3.1)
activerecord (= 6.0.3.1)
activestorage (= 6.0.3.1)
activesupport (= 6.0.3.1)
mail (>= 2.7.1)
actionmailer (6.0.3.1)
actionpack (= 6.0.3.1)
actionview (= 6.0.3.1)
activejob (= 6.0.3.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.3.1)
actionview (= 6.0.3.1)
activesupport (= 6.0.3.1)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.0.3.1)
actionpack (= 6.0.3.1)
activerecord (= 6.0.3.1)
activestorage (= 6.0.3.1)
activesupport (= 6.0.3.1)
nokogiri (>= 1.8.5)
actionview (6.0.3.1)
activesupport (= 6.0.3.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.0.3.1)
activesupport (= 6.0.3.1)
globalid (>= 0.3.6)
activemodel (6.0.3.1)
activesupport (= 6.0.3.1)
activerecord (6.0.3.1)
activemodel (= 6.0.3.1)
activesupport (= 6.0.3.1)
activestorage (6.0.3.1)
actionpack (= 6.0.3.1)
activejob (= 6.0.3.1)
activerecord (= 6.0.3.1)
marcel (~> 0.3.1)
activesupport (6.0.3.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.2, >= 2.2.2)
builder (3.2.4)
byebug (11.0.1)
concurrent-ruby (1.1.6)
crass (1.0.6)
erubi (1.9.0)
globalid (0.4.2)
activesupport (>= 4.2.0)
i18n (1.8.3)
concurrent-ruby (~> 1.0)
loofah (2.5.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
method_source (1.0.0)
mimemagic (0.3.5)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.14.1)
nio4r (2.5.2)
nokogiri (1.10.9)
mini_portile2 (~> 2.4.0)
rack (2.2.2)
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.0.3.1)
actioncable (= 6.0.3.1)
actionmailbox (= 6.0.3.1)
actionmailer (= 6.0.3.1)
actionpack (= 6.0.3.1)
actiontext (= 6.0.3.1)
actionview (= 6.0.3.1)
activejob (= 6.0.3.1)
activemodel (= 6.0.3.1)
activerecord (= 6.0.3.1)
activestorage (= 6.0.3.1)
activesupport (= 6.0.3.1)
bundler (>= 1.3.0)
railties (= 6.0.3.1)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
railties (6.0.3.1)
actionpack (= 6.0.3.1)
activesupport (= 6.0.3.1)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
rake (13.0.0)
sprockets (4.0.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
thor (1.0.1)
thread_safe (0.3.6)
tzinfo (1.2.7)
thread_safe (~> 0.1)
websocket-driver (0.7.2)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.3.0)

PLATFORMS
ruby

DEPENDENCIES
bundler (~> 1.17)
byebug
rake
turbo!

BUNDLED WITH
2.1.4
20 changes: 20 additions & 0 deletions MIT-LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (c) 2020 Basecamp

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Turbo

Turbo gives you the speed of a single-page web application without having to write any JavaScript. Turbo accelerates links and form submissions without requiring you to change your server-side generated HTML. It lets you carve up a page into independent frames, which can be lazy-loaded and operate as independent components. And finally, helps you make partial page updates using just HTML and a set of CRUD-like container tags. These three techniques reduce the amount of custom JavaScript that many web applications need to write by an order of magnitude. And for the few dynamic bits that are left, you're invited to finished the job with Stimulus.

On top of accelerating web applications, Turbo was built from the ground-up to form the foundation of hybrid native applications. Write the navigational shell of your Android or iOS app using the standard platform tooling, then seamlessly fill in features from the web, following native navigation patterns. Not every mobile screen needs to be written in Swift or Kotlin to feel native. With Turbo, you spend less time wrangling JSON, waiting on app stores to approve updates, or reimplementing features you've already created in HTML.

Turbo is a language-agnostic framework written in TypeScript, but this gem builds on top of those basics to make the integration with Rails as smooth as possible. You can deliver turbo updates via model callbacks over Action Cable, respond to controller actions with native navigation or standard redirects, and render turbo frames with helpers and layout-free responses.

## Turbo::Links

Turbo is a continuation of the ideas from the previous Turbolinks framework, and the heart of that past approach lives on as Turbo::Links. When installed, Turbo automatically intercepts all clicks on `<a href>` links to the same domain. When you click an eligible link, Turbo prevents the browser from following it. Instead, Turbo changes the browser’s URL using the History API, requests the new page using `XMLHttpRequest`, and then renders the HTML response.

During rendering, Turbo replaces the current `<body>` element outright and merges the contents of the `<head>` element. The JavaScript window and document objects, and the HTML `<html>` element, persist from one rendering to the next.

Whereas Turbolinks previously just dealt with links, Turbo can now also process form submissions and responses. This means the entire flow in the web application is wrapped into Turbo, making all the parts fast. No more need for `data-remote=true`.

## Turbo::Frames

Turbo reinvents the old HTML technique of frames without any of the drawbacks that lead to developers abandoning it. With Turbo::Frames, you can treat a subset of the page as its own component, where links and form submissions replace only that part. This removes an entire class of problems around partial interactivity that before would have required custom JavaScript.

It also makes it dead easy to carve a single page into smaller pieces that can all live on their own cache timeline. While the bulk of the page might easily be cached between users, a small personalized toolbar perhaps cannot. With Turbo::Frames, you can designate the toolbar as a frame, which will be lazy-loaded automatically by the publicly-cached root page. This means simpler pages, easier caching schemes with fewer dependent keys, and all without needing to write a lick of custom JavaScript.

## Turbo::Updates

Partial page updates that are delivered asynchronously over a web socket connection is the hallmark of modern, reactive web applications. With Turbo::Updates, you can get all of that modern goodness using the existing server-side HTML you're already rendering to deliver the first page load. With a set of simple CRUD container tags, you can send HTML fragments over the web socket (or in response to direct interactions), and see the page change in response to new data. Again, no need to construct an entirely separate API, no need to wrangle JSON, no need to reimplement the HTML construction in JavaScript. Take the HTML you're already making, wrap it in an update tag, and, voila, your page comes alive.

With this Rails integration, you can create these asynchronous updates directly in response to your model changes. Turbo uses Active Jobs to provide asynchronous partial rendering and Action Cable to deliver those updates to subscribers.

## Installation

Turbo consists of two elements: The Rails integration that lives in the gem, and the JavaScript package that lives on NPM. You need to install both to get going:

1. Add the `turbo` gem to your Gemfile: `gem 'turbo'
2. Run `bundle install`.
3. Run `./bin/yarn add @htmloverthewire/turbo`
4. Add Turbo to your pack:

```ruby
import Turbo from "turbo"
Turbo.start()
```

Likewise, you need to ensure that you're keeping both dependencies updated together.

## License

Turbo is released under the [MIT License](https://opensource.org/licenses/MIT).
11 changes: 11 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require "bundler/setup"
require "bundler/gem_tasks"
require "rake/testtask"

Rake::TestTask.new do |test|
test.libs << "test"
test.test_files = FileList["test/**/*_test.rb"]
test.warning = false
end

task default: :test
78 changes: 78 additions & 0 deletions app/channels/turbo/updates/broadcasts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Provides the broadcast commands in synchronous and asynchrous form for the <tt>Turbo::UpdatesChannel</tt>.
# This is not meant to be used directly. See <tt>Turbo::Broadcastable</tt> for the user-facing API that invokes
# these methods with most of the paperwork filled out already.
#
# It is however possible to use it directly like <tt>Turbo::UpdatesChannel.broadcast_remove_to :entries, element: 1</tt>.
module Turbo::Updates::Broadcasts
def broadcast_remove_to(*streamables, element:)
broadcast_command_to *streamables, command: :remove, dom_id: element
end

def broadcast_replace_to(*streamables, element:, **rendering)
broadcast_command_to *streamables, command: :replace, dom_id: element, **rendering
end

def broadcast_append_to(*streamables, container:, **rendering)
broadcast_command_to *streamables, command: :append, dom_id: container, **rendering
end

def broadcast_prepend_to(*streamables, container:, **rendering)
broadcast_command_to *streamables, command: :prepend, dom_id: container, **rendering
end

def broadcast_command_to(*streamables, command:, dom_id:, **rendering)
broadcast_update_to *streamables, content: page_update_command(command, dom_id, content:
rendering.delete(:content) || (rendering.any? ? render_format(:html, **rendering) : nil)
)
end


def broadcast_replace_later_to(*streamables, element:, **rendering)
broadcast_command_later_to *streamables, command: :replace, dom_id: element, **rendering
end

def broadcast_append_later_to(*streamables, container:, **rendering)
broadcast_command_later_to *streamables, command: :append, dom_id: container, **rendering
end

def broadcast_prepend_later_to(*streamables, container:, **rendering)
broadcast_command_later_to *streamables, command: :prepend, dom_id: container, **rendering
end

def broadcast_command_later_to(*streamables, command:, dom_id:, **rendering)
Turbo::Updates::CommandBroadcastJob.perform_later \
stream_name_from(streamables), command: command, dom_id: dom_id, **rendering
end


def broadcast_render_to(*streamables, **rendering)
broadcast_update_to *streamables, content: render_format(:page_update, **rendering)
end

def broadcast_render_later_to(*streamables, **rendering)
Turbo::Updates::BroadcastJob.perform_later stream_name_from(streamables), **rendering
end

def broadcast_update_to(*streamables, content:)
ActionCable.server.broadcast stream_name_from(streamables), content
end


private
def page_update_command(command, element_or_dom_id, content: nil)
%(<template data-page-update="#{command}##{convert_to_dom_id(element_or_dom_id)}">#{content}</template>)
end

def convert_to_dom_id(element_or_dom_id)
if element_or_dom_id.respond_to?(:to_key)
element = element_or_dom_id
ActionView::RecordIdentifier.dom_id(element)
else
dom_id = element_or_dom_id
end
end

def render_format(format, **rendering)
ApplicationController.render(formats: [ format ], **rendering)
end
end
24 changes: 24 additions & 0 deletions app/channels/turbo/updates/stream_name.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Stream names are how we identify which updates should go to which users. All streams run over the same
# <tt>Turbo::UpdatesChannel</tt>, but each with their own subscription. Since stream names are exposed directly to the user
# via the HTML stream subscription tags, we need to ensure that the name isn't tampered with, so the names are signed
# upon generation and verified upon receipt. All verification happens through the <tt>Turbo.signed_stream_verifier</tt>.
module Turbo::Updates::StreamName
# Used by <tt>Turbo::UpdatesChannel</tt> to verify a signed stream name.
def verified_stream_name(signed_stream_name)
Turbo.signed_stream_verifier.verified signed_stream_name
end

# Used by <tt>Turbo::UpdatesHelper#subscribe_to_page_updates_from_signed(*streamables)</tt> to generate a signed stream name.
def signed_stream_name(streamables)
Turbo.signed_stream_verifier.generate stream_name_from(streamables)
end

private
def stream_name_from(streamables)
if streamables.is_a?(Array)
streamables.map { |streamable| stream_name_from(streamable) }.join(":")
else
streamables.then { |streamable| streamable.try(:to_gid_param) || streamable.to_param }
end
end
end
17 changes: 17 additions & 0 deletions app/channels/turbo/updates_channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# The updates channel delivers all the page updates created (primarily) through <tt>Turbo::Broadcastable</tt>.
# A subscription to this channel is made for each individual stream that one wishes to listen for updates to.
# The subscription relies on being passed a <tt>signed_stream_name</tt> parameter generated by turning a set of streamables
# into signed stream name using <tt>Turbo::Updates::StreamName#signed_stream_name</tt>. This is automatically done
# using the view helper <tt>Turbo::UpdatesHelper#subscribe_to_page_updates_from_signed(*streamables)</tt>.
# If the signed stream name cannot be verified, the subscription is rejected.
class Turbo::UpdatesChannel < ActionCable::Channel::Base
extend Turbo::Updates::Broadcasts, Turbo::Updates::StreamName

def subscribed
if verified_stream_name = self.class.verified_stream_name(params[:signed_stream_name])
stream_from verified_stream_name
else
reject
end
end
end
22 changes: 22 additions & 0 deletions app/controllers/turbo/frames/frame_request.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Turbo frame requests are requests made from within a turbo frame with the intention of replacing the content of just
# that frame, not the whole page. They are automatically tagged as such by the Turbo Frame JavaScript, which adds a
# <tt>X-Turbolinks-Frame</tt> header to the request. When that header is detected by the controller, we ensure that any
# template layout is skipped (since we're only working on an in-page frame, thus can skip the weight of the layout), and
# that the etag for the page is changed (such that a cache for a layout-less request isn't served on a normal request
# and vice versa).
#
# This module is automatically included in <tt>ActionController::Base</tt>.
module Turbo::Frames::FrameRequest
extend ActiveSupport::Concern

included do
layout -> { false if turbo_frame_request? }
etag { :frame if turbo_frame_request? }
end

private
def turbo_frame_request?
# FIXME: X-Turbolinks-Frame -> X-Turbo-Frame
request.headers["X-Turbolinks-Frame"].present?
end
end
Loading

0 comments on commit b6e7fe8

Please sign in to comment.