View Component Contrib
Installs ViewComponent and adds ViewComponent::Contrib extensions and configuration settings
Used 2498 times
V
Vladimir Dementyev
Usage
ViewComponent::Contrib contains extensions, patches and guides for working with ViewComponent.
This template:
This template:
- Installs ViewComponent and view_component-contrib gem.
- Configures ViewComponent paths.
- Adds ApplicationViewComponent and ApplicationViewComponentPreview classes.
- Configures testing framework (RSpec or Minitest).
- Adds required JS/CSS configuration.
- Adds a custom generator to create components.
Built by Evil Martians.
Run this command in your Rails app directory in the terminal:
rails app:template LOCATION="https://railsbytes.com/script/zJosO5"
Template Source
Review the code before running this template on your machine.
say "👋 Welcome to interactive ViewComponent installer and configurator. " \
"Make sure you've read the view_component-contrib guide: https://github.com/palkan/view_component-contrib"
run "bundle add view_component view_component-contrib --skip-install"
say_status :info, "✅ ViewComponent gems added"
DEFAULT_ROOT = "app/frontend/components"
root = ask("Where do you want to store your view components? (default: #{DEFAULT_ROOT})")
ROOT_PATH = root.present? && root.downcase != "n" ? root : DEFAULT_ROOT
root_paths = ROOT_PATH.split("/").map { |path| "\"#{path}\"" }.join(", ")
application "config.view_component.preview_paths << Rails.root.join(#{root_paths})"
application "config.autoload_paths << Rails.root.join(#{root_paths})"
say_status :info, "✅ ViewComponent paths configured"
file "#{ROOT_PATH}/application_view_component.rb",
ERB.new(
*[
<<~'TCODE'
class ApplicationViewComponent < ViewComponentContrib::Base
end
TCODE
], trim_mode: "<>").result(binding)
file "#{ROOT_PATH}/application_view_component_preview.rb",
ERB.new(
*[
<<~'TCODE'
class ApplicationViewComponentPreview < ViewComponentContrib::Preview::Base
self.abstract_class = true
end
TCODE
], trim_mode: "<>").result(binding)
say_status :info, "✅ ApplicationViewComponent and ApplicationViewComponentPreview classes added"
USE_RSPEC = File.directory?("spec")
TEST_ROOT_PATH = USE_RSPEC ? File.join("spec", ROOT_PATH.sub("app/", "")) : File.join("test", ROOT_PATH.sub("app/", ""))
TEST_SYSTEM_ROOT_PATH = USE_RSPEC ? File.join("spec", "system", ROOT_PATH.sub("app/", "")) : File.join("test", "system", ROOT_PATH.sub("app/", ""))
USE_DRY = yes? "Would you like to use dry-initializer in your component classes? (y/n)"
if USE_DRY
run "bundle add dry-initializer --skip-install"
inject_into_file "#{ROOT_PATH}/application_view_component.rb", "\n extend Dry::Initializer", after: "class ApplicationViewComponent < ViewComponentContrib::Base"
say_status :info, "✅ Extended ApplicationViewComponent with Dry::Initializer"
end
initializer "view_component.rb",
ERB.new(
*[
<<~'TCODE'
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
TCODE
], trim_mode: "<>").result(binding)
say_status :info, "✅ Added ViewComponent initializer with required patches"
if USE_RSPEC
inject_into_file "spec/rails_helper.rb", after: "require \"rspec/rails\"\n" do
"require \"capybara/rspec\"\nrequire \"view_component/test_helpers\"\n"
end
inject_into_file "spec/rails_helper.rb", after: "RSpec.configure do |config|\n" do
<<-CODE
config.include ViewComponent::TestHelpers, type: :view_component
config.include Capybara::RSpecMatchers, type: :view_component
config.define_derived_metadata(file_path: %r{/#{TEST_ROOT_PATH}}) do |metadata|
metadata[:type] = :view_component
end
CODE
end
end
say_status :info, "✅ RSpec configured"
USE_STIMULUS = yes? "Do you use Stimulus? (y/n)"
if USE_STIMULUS
say "⚠️ See the discussion on how to configure your JS bundler to auto-load controllers: https://github.com/palkan/view_component-contrib/discussions/14"
end
USE_TAILWIND = yes? "Do you use TailwindCSS? (y/n)"
if USE_TAILWIND
# TODO: Use styled variants
else
say "⚠️ Check out PostCSS modules to keep your CSS isolated and closer to your components: https://github.com/palkan/view_component-contrib#isolating-css-with-postcss-modules"
end
if yes?("Would you like to create a custom generator for your setup? (y/n)")
template_choice_to_ext = {"1" => ".erb", "2" => ".haml", "3" => ".slim"}
template = ask "Which template processor do you use? (1) ERB, (2) Haml, (3) Slim, (0) Other"
TEMPLATE_EXT = template_choice_to_ext.fetch(template, "")
TEST_SUFFIX = USE_RSPEC ? 'spec' : 'test'
file "lib/generators/view_component/view_component_generator.rb", <<~CODE
# 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("#{ROOT_PATH}", class_path, file_name, "component.rb")
end
def create_template_file
template "component.html#{TEMPLATE_EXT}", File.join("#{ROOT_PATH}", class_path, file_name, "component.html#{TEMPLATE_EXT}")
end
def create_test_file
return if options[:skip_test]
template "component_#{TEST_SUFFIX}.rb", File.join("#{TEST_ROOT_PATH}", class_path, "\#{file_name}_#{TEST_SUFFIX}.rb")
end
def create_system_test_file
return if options[:skip_system_test]
template "component_system_#{TEST_SUFFIX}.rb", File.join("#{TEST_SYSTEM_ROOT_PATH}", class_path, "\#{file_name}_#{TEST_SUFFIX}.rb")
end
def create_preview_file
return if options[:skip_preview]
template "preview.rb", File.join("#{ROOT_PATH}", class_path, file_name, "preview.rb")
end
private
def parent_class
"ApplicationViewComponent"
end
def preview_parent_class
"ApplicationViewComponentPreview"
end
end
CODE
if USE_DRY
inject_into_file "lib/generators/view_component/view_component_generator.rb", before: "\nend" do
<<-CODE
def initialize_signature
return if attributes.blank?
attributes.map { |attr| "option :\#{attr.name}" }.join("\\n ")
end
CODE
end
file "lib/generators/view_component/templates/component.rb.tt",
<<~CODE
# frozen_string_literal: true
class <%= class_name %>::Component < <%= parent_class %>
with_collection_parameter :<%= singular_name %>
<%- if initialize_signature -%>
<%= initialize_signature %>
<%- end -%>
end
CODE
else
inject_into_file "lib/generators/view_component/view_component_generator.rb", before: "\nend" do
<<-CODE
def initialize_signature
return if attributes.blank?
attributes.map { |attr| "\#{attr.name}:" }.join(", ")
end
def initialize_body
attributes.map { |attr| "@\#{attr.name} = \#{attr.name}" }.join("\\n ")
end
CODE
end
file "lib/generators/view_component/templates/component.rb.tt",
<<~CODE
# frozen_string_literal: true
class <%= class_name %>::Component < <%= parent_class %>
with_collection_parameter :<%= singular_name %>
<%- if initialize_signature -%>
def initialize(<%= initialize_signature %>)
<%= initialize_body %>
end
<%- end -%>
end
CODE
end
if TEMPLATE_EXT == ".slim"
file "lib/generators/view_component/templates/component.html.slim.tt", <<~CODE
div Add <%= class_name %> template here
CODE
end
if TEMPLATE_EXT == ".erb"
file "lib/generators/view_component/templates/component.html.erb.tt", <<~CODE
<div>Add <%= class_name %> template here</div>
CODE
end
if TEMPLATE_EXT == ".haml"
file "lib/generators/view_component/templates/component.html.haml.tt", <<~CODE
%div Add <%= class_name %> template here
CODE
end
if TEMPLATE_EXT == ""
file "lib/generators/view_component/templates/component.html.tt", <<~CODE
<div>Add <%= class_name %> template here</div>
CODE
end
file "lib/generators/view_component/templates/preview.rb.tt", <<~CODE
# 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
CODE
if USE_RSPEC
file "lib/generators/view_component/templates/component_spec.rb.tt", <<~CODE
# frozen_string_literal: true
require "rails_helper"
describe <%= class_name %>::Component do
let(:options) { {} }
let(:component) { <%= class_name %>::Component.new(**options) }
subject { rendered_content }
it "renders" do
render_inline(component)
is_expected.to have_css "div"
end
end
CODE
file "lib/generators/view_component/templates/component_system_spec.rb.tt", <<~CODE
# frozen_string_literal: true
require "rails_helper"
describe "<%= file_name %> component" do
it "default preview" do
visit("/rails/view_components<%= File.join(class_path, file_name) %>/default")
# is_expected.to have_text "Hello!"
# click_on "Click me"
# is_expected.to have_text "Good-bye!"
end
end
CODE
else
file "lib/generators/view_component/templates/component_test.rb.tt", <<~CODE
# 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
CODE
file "lib/generators/view_component/templates/component_system_test.rb.tt", <<~CODE
# 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
CODE
end
file "lib/generators/view_component/USAGE", <<~CODE
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: #{ROOT_PATH}/profile/component.rb
Template: #{ROOT_PATH}/profile/component.html#{TEMPLATE_EXT}
Test: #{TEST_ROOT_PATH}/profile_component_#{TEST_SUFFIX}.rb
System Test: #{TEST_SYSTEM_ROOT_PATH}/profile_component_#{TEST_SUFFIX}.rb
Preview: #{ROOT_PATH}/profile/component_preview.rb
CODE
# Check if autoload_lib is configured
if File.file?("config/application.rb") && File.read("config/application.rb").include?("config.autoload_lib")
say_status :info, "⚠️ Make sure you configured autoload_lib to ignore the lib/generators folder"
end
end
say "Installing gems..."
Bundler.with_unbundled_env { run "bundle install" }
say_status :info, "✅ You're ready to rock!"