Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prepare for ViewComponents #1749

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
8 changes: 8 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ gem("cache_with_locale")
# https://github.com/hotwired/stimulus-rails/issues/108
gem("sprockets", "~>4.2.1")

# ViewComponents for separation of concerns and much faster template rendering
gem("view_component")
# Improved defaults for ViewComponents
# https://evilmartians.com/chronicles/viewcomponent-in-the-wild-supercharging-your-components
gem("view_component-contrib")

# Security fix updates via Dependabot
# CVE-2021-41817 regex denial of service vulnerability
gem("date", ">= 3.2.1")
Expand Down Expand Up @@ -238,3 +244,5 @@ group :production do
# https://newrelic.com/
gem("newrelic_rpm")
end

gem "dry-initializer", "~> 3.1"
14 changes: 13 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ GEM
docile (1.4.0)
drb (2.2.0)
ruby2_keywords
dry-initializer (3.1.1)
erubi (1.12.0)
execjs (2.9.1)
fastimage (2.3.0)
Expand Down Expand Up @@ -158,6 +159,7 @@ GEM
net-pop
net-smtp
matrix (0.4.2)
method_source (1.0.0)
mimemagic (0.4.3)
nokogiri (~> 1)
rake
Expand Down Expand Up @@ -258,6 +260,7 @@ GEM
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0)
ruby-next-core (1.0.0)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
Expand Down Expand Up @@ -306,6 +309,13 @@ GEM
kgio (~> 2.6)
raindrops (~> 0.7)
uniform_notifier (1.16.0)
view_component (3.10.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
method_source (~> 1.0)
view_component-contrib (0.2.2)
ruby-next-core (>= 0.15.0)
view_component
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
Expand All @@ -332,7 +342,6 @@ PLATFORMS
x86_64-darwin-20
x86_64-darwin-21
x86_64-darwin-22
x86_64-darwin-23
x86_64-linux

DEPENDENCIES
Expand Down Expand Up @@ -361,6 +370,7 @@ DEPENDENCIES
database_cleaner-active_record
date (>= 3.2.1)
debug (>= 1.0.0)
dry-initializer (~> 3.1)
fastimage
i18n
importmap-rails
Expand Down Expand Up @@ -399,6 +409,8 @@ DEPENDENCIES
terser
turbo-rails
unicorn
view_component
view_component-contrib
web-console
webmock
xmlrpc
Expand Down
6 changes: 6 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,10 @@ def get_next_id(object)

query.result_ids[idx + 1] || query.result_ids[idx - 1]
end

# make View components easier to call
def component(name, *args, **kwargs, &block)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on how you and others feel about all-arguments forwarding,
the following should work here. Your choice:

Suggested change
def component(name, *args, **kwargs, &block)
def component(name, ...)

component = name.to_s.camelize.constantize::Component
render(component.new(*args, **kwargs), &block)
end
end
5 changes: 5 additions & 0 deletions app/views/components/application_view_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

class ApplicationViewComponent < ViewComponentContrib::Base
extend Dry::Initializer
end
5 changes: 5 additions & 0 deletions app/views/components/application_view_component_preview.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

class ApplicationViewComponentPreview < ViewComponentContrib::Preview::Base
self.abstract_class = true
end
3 changes: 3 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@

module MushroomObserver
class Application < Rails::Application
config.autoload_paths << Rails.root.join("app/views/components")
config.view_component.preview_paths <<
Rails.root.join("app/views/components")
# Settings in config/environments/* take precedence over those
# specified here.
# Application configuration should go into files in config/initializers
Expand Down
18 changes: 18 additions & 0 deletions config/initializers/view_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

ActiveSupport.on_load(:view_component) do
# Extend your preview controller to support authentication and other
# application-specific stuff
#
# Rails.application.config.to_prepare do
# ViewComponentsController.class_eval do
# include Authenticated
# end
# end
#
# Make it possible to store previews in sidecar folders
# See https://github.com/palkan/view_component-contrib#organizing-components-or-sidecar-pattern-extended
ViewComponent::Preview.extend(ViewComponentContrib::Preview::Sidecarable)
# Enable `self.abstract_class = true` to exclude previews from the list
ViewComponent::Preview.extend(ViewComponentContrib::Preview::Abstract)
end
15 changes: 15 additions & 0 deletions lib/generators/view_component/USAGE
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Description:
============
Creates a new view component, test and preview files.
Pass the component name, either CamelCased or under_scored, and an optional list of attributes as arguments.

Example:
========
bin/rails generate view_component Profile name age

creates a Profile component and test:
Component: app/views/components/profile/component.rb
Template: app/views/components/profile/component.html.erb
Test: test/views/components/profile_component_test.rb
System Test: test/system/views/components/profile_component_test.rb
Preview: app/views/components/profile/component_preview.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div>Add <%= class_name %> template here</div>
8 changes: 8 additions & 0 deletions lib/generators/view_component/templates/component.rb.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

class <%= class_name %>::Component < <%= parent_class %>
with_collection_parameter :<%= singular_name %>
<%- if initialize_signature -%>
<%= initialize_signature %>
<%- end -%>
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

require "application_system_test_case"

class <%= class_name %>::ComponentSystemTest < ApplicationSystemTestCase
def test_default_preview
visit("/rails/view_components<%= File.join(class_path, file_name) %>/default")

# assert_text "Hello!"
# click_on("Click me!")
# assert_text "Good-bye!"
end
end
19 changes: 19 additions & 0 deletions lib/generators/view_component/templates/component_test.rb.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

require "test_helper"

class <%= class_name %>::ComponentTest < ViewComponent::TestCase
def test_renders
component = build_component

render_inline(component)

assert_selector "div"
end

private

def build_component(**options)
<%= class_name %>::Component.new(**options)
end
end
9 changes: 9 additions & 0 deletions lib/generators/view_component/templates/preview.rb.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

class <%= class_name %>::Preview < <%= preview_parent_class %>
# You can specify the container class for the default template
# self.container_class = "w-1/2 border border-gray-300"

def default
end
end
64 changes: 64 additions & 0 deletions lib/generators/view_component/view_component_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# frozen_string_literal: true

# Based on https://github.com/github/view_component/blob/master/lib/rails/generators/component/component_generator.rb
class ViewComponentGenerator < Rails::Generators::NamedBase
source_root File.expand_path("templates", __dir__)

class_option :skip_test, type: :boolean, default: false
class_option :skip_system_test, type: :boolean, default: false
class_option :skip_preview, type: :boolean, default: false

argument :attributes, type: :array, default: [], banner: "attribute"

def create_component_file
template("component.rb",
File.join("app/views/components", class_path, file_name,
"component.rb"))
end

def create_template_file
template("component.html.erb",
File.join("app/views/components", class_path, file_name,
"component.html.erb"))
end

def create_test_file
return if options[:skip_test]

template("component_test.rb",
File.join("test/views/components", class_path,
"#{file_name}_test.rb"))
end

def create_system_test_file
return if options[:skip_system_test]

template("component_system_test.rb",
File.join("test/system/views/components", class_path,
"#{file_name}_test.rb"))
end

def create_preview_file
return if options[:skip_preview]

template("preview.rb",
File.join("app/views/components", class_path, file_name,
"preview.rb"))
end

private

def parent_class
"ApplicationViewComponent"
end

def preview_parent_class
"ApplicationViewComponentPreview"
end

def initialize_signature
return if attributes.blank?

attributes.map { |attr| "option :#{attr.name}" }.join("\n ")
end
end