Skip to content

Commit

Permalink
Move superglue_rails into own repo
Browse files Browse the repository at this point in the history
  • Loading branch information
jho406 committed Jan 13, 2025
0 parents commit 3f51edc
Show file tree
Hide file tree
Showing 63 changed files with 2,935 additions and 0 deletions.
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
*.lock
.tool-versions
node_modules
dist
pkg/
vendor/bundle
test/log/
tmp/
.byebug_history
log
blade.yml
breezy/build/**/*.js
*.gem
props_template/performance/**/*.png
.tool-versions
testapp/

11 changes: 11 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
source 'https://rubygems.org'
gemspec

gem 'rails', '~> 7.2.0'
gem 'selenium-webdriver'
gem 'props_template'
gem 'standard'
gem 'capybara'
gem 'minitest'
gem 'rake'
gem 'sqlite3', '~> 1.4'
11 changes: 11 additions & 0 deletions Gemfile.70
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
source 'https://rubygems.org'
gemspec

gem 'rails', '~> 7.0.0'
gem 'selenium-webdriver'
gem 'props_template'
gem 'standard'
gem 'capybara'
gem 'minitest'
gem 'rake'
gem 'sqlite3', '~> 1.4'
11 changes: 11 additions & 0 deletions Gemfile.71
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
source 'https://rubygems.org'
gemspec

gem 'rails', '~> 7.1.0'
gem 'selenium-webdriver'
gem 'props_template'
gem 'standard'
gem 'capybara'
gem 'minitest'
gem 'rake'
gem 'sqlite3', '~> 1.4'
11 changes: 11 additions & 0 deletions Gemfile.72
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
source 'https://rubygems.org'
gemspec

gem 'rails', '~> 7.2.0'
gem 'selenium-webdriver'
gem 'props_template'
gem 'standard'
gem 'capybara'
gem 'minitest'
gem 'rake'
gem 'sqlite3', '~> 1.4'
11 changes: 11 additions & 0 deletions Gemfile.80
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
source 'https://rubygems.org'
gemspec

gem 'rails', '~> 8.0.0'
gem 'selenium-webdriver'
gem 'props_template'
gem 'standard'
gem 'capybara'
gem 'minitest'
gem 'rake'
gem 'sqlite3'
11 changes: 11 additions & 0 deletions Gemfile.main
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
source 'https://rubygems.org'
gemspec

gem 'rails', git: 'https://github.com/rails/rails', ref: 'main'
gem 'selenium-webdriver'
gem 'props_template'
gem 'standard'
gem 'capybara'
gem 'minitest'
gem 'rake'
gem 'sqlite3', '~> 1.4'
9 changes: 9 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "rake/testtask"
require "standard/rake"

Rake::TestTask.new do |t|
t.libs << "test"
t.pattern = "test/**/*_test.rb"
t.warning = false
t.verbose = true
end
119 changes: 119 additions & 0 deletions lib/generators/superglue/install/install_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
require "rails/generators/named_base"
require "rails/generators/resource_helpers"

module Superglue
module Generators
class InstallGenerator < Rails::Generators::Base
source_root File.expand_path("../templates", __FILE__)

class_option :typescript,
type: :boolean,
required: false,
default: false,
desc: "Use typescript"

def create_files
remove_file "#{app_js_path}/application.js"

use_typescript = options["typescript"]
if use_typescript
copy_ts_files
else
copy_js_files
end

say "Copying Superglue initializer"
copy_file "#{__dir__}/templates/initializer.rb", "config/initializers/superglue.rb"

say "Copying application.json.props"
copy_file "#{__dir__}/templates/application.json.props", "app/views/layouts/application.json.props"

say "Adding required member methods to ApplicationRecord"
add_member_methods

say "Installing Superglue and friends"
run "yarn add react react-dom @reduxjs/toolkit react-redux @thoughtbot/superglue"

if use_typescript
run "yarn add -D @types/react-dom @types/react @types/node @thoughtbot/candy_wrapper"
end

say "Superglue is Installed! 🎉", :green
end

private

def copy_ts_files
say "Copying application.tsx file to #{app_js_path}"
copy_file "#{__dir__}/templates/ts/application.tsx", "#{app_js_path}/application.tsx"

say "Copying page_to_page_mapping.ts file to #{app_js_path}"
copy_file "#{__dir__}/templates/ts/page_to_page_mapping.ts", "#{app_js_path}/page_to_page_mapping.ts"

say "Copying flash.ts file to #{app_js_path}"
copy_file "#{__dir__}/templates/ts/flash.ts", "#{app_js_path}/slices/flash.ts"

say "Copying store.ts file to #{app_js_path}"
copy_file "#{__dir__}/templates/ts/store.ts", "#{app_js_path}/store.ts"

say "Copying application_visit.ts file to #{app_js_path}"
copy_file "#{__dir__}/templates/ts/application_visit.ts", "#{app_js_path}/application_visit.ts"

say "Copying components to #{app_js_path}"
copy_file "#{__dir__}/templates/ts/inputs.tsx", "#{app_js_path}/components/Inputs.tsx"
copy_file "#{__dir__}/templates/ts/layout.tsx", "#{app_js_path}/components/Layout.tsx"
copy_file "#{__dir__}/templates/ts/components.ts", "#{app_js_path}/components/index.ts"

say "Copying tsconfig.json file to #{app_js_path}"
copy_file "#{__dir__}/templates/ts/tsconfig.json", "tsconfig.json"
end

def copy_js_files
say "Copying application.js file to #{app_js_path}"
copy_file "#{__dir__}/templates/js/application.jsx", "#{app_js_path}/application.jsx"

say "Copying page_to_page_mapping.js file to #{app_js_path}"
copy_file "#{__dir__}/templates/js/page_to_page_mapping.js", "#{app_js_path}/page_to_page_mapping.js"

say "Copying flash.js file to #{app_js_path}"
copy_file "#{__dir__}/templates/js/flash.js", "#{app_js_path}/slices/flash.js"

say "Copying store.js file to #{app_js_path}"
copy_file "#{__dir__}/templates/js/store.js", "#{app_js_path}/store.js"

say "Copying application_visit.js file to #{app_js_path}"
copy_file "#{__dir__}/templates/js/application_visit.js", "#{app_js_path}/application_visit.js"

say "Copying components to #{app_js_path}"
copy_file "#{__dir__}/templates/js/inputs.jsx", "#{app_js_path}/components/Inputs.jsx"
copy_file "#{__dir__}/templates/js/layout.jsx", "#{app_js_path}/components/Layout.jsx"
copy_file "#{__dir__}/templates/js/components.js", "#{app_js_path}/components/index.js"

say "Copying jsconfig.json file to #{app_js_path}"
copy_file "#{__dir__}/templates/js/jsconfig.json", "jsconfig.json"
end

def add_member_methods
inject_into_file "app/models/application_record.rb", after: "class ApplicationRecord < ActiveRecord::Base\n" do
<<-RUBY
# This enables digging by index when used with props_template
# see https://thoughtbot.github.io/superglue/digging/#index-based-selection
def self.member_at(index)
offset(index).limit(1).first
end
# This enables digging by attribute when used with props_template
# see https://thoughtbot.github.io/superglue/digging/#attribute-based-selection
def self.member_by(attr, value)
find_by(Hash[attr, value])
end
RUBY
end
end

def app_js_path
"app/javascript/"
end
end
end
end
27 changes: 27 additions & 0 deletions lib/generators/superglue/install/templates/application.json.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
path = request.format.json? ? param_to_dig_path(params[:props_at]) : nil

json.data(dig: path) do
yield json
end

json.componentIdentifier active_template_virtual_path
json.defers json.deferred!
json.fragments json.fragments!
json.assets [ asset_path('application.js') ]

if protect_against_forgery?
json.csrfToken form_authenticity_token
end

if path
json.action 'graft'
json.path params[:props_at]
end

json.restoreStrategy 'fromCacheAndRevisitInBackground'

json.renderedAt Time.now.to_i

json.slices do
json.flash flash.to_h
end
1 change: 1 addition & 0 deletions lib/generators/superglue/install/templates/initializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require "props_template/core_ext"
35 changes: 35 additions & 0 deletions lib/generators/superglue/install/templates/js/application.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from "react"
import { createRoot } from "react-dom/client"
import { Application } from "@thoughtbot/superglue"
import { buildVisitAndRemote } from "./application_visit"
import { pageIdentifierToPageComponent } from "./page_to_page_mapping"
import { store } from "./store"

if (typeof window !== "undefined") {
document.addEventListener("DOMContentLoaded", function() {
const appEl = document.getElementById("app")
const location = window.location

if (appEl) {
const root = createRoot(appEl)
root.render(
<Application
// The base url prefixed to all calls made by the `visit`
// and `remote` thunks.
baseUrl={location.origin}
// The global var SUPERGLUE_INITIAL_PAGE_STATE is set by your erb
// template, e.g., index.html.erb
initialPage={window.SUPERGLUE_INITIAL_PAGE_STATE}
// The initial path of the page, e.g., /foobar
path={location.pathname + location.search + location.hash}
// Callback used to setup visit and remote
buildVisitAndRemote={buildVisitAndRemote}
// Callback used to setup the store
store={store}
// Mapping between the page identifier to page component
mapping={pageIdentifierToPageComponent}
/>
)
}
})
}
113 changes: 113 additions & 0 deletions lib/generators/superglue/install/templates/js/application_visit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { visit, remote } from "@thoughtbot/superglue/action_creators"

/**
* This function returns a wrapped visit and remote that will be used by UJS,
* the Navigation component, and passed to your page components through the
* NavigationContext.
*
* You can customize both functions to your liking. For example, for a progress
* bar. This file also adds support for data-sg-remote.
*/
export const buildVisitAndRemote = (ref, store) => {
const appRemote = (path, { dataset, ...options }) => {
/**
* You can make use of `dataset` to add custom UJS options.
* If you are implementing a progress bar, you can selectively
* hide it for some links. For example:
*
* ```
* <a href="/posts?props_at=data.header" data-sg-remote data-sg-hide-progress>
* Click me
* </a>
* ```
*
* This would be available as `sgHideProgress` on the dataset
*/
return store.dispatch(remote(path, options))
}

const appVisit = (path, { dataset, ...options } = {}) => {
/**
* Do something before we make a request.
* e.g, show a [progress bar](https://thoughtbot.github.io/superglue/recipes/progress-bar/).
*
* Hint: you can access the current pageKey
* via `store.getState().superglue.currentPageKey`
*/
return store
.dispatch(visit(path, options))
.then(meta => {
/**
* The assets fingerprints changed, instead of transitioning
* just go to the URL directly to retrieve new assets
*/
if (meta.needsRefresh) {
window.location.href = meta.pageKey
return meta
}

/**
* Your first expanded UJS option, `data-sg-replace`
*
* This option overrides the `navigationAction` to allow a link click or
* a form submission to replace history instead of the usual push.
*/
const navigatonAction = !!dataset?.sgReplace
? "replace"
: meta.navigationAction
ref.current?.navigateTo(meta.pageKey, {
action: navigatonAction
})

/**
* Return the meta object, it's used for scroll restoration when
* handling the back button. You can skip returning, but Superglue
* will warn you about scroll restoration.
*/
return meta
})
.finally(() => {
/**
* Do something after a request.
*
* This is where you hide a progress bar.
*/
})
.catch(err => {
const response = err.response

if (!response) {
/**
* This is for errors that are NOT from a HTTP request.
*
* Tooling like Sentry can capture console errors. If not, feel
* free to customize to send the error to your telemetry tool of choice.
*/
console.error(err)
return
}

if (response.ok) {
/**
* This is for errors that are from a HTTP request.
*
* If the response is OK, it must be an HTML body, we'll
* go to that locaton directly.
*/
window.location = response.url
} else {
if (response.status >= 400 && response.status < 500) {
window.location.href = "/400.html"
return
}

if (response.status >= 500) {
window.location.href = "/500.html"
return
}
}
})
}

return { visit: appVisit, remote: appRemote }
}
Loading

0 comments on commit 3f51edc

Please sign in to comment.