Skip to content

Commit

Permalink
Add jsx rendering defaults
Browse files Browse the repository at this point in the history
This improves the developer experience by adding rendering defaults for superglue views.
Superglue typically requires 3 templates.

```
app/views/
  posts/
    index.html.erb
    index.jsx
    index.json.props
  users/
    index.html.erb
    index.jsx
    index.json.props
```
In most cases `index.html.erb` is the same, so the immediate thought is to remove it. This PR
makes the following possible:

```
app/views
  application/
    superglue.html.erb
  posts/
    index.jsx
    index.json.props
  users/
    index.jsx
    index.json.props
```

In the above scenario, we can now render a common template using `superglue_template` in the controller:

```
class PostsController < ApplicationController
  before_action :use_jsx_rendering_defaults
  superglue_template "application/superglue"
end
```

This PR also adds a custom resolver that allows for the following scenario:

```
app/views
  application/
    superglue.html.erb
  posts/
    index.jsx
  users/
    index.jsx
```

This case is great for projects that have their own API and just want to use
superglue for the rails like routing and take advantage of the UJS helpers. In
that case, they can skip using `props_template`. Note that we still need the
`app/views/layout/application.json.props` that gets generated.
  • Loading branch information
jho406 committed Feb 4, 2025
1 parent bf11402 commit 276b7ef
Show file tree
Hide file tree
Showing 34 changed files with 351 additions and 81 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,3 @@ breezy/build/**/*.js
props_template/performance/**/*.png
.tool-versions
testapp/
superglue/
9 changes: 8 additions & 1 deletion lib/generators/superglue/install/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ def create_files
remove_file "#{app_js_path}/application.js"

use_typescript = options["typescript"]
copy_erb_files

if use_typescript
copy_ts_files
else
Expand Down Expand Up @@ -43,9 +45,14 @@ def create_files

private

def copy_erb_files
say "Copying superglue.html.erb file to app/views/application/"
copy_file "#{__dir__}/templates/erb/superglue.html.erb", "app/views/application/superglue.html.erb"
end

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"
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"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script type="text/javascript">
window.SUPERGLUE_INITIAL_PAGE_STATE=<%= render_props %>;<%# erblint:disable ErbSafety %>
</script>

<div id="app"></div>
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ class ScaffoldControllerGenerator < Rails::Generators::NamedBase # :nodoc:
argument :attributes, type: :array, default: [], banner: "field:type field:type"

def create_controller_files
template "controller.rb", File.join("app/controllers", controller_class_path, "#{controller_file_name}_controller.rb")
controller_file = File.join("app/controllers", controller_class_path, "#{controller_file_name}_controller.rb")
template "controller.rb", controller_file
inject_into_file controller_file, after: /ApplicationController$/ do
"\n before_action :use_jsx_rendering_defaults"
end
end

# Replaces template_engine (and its default erb), with view_collection
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,6 @@ def create_root_folder
empty_directory path unless File.directory?(path)
end

def copy_erb_files
available_views.each do |view|
@action_name = view
filename = filename_with_html_extensions(view)
template "erb/" + filename, File.join("app/views", controller_file_path, filename)
end
end

def copy_prop_files
available_views.each do |view|
@action_name = view
Expand Down
8 changes: 8 additions & 0 deletions lib/superglue.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require "superglue/helpers"
require "superglue/redirection"
require "superglue/rendering"
require "superglue/resolver"
require "props_template"
require "form_props"

Expand All @@ -9,8 +11,10 @@ module Controller
include Helpers

def self.included(base)
base.include ::Superglue::Rendering
if base.respond_to?(:helper_method)
base.helper_method :param_to_dig_path
base.helper_method :render_props
end
end
end
Expand All @@ -31,6 +35,10 @@ class Engine < ::Rails::Engine

if app.config.superglue.auto_include
include Controller

prepend_view_path(
Superglue::Resolver.new(Rails.root.join("app/views"))
)
end
end
end
Expand Down
67 changes: 67 additions & 0 deletions lib/superglue/rendering.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
require "active_support/concern"

module Superglue
module Rendering
REACT_FORMATS = [:tsx, :jsx]

extend ActiveSupport::Concern

included do |base|
base.class_attribute :_superglue_template, instance_accessor: true, default: "application/superglue"
end

class_methods do
def superglue_template(template)
self._superglue_template = template
end
end

def use_jsx_rendering_defaults
@_use_jsx_rendering_defaults = true
end

def _jsx_defaults
@_use_jsx_rendering_defaults && request.format.html?
end

def _ensure_react_page!(template, prefixes)
lookup_context.find(template, prefixes, false, [], formats: [], handlers: [], variants: [], locale: [])
end

def default_render
if _jsx_defaults
_ensure_react_page!(action_name.to_s, _prefixes)
render
else
super
end
end

def render_props
if @_render_options
if template_exists?(@_render_options[:template].to_s, @_render_options[:prefixes], formats: [:json])
render_to_string(@_render_options.merge({formats: [:json], layout: true})).strip.html_safe
else
render_to_string(@_render_options.merge({inline: "", formats: [:json], layout: true})).strip.html_safe
end
end
end

def render(...)
if _jsx_defaults
@_render_options = _normalize_render(...)
_ensure_react_page!(@_render_options[:template].to_s, @_render_options[:prefixes])

html_template_exist = template_exists?(@_render_options[:template].to_s, @_render_options[:prefixes], false)

if html_template_exist
super
else
super(@_render_options.merge({template: _superglue_template, prefixes: []}))
end
else
super
end
end
end
end
58 changes: 58 additions & 0 deletions lib/superglue/resolver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
require 'action_view'

module Superglue
class Resolver < ActionView::FileSystemResolver
class JsxPathParser < ActionView::Resolver::PathParser
REACT_FORMATS = [:tsx, :jsx]

def build_path_regex
formats = Regexp.union(REACT_FORMATS.map(&:to_s))

%r{
\A
(?:(?<prefix>.*)/)?
(?<action>.*?)
(?:\.(?<format>#{formats}))??
\z
}x
end

def parse(path)
@regex ||= build_path_regex
match = @regex.match(path)
path = ActionView::TemplatePath.build(match[:action], match[:prefix] || "", false)
details = ActionView::TemplateDetails.new(
nil,
nil,
match[:format]&.to_sym,
nil
)
ParsedPath.new(path, details)
end
end

def initialize(path)
raise ArgumentError, "path already is a Resolver class" if path.is_a?(ActionView::Resolver)
@unbound_templates = Concurrent::Map.new
@path_parser = JsxPathParser.new
@path = File.expand_path(path)
end

def clear_cache
@unbound_templates.clear
@path_parser = JsxPathParser.new
end

def source_for_template(template)
"''"
end

def filter_and_sort_by_details(templates, requested_details)
if requested_details.formats.empty?
templates
else
[]
end
end
end
end
Loading

0 comments on commit 276b7ef

Please sign in to comment.