diff --git a/.erb-lint.yml b/.erb-lint.yml deleted file mode 100644 index a06830e01..000000000 --- a/.erb-lint.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -EnableDefaultLinters: true -exclude: - - '**/vendor/**/*' -linters: - ErbSafety: - enabled: true - FinalNewline: - enabled: true diff --git a/.erb_lint.yml b/.erb_lint.yml new file mode 100644 index 000000000..530f46bdc --- /dev/null +++ b/.erb_lint.yml @@ -0,0 +1,18 @@ +--- +EnableDefaultLinters: true +exclude: + - '**/vendor/**/*' +linters: + ErbSafety: + enabled: true + FinalNewline: + enabled: true + Rubocop: + enabled: true + rubocop_config: + inherit_from: + - .rubocop.yml + Layout/InitialIndentation: + Enabled: false + Layout/TrailingEmptyLines: + Enabled: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5bb8fc1a8..07abc2bcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,60 +23,20 @@ jobs: bundle exec appraisal rails-8.0 rake partial_benchmark bundle exec appraisal rails-8.0 rake translatable_benchmark test: - name: test (${{ matrix.rails_version }}, ${{ matrix.ruby_version }}, ${{ matrix.mode }}) + name: test (Rails ${{ matrix.rails_version }}, Ruby ${{ matrix.ruby_version }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - - ruby_version: "3.0" - rails_version: "6.1" - mode: "capture_patch_enabled" - - ruby_version: "3.0" - rails_version: "6.1" - mode: "capture_patch_disabled" - - ruby_version: "3.1" - rails_version: "7.0" - mode: "capture_patch_enabled" - - ruby_version: "3.1" - rails_version: "7.0" - mode: "capture_patch_disabled" - ruby_version: "3.2" rails_version: "7.1" - mode: "capture_patch_enabled" - - ruby_version: "3.2" - rails_version: "7.1" - mode: "capture_patch_disabled" - - ruby_version: "3.3" - rails_version: "7.2" - mode: "capture_patch_disabled" - ruby_version: "3.3" rails_version: "7.2" - mode: "capture_patch_enabled" - - ruby_version: "3.3" - rails_version: "8.0" - mode: "capture_patch_disabled" - - ruby_version: "3.3" - rails_version: "8.0" - mode: "capture_patch_enabled" - - ruby_version: "3.4" - rails_version: "8.0" - mode: "capture_patch_disabled" - ruby_version: "3.4" rails_version: "8.0" - mode: "capture_patch_enabled" - - ruby_version: "3.4" - rails_version: "main" - mode: "capture_patch_disabled" - - ruby_version: "3.4" - rails_version: "main" - mode: "capture_patch_enabled" - - ruby_version: "3.5" - rails_version: "8.0" - mode: "capture_patch_disabled" - - ruby_version: "3.5" - rails_version: "8.0" - mode: "capture_patch_enabled" + # - ruby_version: "head" + # rails_version: "main" env: BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.rails_version }}.gemfile steps: @@ -87,17 +47,13 @@ jobs: ruby-version: ${{ matrix.ruby_version }} bundler-cache: true - name: Build and test with Rake - # Code-reloading isn't compatible with simplecov, so we need to run once - # to collect coverage, and again to test reloads. run: | - bundle exec appraisal rails-${{ matrix.rails_version }} bundle - MEASURE_COVERAGE=true bundle exec appraisal rails-${{ matrix.rails_version }} rake - ENABLE_RELOADING=true bundle exec appraisal rails-${{ matrix.rails_version }} rake + bundle exec appraisal rails-${{ matrix.rails_version }} bundle --quiet + MEASURE_COVERAGE=true bundle exec appraisal rails-${{ matrix.rails_version }} rake all_tests env: RAISE_ON_WARNING: 1 RAILS_VERSION: ${{ matrix.rails_version }} RUBY_VERSION: ${{ matrix.ruby_version }} - CAPTURE_PATCH_ENABLED: ${{ matrix.mode == 'capture_patch_enabled' && 'true' || 'false' }} - name: Upload coverage results uses: actions/upload-artifact@v4.4.0 if: always() @@ -113,6 +69,7 @@ jobs: with: repository: 'primer/view_components' path: 'primer_view_components' + ref: '20250603-vc-compat' - uses: actions/checkout@v4.1.1 with: path: 'view_component' @@ -132,10 +89,10 @@ jobs: cd primer_view_components npm ci cd demo && npm ci && cd .. - bundle && bundle exec rake + bundle --quiet && bundle exec rake env: VIEW_COMPONENT_PATH: ../view_component - RAILS_VERSION: '7.1.1' + RAILS_VERSION: '7.1.5' PARALLEL_WORKERS: '1' coverage: needs: test diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 36b2b6faa..cb7421a8c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -74,12 +74,12 @@ jobs: - uses: actions/cache@v4 with: path: vendor/bundle - key: gems-build-rails-7.1-ruby-3.2-${{ hashFiles('**/Gemfile.lock') }} + key: gems-build-rails-8-ruby-3.2-${{ hashFiles('**/Gemfile.lock') }} - name: Lint with Rubocop and ERB Lint run: | bundle config path vendor/bundle bundle update bundle exec standardrb -r "rubocop-md" - bundle exec erblint **/*.html.erb + bundle exec erb_lint --lint-all env: - RAILS_VERSION: '~> 7.1.0' + RAILS_VERSION: '~> 8' diff --git a/.gitignore b/.gitignore index 340f093f5..dfe6728fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.gem *.rbc .ruby-version +.DS_Store /.config /coverage/assets /coverage/index.html diff --git a/.rubocop.yml b/.rubocop.yml index 93e28a2d5..a7861a1ed 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,6 @@ -ruby_version: 2.5 require: - standard - - "rubocop-md" +plugins: + - rubocop-md inherit_gem: standard: config/base.yml diff --git a/.standard.yml b/.standard.yml index 34cd5acf3..127dd6f8d 100644 --- a/.standard.yml +++ b/.standard.yml @@ -1,3 +1,3 @@ -ruby_version: 2.5 +ruby_version: 3.2 ignore: - 'docs/CHANGELOG.md' # Rubocop doesn't like our indenting of code examples diff --git a/Appraisals b/Appraisals index 29f1865a6..0b94d1e66 100644 --- a/Appraisals +++ b/Appraisals @@ -1,46 +1,26 @@ # frozen_string_literal: true -appraise "rails-6.1" do - gem "rails", "~> 6.1" - gem "tailwindcss-rails", "~> 2.0" - gem "sprockets-rails", "~> 3.4.2" - gem "concurrent-ruby", "1.3.4" - - # Required for Ruby 3.1.0 - gem "net-smtp", require: false - gem "net-imap", require: false - gem "net-pop", require: false - gem "turbo-rails", "~> 1" -end - -appraise "rails-7.0" do - gem "rails", "~> 7.0" - gem "tailwindcss-rails", "~> 2.0" - gem "turbo-rails", "~> 1" - gem "sprockets-rails", "~> 3.4.2" -end - appraise "rails-7.1" do gem "rails", "~> 7.1" - gem "tailwindcss-rails", "~> 2.0" + gem "tailwindcss-rails", "~> 2" gem "turbo-rails", "~> 1" - gem "sprockets-rails", "~> 3.4.2" end appraise "rails-7.2" do gem "rails", "~> 7.2" - gem "tailwindcss-rails", "~> 2.0" - gem "sprockets-rails", "~> 3.4.2" + gem "tailwindcss-rails", "~> 2" + gem "turbo-rails", "~> 2" end appraise "rails-8.0" do gem "rails", "~> 8.0" - gem "tailwindcss-rails", "~> 2.0" - gem "propshaft", "~> 1.1.0" + gem "tailwindcss-rails", "~> 2" + gem "turbo-rails", "~> 2" end appraise "rails-main" do + gem "rack", git: "https://github.com/rack/rack", ref: "8a4475a9f416a72e5b02bd7817e4a8ed684f29b0" gem "rails", github: "rails/rails", branch: "main" - gem "tailwindcss-rails", "~> 2.0" - gem "turbo-rails", "~> 1" + gem "tailwindcss-rails", "~> 2" + gem "turbo-rails", "~> 2" end diff --git a/Gemfile b/Gemfile index a16e443d5..dcbb20bd5 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,42 @@ source "https://rubygems.org" gemspec rails_version = (ENV["RAILS_VERSION"] || "~> 8").to_s + gem "rails", (rails_version == "main") ? {git: "https://github.com/rails/rails", ref: "main"} : rails_version ruby_version = (ENV["RUBY_VERSION"] || "~> 3.4").to_s ruby ruby_version + +group :development, :test do + gem "allocation_stats" + gem "appraisal", "~> 2" + gem "benchmark-ips", "~> 2" + gem "better_html" + gem "bundler", "~> 2" + gem "capybara", "~> 3" + gem "cuprite" + gem "dry-initializer", require: true + gem "erb_lint" + gem "haml", "~> 6" + gem "jbuilder", "~> 2" + gem "m", "~> 1" + gem "method_source", "~> 1" + gem "minitest", "~> 5" + gem "propshaft", "~> 1" + gem "puma", "~> 6" + gem "rake", "~> 13" + gem "rails-dom-testing", "~> 2.3.0" + gem "redis" + gem "rspec-rails", "~> 7" + gem "rubocop-md", "~> 2" + gem "selenium-webdriver", "~> 4" + gem "simplecov-console", "< 1" + gem "simplecov", "< 1" + gem "slim", "~> 5" + gem "sprockets-rails", "~> 3" + gem "standard", "~> 1" + gem "turbo-rails" + gem "warning" + gem "yard-activesupport-concern", "< 1" + gem "yard", "< 1" +end diff --git a/Gemfile.lock b/Gemfile.lock index 7ea7c4cc1..316e2f91f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,10 +1,9 @@ PATH remote: . specs: - view_component (3.23.2) - activesupport (>= 5.2.0, < 8.1) + view_component (4.0.0.rc1) + activesupport (>= 7.1.0, < 8.1) concurrent-ruby (~> 1) - method_source (~> 1.0) GEM remote: https://rubygems.org/ @@ -91,7 +90,7 @@ GEM ast (2.4.3) base64 (0.3.0) benchmark (0.4.1) - benchmark-ips (2.13.0) + benchmark-ips (2.14.0) better_html (2.1.1) actionview (>= 6.0) activesupport (>= 6.0) @@ -110,7 +109,6 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - coderay (1.1.3) concurrent-ruby (1.3.5) connection_pool (2.5.3) crass (1.0.6) @@ -118,12 +116,10 @@ GEM capybara (~> 3.0) ferrum (~> 0.17.0) date (3.4.1) - debug (1.10.0) - irb (~> 1.10) - reline (>= 0.3.8) diff-lcs (1.6.2) docile (1.4.1) drb (2.2.3) + dry-initializer (3.2.0) erb (5.0.1) erb_lint (0.9.0) activesupport @@ -171,12 +167,10 @@ GEM net-pop net-smtp marcel (1.0.4) - matrix (0.4.2) + matrix (0.4.3) method_source (1.1.0) mini_mime (1.1.5) - mini_portile2 (2.8.9) minitest (5.25.5) - mutex_m (0.3.0) net-imap (0.5.8) date net-protocol @@ -187,8 +181,21 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.18.8) - mini_portile2 (~> 2.8.2) + nokogiri (1.18.8-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.8-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.18.8-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.8-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.18.8-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.8-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.18.8-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.8-x86_64-linux-musl) racc (~> 1.4) parallel (1.27.0) parser (3.3.8.0) @@ -203,9 +210,6 @@ GEM activesupport (>= 7.0.0) rack railties (>= 7.0.0) - pry (0.15.2) - coderay (~> 1.1) - method_source (~> 1.0) psych (5.2.6) date stringio @@ -252,9 +256,13 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.3.0) - rdoc (6.14.0) + rdoc (6.14.1) erb psych (>= 4.0.0) + redis (5.4.0) + redis-client (>= 0.22.0) + redis-client (0.25.0) + connection_pool regexp_parser (2.10.0) reline (0.6.1) io-console (~> 0.5) @@ -267,14 +275,14 @@ GEM rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (5.1.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) + rspec-rails (7.1.1) + actionpack (>= 7.0) + activesupport (>= 7.0) + railties (>= 7.0) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) rspec-support (3.13.4) rubocop (1.75.8) json (~> 2.3) @@ -290,8 +298,9 @@ GEM rubocop-ast (1.45.1) parser (>= 3.3.7.2) prism (~> 1.4) - rubocop-md (1.2.4) - rubocop (>= 1.45) + rubocop-md (2.0.1) + lint_roller (~> 1.1) + rubocop (>= 1.72.1) rubocop-performance (1.25.0) lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) @@ -299,7 +308,9 @@ GEM ruby-progressbar (1.13.0) rubyzip (2.4.1) securerandom (0.4.1) - selenium-webdriver (4.9.0) + selenium-webdriver (4.33.0) + base64 (~> 0.2) + logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) @@ -321,9 +332,9 @@ GEM concurrent-ruby (~> 1.0) logger rack (>= 2.2.4, < 4) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) sprockets (>= 3.0.0) standard (1.50.0) language_server-protocol (~> 3.17.0.2) @@ -344,10 +355,9 @@ GEM thor (1.3.2) tilt (2.6.0) timeout (0.4.3) - turbo-rails (1.5.0) - actionpack (>= 6.0.0) - activejob (>= 6.0.0) - railties (>= 6.0.0) + turbo-rails (2.0.16) + actionpack (>= 7.1.0) + railties (>= 7.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (3.1.4) @@ -370,50 +380,52 @@ GEM zeitwerk (2.7.3) PLATFORMS - ruby + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES - allocation_stats (~> 0.1.5) - appraisal (~> 2.4) - base64 - benchmark-ips (~> 2.13.0) + allocation_stats + appraisal (~> 2) + benchmark-ips (~> 2) better_html - bigdecimal bundler (~> 2) capybara (~> 3) - cuprite (~> 0.15) - debug - drb + cuprite + dry-initializer erb_lint haml (~> 6) jbuilder (~> 2) m (~> 1) - minitest (~> 5.18) - mutex_m - net-imap - net-pop - net-smtp - propshaft (~> 1.1.0) - pry (~> 0.13) + method_source (~> 1) + minitest (~> 5) + propshaft (~> 1) puma (~> 6) rails (~> 8) - rake (~> 13.0) - rspec-rails (~> 5) - rubocop-md (~> 1) - selenium-webdriver (= 4.9.0) - simplecov (~> 0.22.0) - simplecov-console (~> 0.9.1) - slim (~> 5.1) - sprockets-rails (~> 3.4.2) + rails-dom-testing (~> 2.3.0) + rake (~> 13) + redis + rspec-rails (~> 7) + rubocop-md (~> 2) + selenium-webdriver (~> 4) + simplecov (< 1) + simplecov-console (< 1) + slim (~> 5) + sprockets-rails (~> 3) standard (~> 1) - turbo-rails (~> 1) + turbo-rails view_component! warning - yard (~> 0.9.34) - yard-activesupport-concern (~> 0.0.1) + yard (< 1) + yard-activesupport-concern (< 1) RUBY VERSION - ruby 3.4.1p0 + ruby 3.4.2p28 BUNDLED WITH - 2.5.3 + 2.6.2 diff --git a/Rakefile b/Rakefile index bb04d1133..299b3a9cc 100644 --- a/Rakefile +++ b/Rakefile @@ -2,8 +2,14 @@ require "bundler/gem_tasks" require "rake/testtask" +require "rspec/core/rake_task" require "yard" require "yard/mattr_accessor_handler" +require "rails/version" +require "simplecov" +require "simplecov-console" + +RSpec::Core::RakeTask.new(:spec) Rake::TestTask.new(:test) do |t| t.libs << "test" @@ -17,18 +23,6 @@ Rake::TestTask.new(:engine_test) do |t| t.test_files = FileList["test/test_engine/**/*_test.rb"] end -Rake::TestTask.new(:docs_test) do |t| - t.libs << "test" - t.libs << "lib" - t.test_files = FileList["test/docs/*_test.rb"] -end - -begin - require "rspec/core/rake_task" - RSpec::Core::RakeTask.new(:spec) -rescue LoadError -end - desc "Runs benchmarks against components" task :partial_benchmark do ruby "./performance/partial_benchmark.rb" @@ -60,18 +54,13 @@ namespace :coverage do end namespace :docs do - # Build api.md documentation page from YARD comments. task :build do YARD::Rake::YardocTask.new do |t| - t.options = ["--no-output"] + t.options = ["--no-output", "-q"] end - puts "Building YARD documentation." - Rake::Task["yard"].execute - puts "Converting YARD documentation to Markdown files." - registry = YARD::RegistryStore.new registry.load!(".yardoc") @@ -101,7 +90,6 @@ namespace :docs do require "rails" require "action_controller" require "view_component" - ViewComponent::Base.config.view_component_path = "docs" require "docs/docs_builder_component" error_keys = registry.keys.select { |key| key.to_s.include?("Error::MESSAGE") }.map(&:to_s) @@ -142,4 +130,20 @@ namespace :docs do end end -task default: [:docs_test, :test, :engine_test, :spec] +task :all_tests do + ENV["RAILS_ENV"] = "test" + + if ENV["MEASURE_COVERAGE"] + SimpleCov.start do + command_name "rails#{Rails::VERSION::STRING}-ruby#{RUBY_VERSION}" + enable_coverage :branch + formatter SimpleCov::Formatter::Console + end + end + + Rake::Task["test"].invoke + Rake::Task["engine_test"].invoke + Rake::Task["spec"].invoke +end + +task default: [:all_tests] diff --git a/app/assets/vendor/prism.css b/app/assets/vendor/prism.css deleted file mode 100644 index 1166d38f8..000000000 --- a/app/assets/vendor/prism.css +++ /dev/null @@ -1,4 +0,0 @@ -/* PrismJS 1.28.0 -https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+clike+erb+haml+markup-templating+ruby&plugins=line-highlight+highlight-keywords+normalize-whitespace */ -code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green} -pre[data-line]{position:relative;padding:1em 0 1em 3em}.line-highlight{position:absolute;left:0;right:0;padding:inherit 0;margin-top:1em;background:hsla(24,20%,50%,.08);background:linear-gradient(to right,hsla(24,20%,50%,.1) 70%,hsla(24,20%,50%,0));pointer-events:none;line-height:inherit;white-space:pre}@media print{.line-highlight{-webkit-print-color-adjust:exact;color-adjust:exact}}.line-highlight:before,.line-highlight[data-end]:after{content:attr(data-start);position:absolute;top:.4em;left:.6em;min-width:1em;padding:0 .5em;background-color:hsla(24,20%,50%,.4);color:#f4f1ef;font:bold 65%/1.5 sans-serif;text-align:center;vertical-align:.3em;border-radius:999px;text-shadow:none;box-shadow:0 1px #fff}.line-highlight[data-end]:after{content:attr(data-end);top:auto;bottom:.4em}.line-numbers .line-highlight:after,.line-numbers .line-highlight:before{content:none}pre[id].linkable-line-numbers span.line-numbers-rows{pointer-events:all}pre[id].linkable-line-numbers span.line-numbers-rows>span:before{cursor:pointer}pre[id].linkable-line-numbers span.line-numbers-rows>span:hover:before{background-color:rgba(128,128,128,.2)} diff --git a/app/assets/vendor/prism.min.js b/app/assets/vendor/prism.min.js deleted file mode 100644 index 7841a2e24..000000000 --- a/app/assets/vendor/prism.min.js +++ /dev/null @@ -1,12 +0,0 @@ -/* PrismJS 1.28.0 -https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+clike+erb+haml+markup-templating+ruby&plugins=line-highlight+highlight-keywords+normalize-whitespace */ -var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); -Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var t={"included-cdata":{pattern://i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; -Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; -!function(e){e.languages.ruby=e.languages.extend("clike",{comment:{pattern:/#.*|^=begin\s[\s\S]*?^=end/m,greedy:!0},"class-name":{pattern:/(\b(?:class|module)\s+|\bcatch\s+\()[\w.\\]+|\b[A-Z_]\w*(?=\s*\.\s*new\b)/,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:BEGIN|END|alias|and|begin|break|case|class|def|define_method|defined|do|each|else|elsif|end|ensure|extend|for|if|in|include|module|new|next|nil|not|or|prepend|private|protected|public|raise|redo|require|rescue|retry|return|self|super|then|throw|undef|unless|until|when|while|yield)\b/,operator:/\.{2,3}|&\.|===||[!=]?~|(?:&&|\|\||<<|>>|\*\*|[+\-*/%<>!^&|=])=?|[?:]/,punctuation:/[(){}[\].,;]/}),e.languages.insertBefore("ruby","operator",{"double-colon":{pattern:/::/,alias:"punctuation"}});var n={pattern:/((?:^|[^\\])(?:\\{2})*)#\{(?:[^{}]|\{[^{}]*\})*\}/,lookbehind:!0,inside:{content:{pattern:/^(#\{)[\s\S]+(?=\}$)/,lookbehind:!0,inside:e.languages.ruby},delimiter:{pattern:/^#\{|\}$/,alias:"punctuation"}}};delete e.languages.ruby.function;var t="(?:"+["([^a-zA-Z0-9\\s{(\\[<=])(?:(?!\\1)[^\\\\]|\\\\[^])*\\1","\\((?:[^()\\\\]|\\\\[^]|\\((?:[^()\\\\]|\\\\[^])*\\))*\\)","\\{(?:[^{}\\\\]|\\\\[^]|\\{(?:[^{}\\\\]|\\\\[^])*\\})*\\}","\\[(?:[^\\[\\]\\\\]|\\\\[^]|\\[(?:[^\\[\\]\\\\]|\\\\[^])*\\])*\\]","<(?:[^<>\\\\]|\\\\[^]|<(?:[^<>\\\\]|\\\\[^])*>)*>"].join("|")+")",i='(?:"(?:\\\\.|[^"\\\\\r\n])*"|(?:\\b[a-zA-Z_]\\w*|[^\\s\0-\\x7F]+)[?!]?|\\$.)';e.languages.insertBefore("ruby","keyword",{"regex-literal":[{pattern:RegExp("%r"+t+"[egimnosux]{0,6}"),greedy:!0,inside:{interpolation:n,regex:/[\s\S]+/}},{pattern:/(^|[^/])\/(?!\/)(?:\[[^\r\n\]]+\]|\\.|[^[/\\\r\n])+\/[egimnosux]{0,6}(?=\s*(?:$|[\r\n,.;})#]))/,lookbehind:!0,greedy:!0,inside:{interpolation:n,regex:/[\s\S]+/}}],variable:/[@$]+[a-zA-Z_]\w*(?:[?!]|\b)/,symbol:[{pattern:RegExp("(^|[^:]):"+i),lookbehind:!0,greedy:!0},{pattern:RegExp("([\r\n{(,][ \t]*)"+i+"(?=:(?!:))"),lookbehind:!0,greedy:!0}],"method-definition":{pattern:/(\bdef\s+)\w+(?:\s*\.\s*\w+)?/,lookbehind:!0,inside:{function:/\b\w+$/,keyword:/^self\b/,"class-name":/^\w+/,punctuation:/\./}}}),e.languages.insertBefore("ruby","string",{"string-literal":[{pattern:RegExp("%[qQiIwWs]?"+t),greedy:!0,inside:{interpolation:n,string:/[\s\S]+/}},{pattern:/("|')(?:#\{[^}]+\}|#(?!\{)|\\(?:\r\n|[\s\S])|(?!\1)[^\\#\r\n])*\1/,greedy:!0,inside:{interpolation:n,string:/[\s\S]+/}},{pattern:/<<[-~]?([a-z_]\w*)[\r\n](?:.*[\r\n])*?[\t ]*\1/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<[-~]?[a-z_]\w*|\b[a-z_]\w*$/i,inside:{symbol:/\b\w+/,punctuation:/^<<[-~]?/}},interpolation:n,string:/[\s\S]+/}},{pattern:/<<[-~]?'([a-z_]\w*)'[\r\n](?:.*[\r\n])*?[\t ]*\1/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<[-~]?'[a-z_]\w*'|\b[a-z_]\w*$/i,inside:{symbol:/\b\w+/,punctuation:/^<<[-~]?'|'$/}},string:/[\s\S]+/}}],"command-literal":[{pattern:RegExp("%x"+t),greedy:!0,inside:{interpolation:n,command:{pattern:/[\s\S]+/,alias:"string"}}},{pattern:/`(?:#\{[^}]+\}|#(?!\{)|\\(?:\r\n|[\s\S])|[^\\`#\r\n])*`/,greedy:!0,inside:{interpolation:n,command:{pattern:/[\s\S]+/,alias:"string"}}}]}),delete e.languages.ruby.string,e.languages.insertBefore("ruby","number",{builtin:/\b(?:Array|Bignum|Binding|Class|Continuation|Dir|Exception|FalseClass|File|Fixnum|Float|Hash|IO|Integer|MatchData|Method|Module|NilClass|Numeric|Object|Proc|Range|Regexp|Stat|String|Struct|Symbol|TMS|Thread|ThreadGroup|Time|TrueClass)\b/,constant:/\b[A-Z][A-Z0-9_]*(?:[?!]|\b)/}),e.languages.rb=e.languages.ruby}(Prism); -!function(e){function n(e,n){return"___"+e.toUpperCase()+n+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(t,a,r,o){if(t.language===a){var c=t.tokenStack=[];t.code=t.code.replace(r,(function(e){if("function"==typeof o&&!o(e))return e;for(var r,i=c.length;-1!==t.code.indexOf(r=n(a,i));)++i;return c[i]=e,r})),t.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(t,a){if(t.language===a&&t.tokenStack){t.grammar=e.languages[a];var r=0,o=Object.keys(t.tokenStack);!function c(i){for(var u=0;u=o.length);u++){var g=i[u];if("string"==typeof g||g.content&&"string"==typeof g.content){var l=o[r],s=t.tokenStack[l],f="string"==typeof g?g:g.content,p=n(a,l),k=f.indexOf(p);if(k>-1){++r;var m=f.substring(0,k),d=new e.Token(a,e.tokenize(s,t.grammar),"language-"+a,s),h=f.substring(k+p.length),v=[];m&&v.push.apply(v,c([m])),v.push(d),h&&v.push.apply(v,c([h])),"string"==typeof g?i.splice.apply(i,[u,1].concat(v)):g.content=v}}else g.content&&c(g.content)}return i}(t.tokens)}}}})}(Prism); -!function(e){e.languages.erb={delimiter:{pattern:/^(\s*)<%=?|%>(?=\s*$)/,lookbehind:!0,alias:"punctuation"},ruby:{pattern:/\s*\S[\s\S]*/,alias:"language-ruby",inside:e.languages.ruby}},e.hooks.add("before-tokenize",(function(n){e.languages["markup-templating"].buildPlaceholders(n,"erb",/<%=?(?:[^\r\n]|[\r\n](?!=begin)|[\r\n]=begin\s(?:[^\r\n]|[\r\n](?!=end))*[\r\n]=end)+?%>/g)})),e.hooks.add("after-tokenize",(function(n){e.languages["markup-templating"].tokenizePlaceholders(n,"erb")}))}(Prism); -!function(n){n.languages.haml={"multiline-comment":{pattern:/((?:^|\r?\n|\r)([\t ]*))(?:\/|-#).*(?:(?:\r?\n|\r)\2[\t ].+)*/,lookbehind:!0,alias:"comment"},"multiline-code":[{pattern:/((?:^|\r?\n|\r)([\t ]*)(?:[~-]|[&!]?=)).*,[\t ]*(?:(?:\r?\n|\r)\2[\t ].*,[\t ]*)*(?:(?:\r?\n|\r)\2[\t ].+)/,lookbehind:!0,inside:n.languages.ruby},{pattern:/((?:^|\r?\n|\r)([\t ]*)(?:[~-]|[&!]?=)).*\|[\t ]*(?:(?:\r?\n|\r)\2[\t ].*\|[\t ]*)*/,lookbehind:!0,inside:n.languages.ruby}],filter:{pattern:/((?:^|\r?\n|\r)([\t ]*)):[\w-]+(?:(?:\r?\n|\r)(?:\2[\t ].+|\s*?(?=\r?\n|\r)))+/,lookbehind:!0,inside:{"filter-name":{pattern:/^:[\w-]+/,alias:"symbol"}}},markup:{pattern:/((?:^|\r?\n|\r)[\t ]*)<.+/,lookbehind:!0,inside:n.languages.markup},doctype:{pattern:/((?:^|\r?\n|\r)[\t ]*)!!!(?: .+)?/,lookbehind:!0},tag:{pattern:/((?:^|\r?\n|\r)[\t ]*)[%.#][\w\-#.]*[\w\-](?:\([^)]+\)|\{(?:\{[^}]+\}|[^{}])+\}|\[[^\]]+\])*[\/<>]*/,lookbehind:!0,inside:{attributes:[{pattern:/(^|[^#])\{(?:\{[^}]+\}|[^{}])+\}/,lookbehind:!0,inside:n.languages.ruby},{pattern:/\([^)]+\)/,inside:{"attr-value":{pattern:/(=\s*)(?:"(?:\\.|[^\\"\r\n])*"|[^)\s]+)/,lookbehind:!0},"attr-name":/[\w:-]+(?=\s*!?=|\s*[,)])/,punctuation:/[=(),]/}},{pattern:/\[[^\]]+\]/,inside:n.languages.ruby}],punctuation:/[<>]/}},code:{pattern:/((?:^|\r?\n|\r)[\t ]*(?:[~-]|[&!]?=)).+/,lookbehind:!0,inside:n.languages.ruby},interpolation:{pattern:/#\{[^}]+\}/,inside:{delimiter:{pattern:/^#\{|\}$/,alias:"punctuation"},ruby:{pattern:/[\s\S]+/,inside:n.languages.ruby}}},punctuation:{pattern:/((?:^|\r?\n|\r)[\t ]*)[~=\-&!]+/,lookbehind:!0}};for(var e=["css",{filter:"coffee",language:"coffeescript"},"erb","javascript","less","markdown","ruby","scss","textile"],t={},r=0,a=e.length;r ",document.body.appendChild(t),e=38===t.offsetHeight,document.body.removeChild(t)}return e}()?parseInt:parseFloat)(getComputedStyle(o).lineHeight),p=Prism.util.isActive(o,t),g=o.querySelector("code"),m=p?o:g||o,v=[],y=g.textContent.match(n),b=y?y.length+1:1,A=g&&m!=g?function(e,t){var i=getComputedStyle(e),n=getComputedStyle(t);function r(e){return+e.substr(0,e.length-2)}return t.offsetTop+r(n.borderTopWidth)+r(n.paddingTop)-r(i.paddingTop)}(o,g):0;h.forEach((function(e){var t=e.split("-"),i=+t[0],n=+t[1]||i;if(!((n=Math.min(b,n))i&&r.setAttribute("data-end",String(n)),r.style.top=(i-d-1)*f+A+"px",r.textContent=new Array(n-i+2).join(" \n")}));v.push((function(){r.style.width=o.scrollWidth+"px"})),v.push((function(){m.appendChild(r)}))}}));var P=o.id;if(p&&Prism.util.isActive(o,i)&&P){l(o,i)||v.push((function(){o.classList.add(i)}));var E=parseInt(o.getAttribute("data-start")||"1");s(".line-numbers-rows > span",o).forEach((function(e,t){var i=t+E;e.onclick=function(){var e=P+"."+i;r=!1,location.hash=e,setTimeout((function(){r=!0}),1)}}))}return function(){v.forEach(a)}}};var o=0;Prism.hooks.add("before-sanity-check",(function(e){var t=e.element.parentElement;if(u(t)){var i=0;s(".line-highlight",t).forEach((function(e){i+=e.textContent.length,e.parentNode.removeChild(e)})),i&&/^(?: \n)+$/.test(e.code.slice(-i))&&(e.code=e.code.slice(0,-i))}})),Prism.hooks.add("complete",(function e(i){var n=i.element.parentElement;if(u(n)){clearTimeout(o);var r=Prism.plugins.lineNumbers,s=i.plugins&&i.plugins.lineNumbers;l(n,t)&&r&&!s?Prism.hooks.add("line-numbers",e):(Prism.plugins.lineHighlight.highlightLines(n)(),o=setTimeout(c,1))}})),window.addEventListener("hashchange",c),window.addEventListener("resize",(function(){s("pre").filter(u).map((function(e){return Prism.plugins.lineHighlight.highlightLines(e)})).forEach(a)}))}function s(e,t){return Array.prototype.slice.call((t||document).querySelectorAll(e))}function l(e,t){return e.classList.contains(t)}function a(e){e()}function u(e){return!!(e&&/pre/i.test(e.nodeName)&&(e.hasAttribute("data-line")||e.id&&Prism.util.isActive(e,i)))}function c(){var e=location.hash.slice(1);s(".temporary.line-highlight").forEach((function(e){e.parentNode.removeChild(e)}));var t=(e.match(/\.([\d,-]+)$/)||[,""])[1];if(t&&!document.getElementById(e)){var i=e.slice(0,e.lastIndexOf(".")),n=document.getElementById(i);n&&(n.hasAttribute("data-line")||n.setAttribute("data-line",""),Prism.plugins.lineHighlight.highlightLines(n,t,"temporary ")(),r&&document.querySelector(".temporary.line-highlight").scrollIntoView())}}}(); -"undefined"!=typeof Prism&&Prism.hooks.add("wrap",(function(e){"keyword"===e.type&&e.classes.push("keyword-"+e.content)})); -!function(){if("undefined"!=typeof Prism){var e=Object.assign||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e},t={"remove-trailing":"boolean","remove-indent":"boolean","left-trim":"boolean","right-trim":"boolean","break-lines":"number",indent:"number","remove-initial-line-feed":"boolean","tabs-to-spaces":"number","spaces-to-tabs":"number"};n.prototype={setDefaults:function(t){this.defaults=e(this.defaults,t)},normalize:function(t,n){for(var r in n=e(this.defaults,n)){var i=r.replace(/-(\w)/g,(function(e,t){return t.toUpperCase()}));"normalize"!==r&&"setDefaults"!==i&&n[r]&&this[i]&&(t=this[i].call(this,t,n[r]))}return t},leftTrim:function(e){return e.replace(/^\s+/,"")},rightTrim:function(e){return e.replace(/\s+$/,"")},tabsToSpaces:function(e,t){return t=0|t||4,e.replace(/\t/g,new Array(++t).join(" "))},spacesToTabs:function(e,t){return t=0|t||4,e.replace(RegExp(" {"+t+"}","g"),"\t")},removeTrailing:function(e){return e.replace(/\s*?$/gm,"")},removeInitialLineFeed:function(e){return e.replace(/^(?:\r?\n|\r)/,"")},removeIndent:function(e){var t=e.match(/^[^\S\n\r]*(?=\S)/gm);return t&&t[0].length?(t.sort((function(e,t){return e.length-t.length})),t[0].length?e.replace(RegExp("^"+t[0],"gm"),""):e):e},indent:function(e,t){return e.replace(/^[^\S\n\r]*(?=\S)/gm,new Array(++t).join("\t")+"$&")},breakLines:function(e,t){t=!0===t?80:0|t||80;for(var n=e.split("\n"),i=0;it&&(o[l]="\n"+o[l],a=s)}n[i]=o.join("")}return n.join("\n")}},"undefined"!=typeof module&&module.exports&&(module.exports=n),Prism.plugins.NormalizeWhitespace=new n({"remove-trailing":!0,"remove-indent":!0,"left-trim":!0,"right-trim":!0}),Prism.hooks.add("before-sanity-check",(function(e){var n=Prism.plugins.NormalizeWhitespace;if((!e.settings||!1!==e.settings["whitespace-normalization"])&&Prism.util.isActive(e.element,"whitespace-normalization",!0))if(e.element&&e.element.parentNode||!e.code){var r=e.element.parentNode;if(e.code&&r&&"pre"===r.nodeName.toLowerCase()){for(var i in null==e.settings&&(e.settings={}),t)if(Object.hasOwnProperty.call(t,i)){var o=t[i];if(r.hasAttribute("data-"+i))try{var a=JSON.parse(r.getAttribute("data-"+i)||"true");typeof a===o&&(e.settings[i]=a)}catch(e){}}for(var l=r.childNodes,s="",c="",u=!1,m=0;m= 6.1 || template.source.present? - { - source: template.source, - prism_language_name: prism_language_name_by_template(template: template) - } - # :nocov: - else - # Fetch template source via finding it through preview paths - # to accomodate source view when exclusively using templates - # for previews for Rails < 6.1. - all_template_paths = ViewComponent::Base.config.preview_paths.map do |preview_path| - Dir.glob("#{preview_path}/**/*") - end.flatten - - # Search for templates the contain `html`. - matching_templates = all_template_paths.find_all do |path| - path =~ /#{template_identifier}*.(html)/ - end - - raise ViewComponent::NoMatchingTemplatesForPreviewError.new(template_identifier) if matching_templates.empty? - raise ViewComponent::MultipleMatchingTemplatesForPreviewError.new(template_identifier) if matching_templates.size > 1 - - template_file_path = matching_templates.first - template_source = File.read(template_file_path) - prism_language_name = prism_language_name_by_template_path(template_file_path: template_file_path) - - { - source: template_source, - prism_language_name: prism_language_name - } - end - # :nocov: - end - - private - - def prism_language_name_by_template(template:) - language = template.identifier.split(".").last - - return FALLBACK_LANGUAGE unless AVAILABLE_PRISM_LANGUAGES.include? language - - language - end - - # :nocov: - def prism_language_name_by_template_path(template_file_path:) - language = template_file_path.gsub(".html", "").split(".").last - - return FALLBACK_LANGUAGE unless AVAILABLE_PRISM_LANGUAGES.include? language - - language - end - # :nocov: - - def serve_static_preview_assets? - ViewComponent::Base.config.show_previews && Rails.application.config.public_file_server.enabled - end -end diff --git a/app/views/test_mailer/test_asset_email.html.erb b/app/views/test_mailer/test_asset_email.html.erb new file mode 100644 index 000000000..d93e535c3 --- /dev/null +++ b/app/views/test_mailer/test_asset_email.html.erb @@ -0,0 +1 @@ +<%= render AssetComponent.new %> diff --git a/app/views/test_mailer/test_url_email.html.erb b/app/views/test_mailer/test_url_email.html.erb new file mode 100644 index 000000000..233cedbd9 --- /dev/null +++ b/app/views/test_mailer/test_url_email.html.erb @@ -0,0 +1 @@ +<%= render(UrlForMailerComponent.new) %> diff --git a/app/views/view_components/_preview_source.html.erb b/app/views/view_components/_preview_source.html.erb deleted file mode 100644 index 65675189a..000000000 --- a/app/views/view_components/_preview_source.html.erb +++ /dev/null @@ -1,17 +0,0 @@ - -
-

Source:

-
-    <% if @render_args[:component] %>
-      
-        <%= h @preview.preview_source(@example_name) %>
-      
-    <% else %>
-      <% template_data = find_template_data_for_preview_source(lookup_context: @view_renderer.lookup_context, template_identifier: @render_args[:template]) %>
-      
-        <%= h template_data[:source] %>
-      
-    <% end %>
-  
-
- diff --git a/app/views/view_components/preview.html.erb b/app/views/view_components/preview.html.erb index f364fd725..44b7e8503 100644 --- a/app/views/view_components/preview.html.erb +++ b/app/views/view_components/preview.html.erb @@ -1,13 +1,5 @@ <% if @render_args[:component] %> - <% if ViewComponent::Base.config.render_monkey_patch_enabled || Rails.version.to_f >= 6.1 %> - <%= render(@render_args[:component], @render_args[:args], &@render_args[:block]) %> - <% else %> - <%= render_component(@render_args[:component], &@render_args[:block]) %> - <% end %> + <%= render(@render_args[:component], @render_args[:args], &@render_args[:block]) %> <% else %> <%= render template: @render_args[:template], locals: @render_args[:locals] || {} %> <% end %> - -<% if ViewComponent::Base.config.show_previews_source %> - <%= preview_source %> -<% end %> diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 732c6670a..8b6d735d7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,287 @@ nav_order: 6 ## main +## 4.0.0.rc1 + +Almost six years after releasing [v1.0.0](https://github.com/ViewComponent/view_component/releases/tag/v1.0.0), we're proud to ship the first release candidate of ViewComponent 4. This release marks a shift towards a Long Term Support model for the project, having reached significant feature maturity. While contributions are always welcome, we're unlikely to accept further breaking changes or major feature additions. + +Please report any issues at [https://github.com/ViewComponent/view_component/issues](https://github.com/ViewComponent/view_component/issues). + +### Breaking changes (production) + +* Remove dependency on `ActionView::Base`, eliminating the need for capture compatibility patch. In some edge cases, this change may require switching to use the `helpers.` proxy. +* Require [non-EOL](https://endoflife.date/rails) Rails (`>= 7.1.0`) and Ruby (`>= 3.2.0`). +* Remove `render_component` and `render` monkey patch configured with `render_monkey_patch_enabled`. +* Remove deprecated `use_helper(s)`. Use `include MyHelper` or `helpers.` proxy instead. +* Support compatibility with `Dry::Initializer`. As a result, `EmptyOrInvalidInitializerError` will no longer be raised. +* Remove default initializer from `ViewComponent::Base`. Previously, `ViewComponent::Base` defined a catch-all initializer that allowed components without an initializer defined to be passed arbitrary arguments. +* Remove `use_deprecated_instrumentation_name` configuration option. Events will always use `render.view_component` name. +* Remove unnecessary `#format` methods that returned `nil`. +* Remove support for variant names containing `.` to be consistent with Rails. +* Rename internal methods to have `__vc_` prefix if they shouldn't be used by consumers. Make internal constants private. Make `Collection#components`, `Slotable#register_polymorphic_slot` private. Remove unused `ComponentError` class. +* Use ActionView's `lookup_context` for picking templates instead of the request format. + + 3.15 added support for using templates that match the request format, that is if `/resource.csv` is requested then + ViewComponents would pick `_component.csv.erb` over `_component.html.erb`. + + With this release, the request format is no longer considered and instead ViewComponent will use the Rails logic for picking the most appropriate template type, that is the csv template will be used if it matches the `Accept` header or because the controller uses a `respond_to` block to pick the response format. + +### Breaking changes (dev/test) + +* Rename `config.generate.component_parent_class` to `config.generate.parent_class`. +* Remove `config.test_controller` in favor of `vc_test_controller_class` test helper method. +* `config.component_parent_class` is now `config.generate.component_parent_class`, moving the generator-specific option to the generator configuration namespace. +* Move previews-related configuration (`enabled`, `route`, `paths`, `default_layout`, `controller`) to under `previews` namespace. +* `config.view_component_path` is now `config.generate.path`, as components have long since been able to exist in any directory. +* `--inline` generator option now generates inline template. Use `--call` to generate `#call` method. +* Remove broken integration with `rails stats` that ignored components outside of `app/components`. +* Remove `preview_source` functionality. Consider using [Lookbook](https://lookbook.build/) instead. +* Use `Nokogiri::HTML5` instead of `Nokogiri::HTML4` for test helpers. +* Move generators to a ViewComponent namespace. + + Before, ViewComponent generators pollute the generator namespace with a bunch of top level items, and claim the generic "component" name. + + Now, generators live in a "view_component" module/namespace, so what was before `rails g + component` is now `rails g view_component:component`. + +### New features + +* Add `SystemSpecHelpers` for use with RSpec. +* Add support for including `Turbo::StreamsHelper`. +* Add template annotations for components with `def call`. +* Graduate `SlotableDefault` to be included by default. +* Add `#current_template` accessor and `Template#path` for diagnostic usage. +* Reduce string allocations during compilation. + +### Bug fixes + +* Fix bug where virtual path wasn't reset, breaking translations outside of components. +* Fix bug where `config.previews.enabled` didn't function properly in production environments. +* Fix bug where response format wasn't set, which caused issues with Turbo Frames. +* Fix bug in `SlotableDefault` where default couldn't be overridden when content was passed as a block. +* Fix bug where request-aware helpers didn't work outside of the request context. +* `ViewComponentsSystemTestController` shouldn't be useable outside of test environment + +### Non-functional changes + +* Remove unnecessary usage of `ruby2_keywords`. +* Remove unnecessary `respond_to` checks. +* Require MFA when publishing to RubyGems. +* Clean up project dependencies, relaxing versions of development gems. +* Add test case for absolute URL path helpers in mailers. +* Update documentation on performance to reflect more representative benchmark showing 2-3x speed increase over partials. +* Add documentation note about instrumentation negatively affecting performance. +* Remove unnecessary ENABLE_RELOADING test suite flag. +* `config.previews.default_layout` should default to nil. +* Add test coverage for uncovered code. +* Test against `turbo-rails` `v2` and `rspec-rails` `v7`. + +## 4.0.0.alpha7 + +* BREAKING: Remove deprecated `use_helper(s)`. Use `include MyHelper` or `helpers.` proxy instead. + + *Joel Hawksley* + +* BREAKING: Support compatibility with `Dry::Initializer`. As a result, `EmptyOrInvalidInitializerError` will no longer be raised. + + *Joel Hawksley* + +* BREAKING: Rename `config.generate.component_parent_class` to `config.generate.parent_class`. + + *Joel Hawksley* + +* Fix bug where `config.previews.enabled` didn't function properly in production environments. + + *Joel Hawksley* + +* `config.previews.default_layout` should default to nil. + + *Joel Hawksley* + +* Add test case for absolute URL path helpers in mailers. + + *Joel Hawksley* + +* Fix bug where response format wasn't set, which caused issues with Turbo Frames. + + *Joel Hawksley* + +## 4.0.0.alpha6 + +* BREAKING: Remove `config.test_controller` in favor of `vc_test_controller_class` test helper method. + + *Joel Hawksley* + +* BREAKING: `config.component_parent_class` is now `config.generate.component_parent_class`, moving the generator-specific option to the generator configuration namespace. + + *Joel Hawksley* + +* BREAKING: Move previews-related configuration (`enabled`, `route`, `paths`, `default_layout`, `controller`) to under `previews` namespace. + + *Joel Hawksley* + +* Add template annotations for components with `def call`. + + *Joel Hawksley* + +* Add support for including Turbo::StreamsHelper. + + *Stephen Nelson* + +* Update documentation on performance to reflect more representative benchmark showing 2-3x speed increase over partials. + + *Joel Hawksley* + +* Add documentation note about instrumentation negatively affecting performance. + + *Joel Hawksley* + +* Revert object shapes optimization due to lack of evidence of improvement. + + *Joel Hawksley* + +## 4.0.0.alpha5 + +* BREAKING: `config.view_component_path` is now `config.generate.path`, as components have long since been able to exist in any directory. + + *Joel Hawksley* + +* BREAKING: Remove broken integration with `rails stats` that ignored components outside of `app/components`. + + *Joel Hawksley* + +## 4.0.0.alpha4 + +* BREAKING: Remove default initializer from `ViewComponent::Base`. Previously, `ViewComponent::Base` defined a catch-all initializer that allowed components without an initializer defined to be passed arbitrary arguments. + + *Joel Hawksley* + +* Graduate `SlotableDefault` to be included by default. + + *Joel Hawksley* + +* Fix bug in `SlotableDefault` where default couldn't be overridden when content was passed as a block. + + *Bill Watts*, *Joel Hawksley* + +## 4.0.0.alpha3 + +* BREAKING: Remove dependency on `ActionView::Base`, eliminating the need for capture compatibility patch. + + *Cameron Dutro* + +## 4.0.0.alpha2 + +* Add `#current_template` accessor and `Template#path` for diagnostic usage. + + *Joel Hawksley* + +## 4.0.0.alpha1 + +Almost six years after releasing [v1.0.0](https://github.com/ViewComponent/view_component/releases/tag/v1.0.0), we're proud to ship ViewComponent 4. This release marks a shift towards a Long Term Support model for the project, having reached significant feature maturity. While contributions are always welcome, we're unlikely to accept further breaking changes or major feature additions. + +This release makes the following breaking changes: + +* BREAKING: `--inline` generator option now generates inline template. Use `--call` to generate `#call` method. + + *Joel Hawksley* + +* BREAKING: Remove `use_deprecated_instrumentation_name` configuration option. Events will always use `render.view_component` name. + + *Joel Hawksley* + +* BREAKING: Remove `preview_source` functionality. Consider using [Lookbook](https://lookbook.build/) instead. + + *Joel Hawksley* + +* BREAKING: Use `Nokogiri::HTML5` instead of `Nokogiri::HTML4` for test helpers. + + *Noah Silvera*, *Joel Hawksley* + +* BREAKING: Move generators to a ViewComponent namespace. + + Before, ViewComponent generators pollute the generator namespace with a bunch of top level items, and claim the generic "component" name. + + Now, generators live in a "view_component" module/namespace, so what was before `rails g + component` is now `rails g view_component:component`. + + *Paul Sadauskas* + +* BREAKING: Require [non-EOL](https://endoflife.date/rails) Rails (`>= 7.1.0`). + + *Joel Hawksley* + +* BREAKING: Require [non-EOL](https://www.ruby-lang.org/en/downloads/branches/) Ruby (`>= 3.2.0`). + + *Joel Hawksley* + +* BREAKING: Remove `render_component` and `render` monkey patch configured with `render_monkey_patch_enabled`. + + *Joel Hawksley* + +* BREAKING: Remove support for variant names containing `.` to be consistent with Rails. + + *Stephen Nelson* + +* BREAKING: Use ActionView's `lookup_context` for picking templates instead of the request format. + + 3.15 added support for using templates that match the request format, that is if `/resource.csv` is requested then + ViewComponents would pick `_component.csv.erb` over `_component.html.erb`. + + With this release, the request format is no longer considered and instead ViewComponent will use the Rails logic + for picking the most appropriate template type, that is the csv template will be used if it matches the `Accept` header + or because the controller uses a `respond_to` block to pick the response format. + + *Stephen Nelson* + +* BREAKING: Rename internal methods to have `__vc_` prefix if they shouldn't be used by consumers. Make internal constants private. Make `Collection#components`, `Slotable#register_polymorphic_slot` private. Remove unused `ComponentError` class. + + *Joel Hawksley* + +* Fix bug where request-aware helpers didn't work outside of the request context. + + *Joel Hawksley*, *Stephen Nelson* + +* `ViewComponentsSystemTestController` shouldn't be useable outside of test environment + + *Joel Hawksley*, *Stephen Nelson* + +* Remove unnecessary ENABLE_RELOADING test suite flag. + + *Joel Hawksley* + +* Add test coverage for uncovered code. + + *Joel Hawksley* + +* Remove unnecessary `#format` methods that returned `nil`. + + *Joel Hawksley* + +* Clean up project dependencies, relaxing versions of development gems. + + *Joel Hawksley* + +* Test against `turbo-rails` `v2`. + + *Joel Hawksley* + +* Test against `rspec-rails` `v7`. + + *Joel Hawksley* + +* Remove unnecessary usage of `ruby2_keywords`. + + *Joel Hawksley* + +* Remove unnecessary `respond_to` checks. + + *Tiago Menegaz*, *Joel Hawksley* + +* Introduce component-local config and migrate `strip_trailing_whitespace` to use it under the hood. + + *Simon Fish* + * Deprecate `use_helper(s)`. Use `include MyHelper` or `helpers.` proxy instead. *Joel Hawksley* diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 507f11f36..9866bd335 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -8,6 +8,8 @@ nav_order: 9 _ViewComponent is intended to be a safe, welcoming space for collaboration. By participating you agree to abide by the [Contributor Code of Conduct](CODE_OF_CONDUCT.md)._ +_As of version 4, ViewComponent is considered feature-complete. While contributions are always welcome, we're unlikely to accept further breaking changes or significant feature additions._ + Hi there! We're thrilled that you'd like to contribute to ViewComponent. Your help is essential for keeping it great. If you have any substantial changes that you would like to make, please [open an issue](http://github.com/viewcomponent/view_component/issues/new) first to discuss them with us. @@ -68,7 +70,7 @@ bundle exec m test/view_component/YOUR_COMPONENT_test.rb:line_number Specify one of the supported versions listed in [Appraisals](https://github.com/viewcomponent/view_component/blob/main/Appraisals): ```command -bundle exec appraisal rails-5.2 rake +bundle exec appraisal rails-8.0 rake ``` ## Documentation diff --git a/docs/api.md b/docs/api.md index 8fdf67b00..94aebc8d2 100644 --- a/docs/api.md +++ b/docs/api.md @@ -14,6 +14,15 @@ nav_order: 3 Returns the current config. +### `.identifier` → [String] + +The file path of the component Ruby file. + +### `.new(...)` + +Redefine `new` so we can pre-allocate instance variables to optimize +for Ruby object shapes. + ### `.sidecar_files(extensions)` Find sidecar files for the given extensions. @@ -37,7 +46,7 @@ end Whether trailing whitespace will be stripped before compilation. -### `.with_collection(collection, **args)` +### `.with_collection(collection, spacer_component: nil, **args)` Render a component for each element in a collection ([documentation](/guide/collections)): @@ -73,19 +82,23 @@ Whether `content` has been passed to the component. The current controller. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult. +### `#current_template` + +Returns the value of attribute current_template. + ### `#helpers` → [ActionView::Base] A proxy through which to access helpers. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult. -### `#output_preamble` → [String] - -Optional content to be returned before the rendered template. - ### `#output_postamble` → [String] Optional content to be returned after the rendered template. +### `#output_preamble` → [String] + +Optional content to be returned before the rendered template. + ### `#render?` → [Boolean] Override to determine whether the ViewComponent should render. @@ -142,19 +155,6 @@ so helpers, etc work as expected. ## Configuration -### `.capture_compatibility_patch_enabled` - -Enables the experimental capture compatibility patch that makes ViewComponent -compatible with forms, capture, and other built-ins. -previews. -Defaults to `false`. - -### `.component_parent_class` - -The parent class from which generated components will inherit. -Defaults to `nil`. If this is falsy, generators will use -`"ApplicationComponent"` if defined, `"ViewComponent::Base"` otherwise. - ### `#config` Returns the value of attribute config. @@ -166,12 +166,6 @@ class so that config options remain accessible before the rest of ViewComponent has loaded. Defaults to an instance of ViewComponent::Config with all other documented defaults set. -### `.default_preview_layout` - -A custom default layout used for the previews index page and individual -previews. -Defaults to `nil`. If this is falsy, `"component_preview"` is used. - ### `.generate` The subset of configuration options relating to generators. @@ -179,6 +173,12 @@ The subset of configuration options relating to generators. All options under this namespace default to `false` unless otherwise stated. +#### `#path` + +Where to put generated components. Defaults to `app/components`: + + config.view_component.generate.path = "lib/components" + #### `#sidecar` Always generate a component with a sidecar directory: @@ -191,6 +191,12 @@ Always generate a Stimulus controller alongside the component: config.view_component.generate.stimulus_controller = true +#### `#typescript` + +Generate TypeScript files instead of JavaScript files: + + config.view_component.generate.typescript = true + #### `#locale` Always generate translations file alongside the component: @@ -220,67 +226,58 @@ Path to generate preview: Required when there is more than one path defined in preview_paths. Defaults to `""`. If this is blank, the generator will use -`ViewComponent.config.preview_paths` if defined, +`ViewComponent.config.previews.paths` if defined, `"test/components/previews"` otherwise -### `.instrumentation_enabled` +#### `#use_component_path_for_rspec_tests` -Whether ActiveSupport notifications are enabled. -Defaults to `false`. +Whether to use `config.generate.path` when generating new +RSpec component tests: -### `.preview_controller` + config.view_component.generate.use_component_path_for_rspec_tests = true -The controller used for previewing components. -Defaults to `ViewComponentsController`. +When set to `true`, the generator will use the `path` to +decide where to generate the new RSpec component test. +For example, if the `path` is +`app/views/components`, then the generator will create a new spec file +in `spec/views/components/` rather than the default `spec/components/`. -### `.preview_paths` +### `.instrumentation_enabled` -The locations in which component previews will be looked up. -Defaults to `['test/components/previews']` relative to your Rails root. +Whether ActiveSupport notifications are enabled. +Defaults to `false`. -### `.preview_route` +### `.previews` -The entry route for component previews. -Defaults to `"/rails/view_components"`. +The subset of configuration options relating to previews. -### `.render_monkey_patch_enabled` +#### `#controller` -If this is disabled, use `#render_component` or -`#render_component_to_string` instead. -Defaults to `true`. +The controller used for previewing components. Defaults to `ViewComponentsController`: -### `.show_previews` + config.view_component.previews.controller = "MyPreviewController" -Whether component previews are enabled. -Defaults to `true` in development and test environments. +#### `#route` -### `.show_previews_source` +The entry route for component previews. Defaults to `/rails/view_components`: -Whether to display source code previews in component previews. -Defaults to `false`. + config.view_component.previews.route = "/my_previews" -### `.test_controller` +#### `#enabled` -The controller used for testing components. -Can also be configured on a per-test basis using `#with_controller_class`. -Defaults to `ApplicationController`. +Whether component previews are enabled. Defaults to `true` in development and test environments: -### `.use_deprecated_instrumentation_name` + config.view_component.previews.enabled = false -Whether ActiveSupport Notifications use the private name `"!render.view_component"` -or are made more publicly available via `"render.view_component"`. -Will be removed in next major version. -Defaults to `true`. +#### `#default_layout` -### `.view_component_path` +A custom default layout used for the previews index page and individual previews. Defaults to `nil`: -The path in which components, their templates, and their sidecars should -be stored. -Defaults to `"app/components"`. + config.view_component.previews.default_layout = "preview_layout" ## ViewComponent::TestHelpers -### `#render_in_view_context(*args, &block)` +### `#render_in_view_context(...)` Execute the given block in the view context (using `instance_exec`). Internally sets `page` to be a `Capybara::Node::Simple`, allowing for @@ -294,7 +291,7 @@ end assert_text("Hello, World!") ``` -### `#render_inline(component, **args, &block)` → [Nokogiri::HTML] +### `#render_inline(component, **args, &block)` → [Nokogiri::HTML5] Render a component inline. Internally sets `page` to be a `Capybara::Node::Simple`, allowing for Capybara assertions to be used: @@ -304,7 +301,7 @@ render_inline(MyComponent.new) assert_text("Hello, World!") ``` -### `#render_preview(name, from: __vc_test_helpers_preview_class, params: {})` → [Nokogiri::HTML] +### `#render_preview(name, from: __vc_test_helpers_preview_class, params: {})` → [Nokogiri::HTML5] Render a preview inline. Internally sets `page` to be a `Capybara::Node::Simple`, allowing for Capybara assertions to be used: @@ -325,6 +322,15 @@ In RSpec, `Preview` is appended to `described_class`. Returns the result of a render_inline call. +### `#rendered_json` + +`JSON.parse`-d component output. + +```ruby +render_inline(MyJsonComponent.new) +assert_equal(rendered_json["hello"], "world") +``` + ### `#vc_test_controller` → [ActionController::Base] Access the controller used by `render_inline`: @@ -337,13 +343,23 @@ test "logged out user sees login link" do end ``` +### `#vc_test_controller_class` + +Set the controller used by `render_inline`: + +```ruby +def vc_test_controller_class + MyTestController +end +``` + ### `#vc_test_request` → [ActionDispatch::TestRequest] Access the request used by `render_inline`: ```ruby test "component does not render in Firefox" do - vc_test_request.env["HTTP_USER_AGENT"] = "Mozilla/5.0" + request.env["HTTP_USER_AGENT"] = "Mozilla/5.0" render_inline(NoFirefoxComponent.new) refute_component_rendered end @@ -360,6 +376,16 @@ with_controller_class(UsersController) do end ``` +### `#with_format(*formats)` + +Set format of the current request + +```ruby +with_format(:json) do + render_inline(MyComponent.new) +end +``` + ### `#with_request_url(full_path, host: nil, method: nil)` Set the URL of the current request (such as when using request-dependent path helpers): @@ -386,7 +412,7 @@ with_request_url("/users/42", method: "POST") do end ``` -### `#with_variant(variant)` +### `#with_variant(*variants)` Set the Action Pack request variant for the given block: @@ -418,7 +444,7 @@ To fix this issue, either use the `content` accessor directly or choose a differ ### `ControllerCalledBeforeRenderError` -`#controller` can't be used during initialization, as it depends on the view context that only exists once a ViewComponent is passed to the Rails render pipeline. +`#controller` can't be used before rendering, as it depends on the view context that only exists once a ViewComponent is passed to the Rails render pipeline. It's sometimes possible to fix this issue by moving code dependent on `#controller` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void). @@ -434,17 +460,9 @@ It looks like a block was provided after calling `with_content` on COMPONENT, wh To fix this issue, use either `with_content` or a block. -### `EmptyOrInvalidInitializerError` - -The COMPONENT initializer is empty or invalid. It must accept the parameter `PARAMETER` to render it as a collection. - -To fix this issue, update the initializer to accept `PARAMETER`. - -See [the collections docs](https://viewcomponent.org/guide/collections.html) for more information on rendering collections. - ### `HelpersCalledBeforeRenderError` -`#helpers` can't be used during initialization as it depends on the view context that only exists once a ViewComponent is passed to the Rails render pipeline. +`#helpers` can't be used before rendering as it depends on the view context that only exists once a ViewComponent is passed to the Rails render pipeline. It's sometimes possible to fix this issue by moving code dependent on `#helpers` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void). @@ -470,13 +488,15 @@ A preview template for example EXAMPLE doesn't exist. To fix this issue, create a template for the example. -### `MultipleInlineTemplatesError` +### `MissingTemplateError` -Inline templates can only be defined once per-component. +No templates for COMPONENT match the request DETAIL. + +To fix this issue, provide a suitable template. -### `MultipleMatchingTemplatesForPreviewError` +### `MultipleInlineTemplatesError` -Found multiple templates for TEMPLATE_IDENTIFIER. +Inline templates can only be defined once per-component. ### `NilWithContentError` @@ -484,10 +504,6 @@ No content provided to `#with_content` for ViewComponent::NilWithContentError. To fix this issue, pass a value. -### `NoMatchingTemplatesForPreviewError` - -Found 0 matches for templates for TEMPLATE_IDENTIFIER. - ### `RedefinedSlotError` COMPONENT declares the SLOT_NAME slot multiple times. @@ -522,13 +538,9 @@ To fix this issue, choose a different name. ViewComponent SystemTest controller attempted to load a file outside of the expected directory. -### `SystemTestControllerOnlyAllowedInTestError` - -ViewComponent SystemTest controller must only be called in a test environment for security reasons. - ### `TranslateCalledBeforeRenderError` -`#translate` can't be used during initialization as it depends on the view context that only exists once a ViewComponent is passed to the Rails render pipeline. +`#translate` can't be used before rendering as it depends on the view context that only exists once a ViewComponent is passed to the Rails render pipeline. It's sometimes possible to fix this issue by moving code dependent on `#translate` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void). diff --git a/docs/compatibility.md b/docs/compatibility.md index 0fcdf9210..1c7231ce9 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -8,29 +8,12 @@ nav_order: 8 ## Ruby & Rails -ViewComponent supports all actively supported versions of Ruby (3.0+) and Ruby on Rails (6.1+) and is tested against a combination of these versions of Ruby on Rails. - -While EOL (end-of-life) versions of Ruby and Ruby on Rails may still work with ViewComponent, they're not actively supported and no longer tested. We will still accept patches on a case-by-case basis to support older Ruby & Rails versions based on the complexity and maintenance burden. Please open an issue before submitting such a Pull Request. +ViewComponent supports all actively supported versions of [Ruby](https://endoflife.date/ruby) (>= 3.2) and [Ruby on Rails](https://endoflife.date/rails) (>= 7.1). Changes to the minimum Ruby and Rails versions supported will only be made in major releases. ## Template languages ViewComponent is tested against ERB, Haml, and Slim, but it should support most Rails template handlers. -## Disabling the render monkey patch (Rails < 6.1) - -Since 2.13.0 -{: .label } - -To [avoid conflicts](https://github.com/viewcomponent/view_component/issues/288) between ViewComponent and other gems that also monkey patch the `render` method, it's possible to configure ViewComponent to not include the render monkey patch: - -`config.view_component.render_monkey_patch_enabled = false # defaults to true` - -With the monkey patch disabled, use `render_component` (or `render_component_to_string`) instead: - -```erb -<%= render_component Component.new(message: "bar") %> -``` - ## Bridgetown (Static Site Generator) [Bridgetown](https://www.bridgetownrb.com/) supports ViewComponent via an experimental shim provided by the [bridgetown-view-component gem](https://github.com/bridgetownrb/bridgetown-view-component). More information available [here](https://www.bridgetownrb.com/docs/components/ruby#need-compatibility-with-rails-try-viewcomponent-experimental). diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md new file mode 100644 index 000000000..8d7303895 --- /dev/null +++ b/docs/guide/configuration.md @@ -0,0 +1,19 @@ +--- +layout: default +title: Configuration +parent: How-to guide +--- + +# Configuration + +To configure ViewComponent, set options in `config/ENVIRONMENT.rb`: + +```ruby +MyApplication.configure do + config.view_component.instrumentation_enabled = true + config.view_component.generate.path = "app/custom_components" + config.view_component.previews.controller = "MyPreviewController" +end +``` + +For a list of available options, see [/api](/api#configuration). diff --git a/docs/guide/generators.md b/docs/guide/generators.md index 6a81d7095..965e16bed 100644 --- a/docs/guide/generators.md +++ b/docs/guide/generators.md @@ -11,7 +11,7 @@ The generator accepts a component name and a list of arguments. To create an `ExampleComponent` with `title` and `content` attributes: ```console -bin/rails generate component Example title content +bin/rails generate view_component:component Example title content create app/components/example_component.rb invoke test_unit @@ -25,7 +25,7 @@ bin/rails generate component Example title content To generate a namespaced `Sections::ExampleComponent`: ```console -bin/rails generate component Sections::Example title content +bin/rails generate view_component:component Sections::Example title content create app/components/sections/example_component.rb invoke test_unit @@ -38,11 +38,11 @@ bin/rails generate component Sections::Example title content You can specify options when running the generator. To alter the default values project-wide, define the configuration settings described in [API docs](/api.html#configuration). -Generated ViewComponents are added to `app/components` by default. Set `config.view_component.view_component_path` to use a different path. Note that you need to add the same path to `config.eager_load_paths` as well. +Generated ViewComponents are added to `app/components` by default. Set `config.view_component.generate.path` to use a different path. ```ruby # config/application.rb -config.view_component.view_component_path = "app/views/components" +config.view_component.generate.path = "app/views/components" config.eager_load_paths << Rails.root.join("app/views/components") ``` @@ -51,7 +51,7 @@ config.eager_load_paths << Rails.root.join("app/views/components") ViewComponent includes template generators for the `erb`, `haml`, and `slim` template engines and will default to the template engine specified in `config.generators.template_engine`. ```console -bin/rails generate component Example title --template-engine slim +bin/rails generate view_component:component Example title --template-engine slim create app/components/example_component.rb invoke test_unit @@ -65,7 +65,7 @@ bin/rails generate component Example title --template-engine slim By default, `config.generators.test_framework` is used. ```console -bin/rails generate component Example title --test-framework rspec +bin/rails generate view_component:component Example title --test-framework rspec create app/components/example_component.rb invoke rspec @@ -80,7 +80,7 @@ Since 2.25.0 {: .label } ```console -bin/rails generate component Example title --preview +bin/rails generate view_component:component Example title --preview create app/components/example_component.rb invoke test_unit @@ -97,7 +97,7 @@ Since 2.38.0 {: .label } ```console -bin/rails generate component Example title --stimulus +bin/rails generate view_component:component Example title --stimulus create app/components/example_component.rb invoke test_unit @@ -121,7 +121,7 @@ Since 2.47.0 {: .label } ```console -bin/rails generate component Example title --locale +bin/rails generate view_component:component Example title --locale create app/components/example_component.rb invoke test_unit @@ -142,7 +142,7 @@ Since 2.16.0 {: .label } ```console -bin/rails generate component Example title --sidecar +bin/rails generate view_component:component Example title --sidecar create app/components/example_component.rb invoke test_unit @@ -153,13 +153,21 @@ bin/rails generate component Example title --sidecar To always generate in the sidecar directory, set `config.view_component.generate.sidecar = true`. -### Use [inline rendering](/guide/templates.html#inline) (no template file) +### Use [inline template](/guide/templates.html#inline) (no template file) -Since 2.24.0 -{: .label } +```console +bin/rails generate view_component:component Example title --inline + + create app/components/example_component.rb + invoke test_unit + create test/components/example_component_test.rb + invoke erb +``` + +### Use [call method](/guide/templates.html#call) (no template file) ```console -bin/rails generate component Example title --inline +bin/rails generate view_component:component Example title --call create app/components/example_component.rb invoke test_unit @@ -175,7 +183,7 @@ Since 2.41.0 By default, `ApplicationComponent` is used if defined, `ViewComponent::Base` otherwise. ```console -bin/rails generate component Example title content --parent MyBaseComponent +bin/rails generate view_component:component Example title content --parent MyBaseComponent create app/components/example_component.rb invoke test_unit @@ -184,7 +192,7 @@ bin/rails generate component Example title content --parent MyBaseComponent create app/components/example_component.html.erb ``` -To always use a specific parent class, set `config.view_component.component_parent_class = "MyBaseComponent"`. +To always use a specific parent class, set `config.view_component.parent_class = "MyBaseComponent"`. ### Skip collision check diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 21adfcaa8..f9ef03f31 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -29,7 +29,7 @@ Use the component generator to create a new ViewComponent. The generator accepts a component name and a list of arguments: ```console -bin/rails generate component Example title +bin/rails generate view_component:component Example title invoke test_unit create test/components/example_component_test.rb @@ -92,7 +92,7 @@ def show end ``` -_Note: Content can't be passed to a component via a block in controllers. Instead, use `with_content`. In versions of Rails < 6.1, rendering a ViewComponent from a controller doesn't include the layout._ +_Note: Content can't be passed to a component via a block in controllers. Instead, use `with_content`._ When using turbo frames with [turbo-rails](https://github.com/hotwired/turbo-rails), set `content_type` as `text/html`: diff --git a/docs/guide/helpers.md b/docs/guide/helpers.md index 841a43555..65bdc6d7a 100644 --- a/docs/guide/helpers.md +++ b/docs/guide/helpers.md @@ -51,72 +51,6 @@ class UserComponent < ViewComponent::Base end ``` -## UseHelpers setter - -By default, ViewComponents don't have access to helper methods defined externally. The `use_helpers` method allows external helpers to be called from the component. - -`use_helpers` defines the helper on the component, similar to `delegate`: - -```ruby -class UseHelpersComponent < ViewComponent::Base - use_helpers :icon, :icon? - - erb_template <<-ERB -
- <%= icon? ? icon(:user) : icon(:guest) %> -
- ERB -end -``` - -Use the `from:` keyword to include individual methods defined in helper modules not available in the component: - -```ruby -class UserComponent < ViewComponent::Base - use_helpers :icon, :icon?, from: IconHelper - - def profile_icon - icon? ? icon(:user) : icon(:guest) - end -end -``` - -Use the `prefix:` keyword to prefix the helper method with the name of the helper module: - -```ruby -class UserComponent < ViewComponent::Base - use_helpers :icon, :icon?, from: IconHelper, prefix: true - - def profile_icon - icon_helper_icon? ? icon_helper_icon(:user) : icon_helper_icon(:guest) - end -end -``` - -or use the `prefix:` keyword with a custom prefix: - -```ruby -class UserComponent < ViewComponent::Base - use_helpers :icon, :icon?, from: IconHelper, prefix: :user - - def profile_icon - user_icon? ? user_icon(:user) : user_icon(:guest) - end -end -``` - -The singular version `use_helper` is also available: - -```ruby -class UserComponent < ViewComponent::Base - use_helper :icon, from: IconHelper - - def profile_icon - icon :user - end -end -``` - ## Nested URL helpers Rails nested URL helpers implicitly depend on the current `request` in certain cases. Since ViewComponent is built to enable reusing components in different contexts, nested URL helpers should be passed their options explicitly: @@ -128,3 +62,9 @@ edit_user_path # implicitly depends on current request to provide `user` # good edit_user_path(user: current_user) ``` + +Alternatively, use the `helpers` proxy: + +```ruby +helpers.edit_user_path +``` diff --git a/docs/guide/instrumentation.md b/docs/guide/instrumentation.md index c1230a212..9ff6fb976 100644 --- a/docs/guide/instrumentation.md +++ b/docs/guide/instrumentation.md @@ -15,11 +15,8 @@ To enable ActiveSupport notifications, use the `instrumentation_enabled` option: # config/application.rb # Enable ActiveSupport notifications for all ViewComponents config.view_component.instrumentation_enabled = true -config.view_component.use_deprecated_instrumentation_name = false ``` -Setting `use_deprecated_instrumentation_name` configures the event name. If `false` the name is `"render.view_component"`. If `true` (default) the deprecated `"!render.view_component"` will be used. - Subscribe to the event: ```ruby @@ -29,6 +26,8 @@ ActiveSupport::Notifications.subscribe("render.view_component") do |event| # or end ``` +_Note: Enabling instrumentation negatively impacts the performance of ViewComponent._ + ## Viewing instrumentation sums in the browser developer tools When using `render.view_component` with `config.server_timing = true` (default in development) in Rails 7, the browser developer tools display the sum total timing information in Network > Timing under the key `render.view_component`. diff --git a/docs/guide/previews.md b/docs/guide/previews.md index 54445ba33..8e3418ded 100644 --- a/docs/guide/previews.md +++ b/docs/guide/previews.md @@ -119,16 +119,16 @@ Preview classes live in `test/components/previews`, which can be configured usin ```ruby # config/application.rb -config.view_component.preview_paths << "#{Rails.root}/lib/component_previews" +config.view_component.previews.paths << "#{Rails.root}/lib/component_previews" ``` ## Previews route -Previews are served from `/rails/view_components` by default. To use a different endpoint, set the `preview_route` option: +Previews are served from `/rails/view_components` by default. To use a different endpoint, set the `previews.route` option: ```ruby # config/application.rb -config.view_component.preview_route = "/previews" +config.view_component.previews.route = "/previews" ``` ## Preview templates @@ -155,7 +155,7 @@ end ``` To use a different location for preview templates, pass the `template` argument: -(the path should be relative to `config.view_component.preview_paths`): +(the path should be relative to `config.view_component.previews.paths`): ```ruby # test/components/previews/cell_component_preview.rb @@ -184,11 +184,11 @@ Which enables passing in a value: `/rails/view_components/cell_component/default ## Configuring preview controller -Extend previews to add authentication, authorization, before actions, etc. using the `preview_controller` option: +Extend previews to add authentication, authorization, before actions, etc. using the `previews.controller` option: ```ruby # config/application.rb -config.view_component.preview_controller = "MyPreviewController" +config.view_component.previews.controller = "MyPreviewController" ``` Then include `PreviewActions` in the controller: @@ -201,25 +201,13 @@ end ## Enabling previews -Previews are enabled by default in test and development environments. To enable or disable previews, use the `show_previews` option: +Previews are enabled by default in test and development environments. To enable or disable previews, use the `previews.enabled` option: ```ruby # config/environments/test.rb -config.view_component.show_previews = false +config.view_component.previews.enabled = false ``` -## Source previews - -A source preview is a syntax highlighted source code example of the usage of a view component, shown below the preview. -Source previews are disabled by default. To enable or disable source previews, use the `show_previews_source` option: - -```ruby -# config/environments/test.rb -config.view_component.show_previews_source = true -``` - -To render the source preview in a different location, use the view helper `preview_source` from within the preview template or preview layout. - ## Use with RSpec When using previews with RSpec, replace `test/components` with `spec/components` and update `preview_paths`: diff --git a/docs/guide/slots.md b/docs/guide/slots.md index 9886685b0..d4225ab09 100644 --- a/docs/guide/slots.md +++ b/docs/guide/slots.md @@ -352,15 +352,13 @@ The setters are now `#with_icon_visual` and `#with_avatar_visual` instead of the ## `#default_SLOT_NAME` -Since 3.14.0 +Since 4.0.0 {: .label } -To provide a default value for a slot, include the experimental `SlotableDefault` module and define a `default_SLOT_NAME` method: +To provide a default value for a slot, define a `default_SLOT_NAME` method: ```ruby class SlotableDefaultComponent < ViewComponent::Base - include SlotableDefault - renders_one :header def default_header @@ -373,8 +371,6 @@ end ```ruby class SlotableDefaultInstanceComponent < ViewComponent::Base - include SlotableDefault - renders_one :header def default_header diff --git a/docs/guide/templates.md b/docs/guide/templates.md index 2caf95ec0..988f04352 100644 --- a/docs/guide/templates.md +++ b/docs/guide/templates.md @@ -80,7 +80,7 @@ app/components To generate a component with a sidecar directory, use the `--sidecar` flag: ```console -bin/rails generate component Example title --sidecar +bin/rails generate view_component:component Example title --sidecar invoke test_unit create test/components/example_component_test.rb create app/components/example_component.rb diff --git a/docs/guide/testing.md b/docs/guide/testing.md index cf344c123..1f59f06d1 100644 --- a/docs/guide/testing.md +++ b/docs/guide/testing.md @@ -42,6 +42,8 @@ RSpec.configure do |config| # ... config.include ViewComponent::TestHelpers, type: :component + config.include ViewComponent::SystemSpecHelpers, type: :feature + config.include ViewComponent::SystemSpecHelpers, type: :system end ``` @@ -169,10 +171,20 @@ end Since 2.27.0 {: .label } -Component tests assume the existence of an `ApplicationController` class, which can be configured globally using the `test_controller` option: +Component tests assume the existence of an `ApplicationController` class. To set the controller for a test file, define `vc_test_controller_class`: ```ruby -config.view_component.test_controller = "BaseController" +class ExampleComponentTest < ViewComponent::TestCase + def vc_test_controller_class + PublicController + end + + def test_component_in_public_controller + render_inline ExampleComponent.new + + assert_text "foo" + end +end ``` To configure the controller used for a test case, use `with_controller_class` from `ViewComponent::TestHelpers`. @@ -304,7 +316,7 @@ To use component previews: ```ruby # config/application.rb -config.view_component.preview_paths << "#{Rails.root}/spec/components/previews" +config.view_component.previews.paths << "#{Rails.root}/spec/components/previews" ``` ## Component system tests diff --git a/docs/guide/translations.md b/docs/guide/translations.md index 3124df8c8..0707addfa 100644 --- a/docs/guide/translations.md +++ b/docs/guide/translations.md @@ -42,6 +42,21 @@ Global Rails translations are available as well: <%= t("my.global.translation") %> ``` +Including translations namespaced under the component name: + +```yml +# config/locales/en.yml +en: + my_module: + example_component: + hello: "Hello world!" +``` + +```erb +<%# app/components/my_module/example_component.html.erb %> +<%= t(".hello") %> +``` + Access global translations via `helpers` or `I18n`: ```erb diff --git a/docs/history.md b/docs/history.md index 25cf7da11..f4eb161a7 100644 --- a/docs/history.md +++ b/docs/history.md @@ -214,3 +214,7 @@ The largest consequence of this change is that we'll need to deprecate the old s We propose that we make at least one release with the new API and no deprecation warning followed by another release that includes the deprecation warning. This will give teams some time to migrate before running into deprecation warnings. + +## May 2024: ViewComponent 4 + +ViewComponent 4 is released and the project is moved into Long-Term Support, recognizing the feature-completeness of the framework. diff --git a/docs/index.md b/docs/index.md index baee4965c..349761332 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,6 +8,8 @@ nav_order: 1 A framework for creating reusable, testable & encapsulated view components, built to integrate seamlessly with Ruby on Rails. +_As of version 4, ViewComponent is in Long-Term Support and considered feature-complete._ + ## What's a ViewComponent? ViewComponents are Ruby objects used to build markup. Think of them as an evolution of the presenter pattern, inspired by [React](https://reactjs.org/docs/react-component.html). @@ -83,26 +85,12 @@ ViewComponents use a standard Ruby initializer that clearly defines what's neede ### Performance -Based on several [benchmarks](https://github.com/viewcomponent/view_component/blob/main/performance/partial_benchmark.rb), ViewComponents are ~10x faster than partials in real-world use-cases. - -The primary optimization is pre-compiling all ViewComponent templates at application boot, instead of at runtime like traditional Rails views. - -For example, the `MessageComponent` template is compiled onto the Ruby object: - -```ruby -# app/components/message_component.rb -class MessageComponent < ViewComponent::Base - def initialize(name:) - @name = name - end +Based on several [benchmarks](https://github.com/viewcomponent/view_component/blob/main/performance/partial_benchmark.rb), ViewComponents are ~2.5x faster than partials: - def call - @output_buffer.safe_append = "

Hello, ".freeze - @output_buffer.append = (@name) - @output_buffer.safe_append = "!

".freeze - @output_buffer.to_s - end -end +```console +Comparison: + component: 6498.1 i/s + partial: 2676.5 i/s - 2.50x slower ``` ### Code quality @@ -193,6 +181,7 @@ ViewComponent is built by over a hundred members of the community, including: sammyhenningsson sampart seanpdoyle +sfnelson simonrand skryukov smashwilson diff --git a/docs/known_issues.md b/docs/known_issues.md index 282d36c68..f0e600979 100644 --- a/docs/known_issues.md +++ b/docs/known_issues.md @@ -8,65 +8,6 @@ nav_order: 11 _There remain several known issues with ViewComponent. We'd be thrilled to see you consider solutions to these thorny bugs!_ -## Limited i18n support - -ViewComponent currently only supports sidecar translation files. In some cases, it could be useful to support centralized translations using namespacing: - -```yml -en: - view_components: - login_form: - submit: "Log in" - nav: - user_info: - login: "Log in" - logout: "Log out" -``` - -## Lack of Jekyll support - -It would be lovely if we could support rendering ViewComponents in Jekyll, as it would enable the reuse of ViewComponents across static and dynamic (Rails-based) sites. - -## Issues resolved by the optional capture compatibility patch - -If you're experiencing issues with duplicated content or malformed HTML output (such as using `concat` in a helper), the capture compatibility patch may resolve these. - -[Set `config.view_component.capture_compatibility_patch_enabled` to `true`](https://viewcomponent.org/api.html#capture_compatibility_patch_enabled) to resolve these issues. - -These issues arise because the related features/methods keep a reference to the -primary `ActionView::Base` instance, which has its own `@output_buffer`. When -`#capture` is called on the original `ActionView::Base` instance while -evaluating a block from a ViewComponent, the `@output_buffer` is overridden in -the `ActionView::Base` instance, and *not* the component. This results in a -double render due to `#capture` implementation details. - -To resolve the issue, we override `#capture` so that we can delegate the -`capture` logic to the ViewComponent that created the block. - -### turbo_frame_tag double rendering or scrambled HTML structure - -When using `turbo_frame_tag` inside a ViewComponent, the template may be rendered twice. See [https://github.com/github/view_component/issues/1099](https://github.com/github/view_component/issues/1099). - -As a workaround, use `tag.turbo_frame` instead of `turbo_frame_tag`. - -Note: For the same functionality as `turbo_frame_tag(my_model)`, use `tag.turbo_frame(id: dom_id(my_model))`. - -### Compatibility with Rails form helpers - -ViewComponent [isn't compatible](https://github.com/viewcomponent/view_component/issues/241) with `form_for` helpers by default. - -Passing a form object (often `f`) to a ViewComponent works for simple cases like `f.text_field :name`. Content may be ill-ordered or duplicated in complex cases, such as passing blocks to form helpers or when nesting components. - -Some workarounds include: - -- Experimental: Enable the capture compatibility patch with `config.view_component.capture_compatibility_patch_enabled = true`. -- Render an entire form within a single ViewComponent. -- Render a [partial](https://guides.rubyonrails.org/layouts_and_rendering.html#using-partials) within the ViewComponent which includes the form. -- Use a [custom `FormBuilder`](https://guides.rubyonrails.org/form_helpers.html#customizing-form-builders) to create reusable form components: - - Using FormBuilder with [Action View helpers](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html). - - Using a FormBuilder overriding all field helpers to render a ViewComponent so each field can be customized individually (for example, [view_component-form](https://github.com/pantographe/view_component-form)). - - Using a lightweight re-implementation of ViewComponent. For example, [Primer ViewComponents](https://github.com/primer/view_components) implemented [`ActsAsComponent`](https://github.com/primer/view_components/blob/main/lib/primer/forms/acts_as_component.rb) which is used in the context of `FormBuilder`. - ## Forms don't use the default `FormBuilder` Calls to form helpers such as `form_with` in ViewComponents [don't use the default form builder](https://github.com/viewcomponent/view_component/pull/1090#issue-753331927). This is by design, as it allows global state to change the rendered output of a component. Instead, consider passing a form builder into form helpers via the `builder` argument: @@ -77,6 +18,6 @@ Calls to form helpers such as `form_with` in ViewComponents [don't use the defau <% end %> ``` -## Inconsistent controller rendering behavior between Rails versions +## Incompatibility with `active_scaffold` -In versions of Rails < 6.1, rendering a ViewComponent from a controller doesn't include the layout. +Due to `active_scaffold`'s [monkey-patching](https://github.com/activescaffold/active_scaffold/blob/0ada8b7a51bf608c4ade18983ce4494c963963f3/lib/active_scaffold/extensions/action_view_rendering.rb) of `render` that hasn't been updated to support renderable objects like ViewComponents, it's impossible to use `active_scaffold` alongside `view_component`. diff --git a/gemfiles/rails_6.1.gemfile b/gemfiles/rails_6.1.gemfile deleted file mode 100644 index 82ab252d0..000000000 --- a/gemfiles/rails_6.1.gemfile +++ /dev/null @@ -1,14 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "~> 6.1" -gem "tailwindcss-rails", "~> 2.0" -gem "net-smtp", require: false -gem "net-imap", require: false -gem "net-pop", require: false -gem "turbo-rails", "~> 1" -gem "sprockets-rails", "~> 3.4.2" -gem "concurrent-ruby", "1.3.4" - -gemspec path: "../" diff --git a/gemfiles/rails_7.0.gemfile b/gemfiles/rails_7.0.gemfile deleted file mode 100644 index da308880c..000000000 --- a/gemfiles/rails_7.0.gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "~> 7.0" -gem "tailwindcss-rails", "~> 2.0" -gem "turbo-rails", "~> 1" -gem "sprockets-rails", "~> 3.4.2" - -gemspec path: "../" diff --git a/gemfiles/rails_7.1.gemfile b/gemfiles/rails_7.1.gemfile index a6a5d6021..cb1fff88e 100644 --- a/gemfiles/rails_7.1.gemfile +++ b/gemfiles/rails_7.1.gemfile @@ -3,8 +3,40 @@ source "https://rubygems.org" gem "rails", "~> 7.1" -gem "tailwindcss-rails", "~> 2.0" +gem "tailwindcss-rails", "~> 2" gem "turbo-rails", "~> 1" -gem "sprockets-rails", "~> 3.4.2" + +group :development, :test do + gem "allocation_stats" + gem "appraisal", "~> 2" + gem "benchmark-ips", "~> 2" + gem "better_html" + gem "bundler", "~> 2" + gem "capybara", "~> 3" + gem "cuprite" + gem "dry-initializer", require: true + gem "erb_lint" + gem "haml", "~> 6" + gem "jbuilder", "~> 2" + gem "m", "~> 1" + gem "method_source", "~> 1" + gem "minitest", "~> 5" + gem "propshaft", "~> 1" + gem "puma", "~> 6" + gem "rake", "~> 13" + gem "rails-dom-testing", "~> 2.3.0" + gem "redis" + gem "rspec-rails", "~> 7" + gem "rubocop-md", "~> 2" + gem "selenium-webdriver", "~> 4" + gem "simplecov-console", "< 1" + gem "simplecov", "< 1" + gem "slim", "~> 5" + gem "sprockets-rails", "~> 3" + gem "standard", "~> 1" + gem "warning" + gem "yard-activesupport-concern", "< 1" + gem "yard", "< 1" +end gemspec path: "../" diff --git a/gemfiles/rails_7.2.gemfile b/gemfiles/rails_7.2.gemfile index 840b6a532..83b36ba60 100644 --- a/gemfiles/rails_7.2.gemfile +++ b/gemfiles/rails_7.2.gemfile @@ -2,27 +2,41 @@ source "https://rubygems.org" -gem "capybara", "~> 3" gem "rails", "~> 7.2" -gem "rspec-rails", "~> 5" -gem "net-imap", require: false -gem "net-pop", require: false -gem "net-smtp", require: false -gem "debug" +gem "tailwindcss-rails", "~> 2" +gem "turbo-rails", "~> 2" -gem "tailwindcss-rails", "~> 2.0" - -gem "sprockets-rails", "~> 3.4.2" - -group :test do - gem "cuprite", "~> 0.15" +group :development, :test do + gem "allocation_stats" + gem "appraisal", "~> 2" + gem "benchmark-ips", "~> 2" + gem "better_html" + gem "bundler", "~> 2" + gem "capybara", "~> 3" + gem "cuprite" + gem "dry-initializer", require: true + gem "erb_lint" + gem "haml", "~> 6" + gem "jbuilder", "~> 2" + gem "m", "~> 1" + gem "method_source", "~> 1" + gem "minitest", "~> 5" + gem "propshaft", "~> 1" gem "puma", "~> 6" + gem "rake", "~> 13" + gem "rails-dom-testing", "~> 2.3.0" + gem "redis" + gem "rspec-rails", "~> 7" + gem "rubocop-md", "~> 2" + gem "selenium-webdriver", "~> 4" + gem "simplecov-console", "< 1" + gem "simplecov", "< 1" + gem "slim", "~> 5" + gem "sprockets-rails", "~> 3" + gem "standard", "~> 1" gem "warning" - gem "selenium-webdriver", "4.9.0" -end - -group :development, :test do - gem "appraisal", "~> 2.5" + gem "yard-activesupport-concern", "< 1" + gem "yard", "< 1" end gemspec path: "../" diff --git a/gemfiles/rails_8.0.gemfile b/gemfiles/rails_8.0.gemfile index 6280d60ba..ad46855a6 100644 --- a/gemfiles/rails_8.0.gemfile +++ b/gemfiles/rails_8.0.gemfile @@ -3,8 +3,40 @@ source "https://rubygems.org" gem "rails", "~> 8.0" -gem "tailwindcss-rails", "~> 2.0" -gem "turbo-rails", "~> 1" -gem "propshaft", "~> 1.1.0" +gem "tailwindcss-rails", "~> 2" +gem "turbo-rails", "~> 2" + +group :development, :test do + gem "allocation_stats" + gem "appraisal", "~> 2" + gem "benchmark-ips", "~> 2" + gem "better_html" + gem "bundler", "~> 2" + gem "capybara", "~> 3" + gem "cuprite" + gem "dry-initializer", require: true + gem "erb_lint" + gem "haml", "~> 6" + gem "jbuilder", "~> 2" + gem "m", "~> 1" + gem "method_source", "~> 1" + gem "minitest", "~> 5" + gem "propshaft", "~> 1" + gem "puma", "~> 6" + gem "rake", "~> 13" + gem "rails-dom-testing", "~> 2.3.0" + gem "redis" + gem "rspec-rails", "~> 7" + gem "rubocop-md", "~> 2" + gem "selenium-webdriver", "~> 4" + gem "simplecov-console", "< 1" + gem "simplecov", "< 1" + gem "slim", "~> 5" + gem "sprockets-rails", "~> 3" + gem "standard", "~> 1" + gem "warning" + gem "yard-activesupport-concern", "< 1" + gem "yard", "< 1" +end gemspec path: "../" diff --git a/gemfiles/rails_main.gemfile b/gemfiles/rails_main.gemfile index 5c92f2cf5..598dd76bb 100644 --- a/gemfiles/rails_main.gemfile +++ b/gemfiles/rails_main.gemfile @@ -3,8 +3,41 @@ source "https://rubygems.org" gem "rails", github: "rails/rails", branch: "main" -gem "tailwindcss-rails", "~> 2.0" -gem "turbo-rails", "~> 1" -gem "propshaft", "~> 1.1.0" +gem "rack", git: "https://github.com/rack/rack", ref: "8a4475a9f416a72e5b02bd7817e4a8ed684f29b0" +gem "tailwindcss-rails", "~> 2" +gem "turbo-rails", "~> 2" + +group :development, :test do + gem "allocation_stats" + gem "appraisal", "~> 2" + gem "benchmark-ips", "~> 2" + gem "better_html" + gem "bundler", "~> 2" + gem "capybara", "~> 3" + gem "cuprite" + gem "dry-initializer", require: true + gem "erb_lint" + gem "haml", "~> 6" + gem "jbuilder", "~> 2" + gem "m", "~> 1" + gem "method_source", "~> 1" + gem "minitest", "~> 5" + gem "propshaft", "~> 1" + gem "puma", "~> 6" + gem "rake", "~> 13" + gem "rails-dom-testing", "~> 2.3.0" + gem "redis" + gem "rspec-rails", "~> 7" + gem "rubocop-md", "~> 2" + gem "selenium-webdriver", "~> 4" + gem "simplecov-console", "< 1" + gem "simplecov", "< 1" + gem "slim", "~> 5" + gem "sprockets-rails", "~> 3" + gem "standard", "~> 1" + gem "warning" + gem "yard-activesupport-concern", "< 1" + gem "yard", "< 1" +end gemspec path: "../" diff --git a/lib/docs/docs_builder_component.rb b/lib/docs/docs_builder_component.rb index d597fc0c4..a3a839049 100644 --- a/lib/docs/docs_builder_component.rb +++ b/lib/docs/docs_builder_component.rb @@ -62,9 +62,12 @@ def docstring @method.docstring end + # Not covered as we have no deprecated methods + # :nocov: def deprecation_text @method.tag(:deprecated)&.text end + # :nocov: def docstring_and_deprecation_text <<~DOCS.strip.html_safe diff --git a/lib/rails/generators/abstract_generator.rb b/lib/generators/view_component/abstract_generator.rb similarity index 88% rename from lib/rails/generators/abstract_generator.rb rename to lib/generators/view_component/abstract_generator.rb index 8159f4e64..505ecda07 100644 --- a/lib/rails/generators/abstract_generator.rb +++ b/lib/generators/view_component/abstract_generator.rb @@ -3,7 +3,7 @@ module ViewComponent module AbstractGenerator def copy_view_file - template "component.html.#{engine_name}", destination unless options["inline"] + template("component.html.#{engine_name}", destination) unless options["inline"] || options["call"] end private @@ -29,7 +29,7 @@ def file_name end def component_path - ViewComponent::Base.config.view_component_path + ViewComponent::Base.config.generate.path end def stimulus_controller diff --git a/lib/rails/generators/component/USAGE b/lib/generators/view_component/component/USAGE similarity index 88% rename from lib/rails/generators/component/USAGE rename to lib/generators/view_component/component/USAGE index 88ac4d697..bc475db11 100644 --- a/lib/rails/generators/component/USAGE +++ b/lib/generators/view_component/component/USAGE @@ -5,7 +5,7 @@ Description: Example: ======== - bin/rails generate component Profile name age + bin/rails generate view_component Profile name age creates a Profile component and test: Component: app/components/profile_component.rb diff --git a/lib/rails/generators/component/component_generator.rb b/lib/generators/view_component/component/component_generator.rb similarity index 82% rename from lib/rails/generators/component/component_generator.rb rename to lib/generators/view_component/component/component_generator.rb index d5e4b8b3c..0cce03808 100644 --- a/lib/rails/generators/component/component_generator.rb +++ b/lib/generators/view_component/component/component_generator.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require "rails/generators/abstract_generator" +require "generators/view_component/abstract_generator" -module Rails +module ViewComponent module Generators class ComponentGenerator < Rails::Generators::NamedBase include ViewComponent::AbstractGenerator @@ -12,6 +12,7 @@ class ComponentGenerator < Rails::Generators::NamedBase argument :attributes, type: :array, default: [], banner: "attribute" check_class_collision suffix: "Component" + class_option :call, type: :boolean, default: false class_option :inline, type: :boolean, default: false class_option :locale, type: :boolean, default: ViewComponent::Base.config.generate.locale class_option :parent, type: :string, desc: "The parent class for the generated component" @@ -42,7 +43,11 @@ def create_component_file def parent_class return options[:parent] if options[:parent] - ViewComponent::Base.config.component_parent_class || default_parent_class + ViewComponent::Base.config.generate.parent_class || default_parent_class + end + + def initialize_signature? + initialize_signature.present? end def initialize_signature @@ -56,9 +61,17 @@ def initialize_body end def initialize_call_method_for_inline? + options["call"] + end + + def inline_template? options["inline"] end + def template_engine + options["template_engine"] + end + def default_parent_class defined?(ApplicationComponent) ? ApplicationComponent : ViewComponent::Base end diff --git a/lib/rails/generators/component/templates/component.rb.tt b/lib/generators/view_component/component/templates/component.rb.tt similarity index 69% rename from lib/rails/generators/component/templates/component.rb.tt rename to lib/generators/view_component/component/templates/component.rb.tt index 0d70706cf..ce513c596 100644 --- a/lib/rails/generators/component/templates/component.rb.tt +++ b/lib/generators/view_component/component/templates/component.rb.tt @@ -2,7 +2,12 @@ <% module_namespacing do -%> class <%= class_name %><%= options[:skip_suffix] ? "" : "Component" %> < <%= parent_class %> -<%- if initialize_signature -%> +<%- if inline_template? -%> + <%= template_engine %>_template <<~<%= template_engine.upcase %> +

Hello, World!

+ <%= template_engine.upcase %> +<%- end -%> +<%- if initialize_signature? -%> def initialize(<%= initialize_signature %>) <%= initialize_body %> end diff --git a/lib/rails/generators/erb/component_generator.rb b/lib/generators/view_component/erb/erb_generator.rb similarity index 76% rename from lib/rails/generators/erb/component_generator.rb rename to lib/generators/view_component/erb/erb_generator.rb index e2e2bf55b..4ee2ee437 100644 --- a/lib/rails/generators/erb/component_generator.rb +++ b/lib/generators/view_component/erb/erb_generator.rb @@ -1,16 +1,17 @@ # frozen_string_literal: true require "rails/generators/erb" -require "rails/generators/abstract_generator" +require "generators/view_component/abstract_generator" -module Erb +module ViewComponent module Generators - class ComponentGenerator < Base + class ErbGenerator < Rails::Generators::NamedBase include ViewComponent::AbstractGenerator source_root File.expand_path("templates", __dir__) class_option :sidecar, type: :boolean, default: false class_option :inline, type: :boolean, default: false + class_option :call, type: :boolean, default: false class_option :stimulus, type: :boolean, default: false def engine_name diff --git a/lib/rails/generators/erb/templates/component.html.erb.tt b/lib/generators/view_component/erb/templates/component.html.erb.tt similarity index 100% rename from lib/rails/generators/erb/templates/component.html.erb.tt rename to lib/generators/view_component/erb/templates/component.html.erb.tt diff --git a/lib/rails/generators/haml/component_generator.rb b/lib/generators/view_component/haml/haml_generator.rb similarity index 70% rename from lib/rails/generators/haml/component_generator.rb rename to lib/generators/view_component/haml/haml_generator.rb index c25d08dc1..0489b140d 100644 --- a/lib/rails/generators/haml/component_generator.rb +++ b/lib/generators/view_component/haml/haml_generator.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require "rails/generators/erb/component_generator" +require "generators/view_component/erb/erb_generator" -module Haml +module ViewComponent module Generators - class ComponentGenerator < Erb::Generators::ComponentGenerator + class HamlGenerator < ViewComponent::Generators::ErbGenerator include ViewComponent::AbstractGenerator source_root File.expand_path("templates", __dir__) diff --git a/lib/rails/generators/haml/templates/component.html.haml.tt b/lib/generators/view_component/haml/templates/component.html.haml.tt similarity index 100% rename from lib/rails/generators/haml/templates/component.html.haml.tt rename to lib/generators/view_component/haml/templates/component.html.haml.tt diff --git a/lib/rails/generators/locale/component_generator.rb b/lib/generators/view_component/locale/locale_generator.rb similarity index 90% rename from lib/rails/generators/locale/component_generator.rb rename to lib/generators/view_component/locale/locale_generator.rb index 55f735353..2bb61a708 100644 --- a/lib/rails/generators/locale/component_generator.rb +++ b/lib/generators/view_component/locale/locale_generator.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require "rails/generators/abstract_generator" +require "generators/view_component/abstract_generator" -module Locale +module ViewComponent module Generators - class ComponentGenerator < ::Rails::Generators::NamedBase + class LocaleGenerator < ::Rails::Generators::NamedBase include ViewComponent::AbstractGenerator source_root File.expand_path("templates", __dir__) diff --git a/lib/rails/generators/preview/component_generator.rb b/lib/generators/view_component/preview/preview_generator.rb similarity index 88% rename from lib/rails/generators/preview/component_generator.rb rename to lib/generators/view_component/preview/preview_generator.rb index abb6fbd07..0997f4c20 100644 --- a/lib/rails/generators/preview/component_generator.rb +++ b/lib/generators/view_component/preview/preview_generator.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -module Preview +module ViewComponent module Generators - class ComponentGenerator < ::Rails::Generators::NamedBase + class PreviewGenerator < ::Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__) class_option :preview_path, type: :string, desc: "Path for previews, required when multiple preview paths are configured", default: ViewComponent::Base.config.generate.preview_path @@ -10,7 +10,7 @@ class ComponentGenerator < ::Rails::Generators::NamedBase check_class_collision suffix: "ComponentPreview" def create_preview_file - preview_paths = ViewComponent::Base.config.preview_paths + preview_paths = ViewComponent::Base.config.previews.paths optional_path = options[:preview_path] return if preview_paths.count > 1 && optional_path.blank? diff --git a/lib/rails/generators/preview/templates/component_preview.rb.tt b/lib/generators/view_component/preview/templates/component_preview.rb.tt similarity index 100% rename from lib/rails/generators/preview/templates/component_preview.rb.tt rename to lib/generators/view_component/preview/templates/component_preview.rb.tt diff --git a/lib/rails/generators/rspec/component_generator.rb b/lib/generators/view_component/rspec/rspec_generator.rb similarity index 85% rename from lib/rails/generators/rspec/component_generator.rb rename to lib/generators/view_component/rspec/rspec_generator.rb index 605ada5fb..eff496455 100644 --- a/lib/rails/generators/rspec/component_generator.rb +++ b/lib/generators/view_component/rspec/rspec_generator.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require "rails/generators/abstract_generator" +require "generators/view_component/abstract_generator" -module Rspec +module ViewComponent module Generators - class ComponentGenerator < ::Rails::Generators::NamedBase + class RspecGenerator < ::Rails::Generators::NamedBase include ViewComponent::AbstractGenerator source_root File.expand_path("templates", __dir__) diff --git a/lib/rails/generators/rspec/templates/component_spec.rb.tt b/lib/generators/view_component/rspec/templates/component_spec.rb.tt similarity index 100% rename from lib/rails/generators/rspec/templates/component_spec.rb.tt rename to lib/generators/view_component/rspec/templates/component_spec.rb.tt diff --git a/lib/rails/generators/slim/component_generator.rb b/lib/generators/view_component/slim/slim_generator.rb similarity index 70% rename from lib/rails/generators/slim/component_generator.rb rename to lib/generators/view_component/slim/slim_generator.rb index a8e0576bc..29a0bcdbe 100644 --- a/lib/rails/generators/slim/component_generator.rb +++ b/lib/generators/view_component/slim/slim_generator.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require "rails/generators/erb/component_generator" +require "generators/view_component/erb/erb_generator" -module Slim +module ViewComponent module Generators - class ComponentGenerator < Erb::Generators::ComponentGenerator + class SlimGenerator < ViewComponent::Generators::ErbGenerator include ViewComponent::AbstractGenerator source_root File.expand_path("templates", __dir__) diff --git a/lib/rails/generators/slim/templates/component.html.slim.tt b/lib/generators/view_component/slim/templates/component.html.slim.tt similarity index 100% rename from lib/rails/generators/slim/templates/component.html.slim.tt rename to lib/generators/view_component/slim/templates/component.html.slim.tt diff --git a/lib/rails/generators/stimulus/component_generator.rb b/lib/generators/view_component/stimulus/stimulus_generator.rb similarity index 89% rename from lib/rails/generators/stimulus/component_generator.rb rename to lib/generators/view_component/stimulus/stimulus_generator.rb index f81912ddf..85395a011 100644 --- a/lib/rails/generators/stimulus/component_generator.rb +++ b/lib/generators/view_component/stimulus/stimulus_generator.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require "rails/generators/abstract_generator" +require "generators/view_component/abstract_generator" -module Stimulus +module ViewComponent module Generators - class ComponentGenerator < ::Rails::Generators::NamedBase + class StimulusGenerator < ::Rails::Generators::NamedBase include ViewComponent::AbstractGenerator source_root File.expand_path("templates", __dir__) diff --git a/lib/rails/generators/stimulus/templates/component_controller.js.tt b/lib/generators/view_component/stimulus/templates/component_controller.js.tt similarity index 100% rename from lib/rails/generators/stimulus/templates/component_controller.js.tt rename to lib/generators/view_component/stimulus/templates/component_controller.js.tt diff --git a/lib/rails/generators/stimulus/templates/component_controller.ts.tt b/lib/generators/view_component/stimulus/templates/component_controller.ts.tt similarity index 100% rename from lib/rails/generators/stimulus/templates/component_controller.ts.tt rename to lib/generators/view_component/stimulus/templates/component_controller.ts.tt diff --git a/lib/generators/view_component/tailwindcss/tailwindcss_generator.rb b/lib/generators/view_component/tailwindcss/tailwindcss_generator.rb new file mode 100644 index 000000000..102df5acd --- /dev/null +++ b/lib/generators/view_component/tailwindcss/tailwindcss_generator.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require "generators/view_component/erb/erb_generator" + +module ViewComponent + module Generators + class TailwindcssGenerator < ViewComponent::Generators::ErbGenerator + source_root File.expand_path("templates", __dir__) + end + end +end diff --git a/lib/rails/generators/tailwindcss/templates/component.html.erb.tt b/lib/generators/view_component/tailwindcss/templates/component.html.erb.tt similarity index 100% rename from lib/rails/generators/tailwindcss/templates/component.html.erb.tt rename to lib/generators/view_component/tailwindcss/templates/component.html.erb.tt diff --git a/lib/rails/generators/test_unit/templates/component_test.rb.tt b/lib/generators/view_component/test_unit/templates/component_test.rb.tt similarity index 100% rename from lib/rails/generators/test_unit/templates/component_test.rb.tt rename to lib/generators/view_component/test_unit/templates/component_test.rb.tt diff --git a/lib/rails/generators/test_unit/component_generator.rb b/lib/generators/view_component/test_unit/test_unit_generator.rb similarity index 83% rename from lib/rails/generators/test_unit/component_generator.rb rename to lib/generators/view_component/test_unit/test_unit_generator.rb index 2eb2cfe28..1bae1aab5 100644 --- a/lib/rails/generators/test_unit/component_generator.rb +++ b/lib/generators/view_component/test_unit/test_unit_generator.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -module TestUnit +module ViewComponent module Generators - class ComponentGenerator < ::Rails::Generators::NamedBase + class TestUnitGenerator < ::Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__) check_class_collision suffix: "ComponentTest" diff --git a/lib/rails/generators/tailwindcss/component_generator.rb b/lib/rails/generators/tailwindcss/component_generator.rb deleted file mode 100644 index fe0046248..000000000 --- a/lib/rails/generators/tailwindcss/component_generator.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require "rails/generators/erb/component_generator" - -module Tailwindcss - module Generators - class ComponentGenerator < Erb::Generators::ComponentGenerator - source_root File.expand_path("templates", __dir__) - end - end -end diff --git a/lib/view_component.rb b/lib/view_component.rb index 62c9cd2cb..3db9728ba 100644 --- a/lib/view_component.rb +++ b/lib/view_component.rb @@ -7,20 +7,22 @@ module ViewComponent extend ActiveSupport::Autoload autoload :Base - autoload :CaptureCompatibility autoload :Compiler autoload :CompileCache - autoload :ComponentError autoload :Config autoload :Deprecation autoload :InlineTemplate autoload :Instrumentation autoload :Preview - autoload :TestHelpers - autoload :SystemTestHelpers - autoload :TestCase - autoload :SystemTestCase autoload :Translatable + + if Rails.env.test? + autoload :TestHelpers + autoload :SystemSpecHelpers + autoload :SystemTestHelpers + autoload :TestCase + autoload :SystemTestCase + end end require "view_component/engine" if defined?(Rails::Engine) diff --git a/lib/view_component/base.rb b/lib/view_component/base.rb index a59310497..2c00e0041 100644 --- a/lib/view_component/base.rb +++ b/lib/view_component/base.rb @@ -9,15 +9,27 @@ require "view_component/errors" require "view_component/inline_template" require "view_component/preview" +require "view_component/request_details" require "view_component/slotable" -require "view_component/slotable_default" require "view_component/template" require "view_component/translatable" require "view_component/with_content_helper" -require "view_component/use_helpers" + +module ActionView + class OutputBuffer + def with_buffer(buf = nil) + new_buffer = buf || +"" + old_buffer, @raw_buffer = @raw_buffer, new_buffer + yield + new_buffer + ensure + @raw_buffer = old_buffer + end + end +end module ViewComponent - class Base < ActionView::Base + class Base class << self delegate(*ViewComponent::Config.defaults.keys, to: :config) @@ -34,26 +46,35 @@ def config end end + include ActionView::Helpers + include Rails.application.routes.url_helpers if defined?(Rails) && Rails.application + include ERB::Escape + include ActiveSupport::CoreExt::ERBUtil + include ViewComponent::InlineTemplate - include ViewComponent::UseHelpers include ViewComponent::Slotable include ViewComponent::Translatable include ViewComponent::WithContentHelper - RESERVED_PARAMETER = :content - VC_INTERNAL_DEFAULT_FORMAT = :html - # For CSRF authenticity tokens in forms delegate :form_authenticity_token, :protect_against_forgery?, :config, to: :helpers + # HTML construction methods + delegate :output_buffer, :lookup_context, :view_renderer, :view_flow, to: :helpers + + # For Turbo::StreamsHelper + delegate :formats, :formats=, to: :helpers + # For Content Security Policy nonces delegate :content_security_policy_nonce, to: :helpers # Config option that strips trailing whitespace in templates before compiling them. - class_attribute :__vc_strip_trailing_whitespace, instance_accessor: false, instance_predicate: false - self.__vc_strip_trailing_whitespace = false # class_attribute:default doesn't work until Rails 5.2 + class_attribute :__vc_strip_trailing_whitespace, instance_accessor: false, instance_predicate: false, default: false + + class_attribute :__vc_response_format, instance_accessor: false, instance_predicate: false, default: nil attr_accessor :__vc_original_view_context + attr_reader :current_template # Components render in their own view context. Helpers and other functionality # require a reference to the original Rails view context, an instance of @@ -65,7 +86,14 @@ def config # @param view_context [ActionView::Base] The original view context. # @return [void] def set_original_view_context(view_context) - self.__vc_original_view_context = view_context + # noop + end + + using RequestDetails + + # Including `Rails.application.routes.url_helpers` defines an initializer that accepts (...), + # so we have to define our own empty initializer to overwrite it. + def initialize end # Entrypoint for rendering components. @@ -77,31 +105,28 @@ def set_original_view_context(view_context) # # @return [String] def render_in(view_context, &block) - self.class.compile(raise_errors: true) + self.class.__vc_compile(raise_errors: true) @view_context = view_context + old_virtual_path = view_context.instance_variable_get(:@virtual_path) self.__vc_original_view_context ||= view_context - @output_buffer = ActionView::OutputBuffer.new + @output_buffer = view_context.output_buffer @lookup_context ||= view_context.lookup_context - # required for path helpers in older Rails versions - @view_renderer ||= view_context.view_renderer - # For content_for @view_flow ||= view_context.view_flow # For i18n @virtual_path ||= virtual_path - # For template variants (+phone, +desktop, etc.) - @__vc_variant ||= @lookup_context.variants.first + # Describes the inferred request constraints (locales, formats, variants) + @__vc_requested_details ||= @lookup_context.vc_requested_details # For caching, such as #cache_if @current_template = nil unless defined?(@current_template) old_current_template = @current_template - @current_template = self if block && defined?(@__vc_content_set_by_with_content) raise DuplicateContentError.new(self.class.name) @@ -113,18 +138,32 @@ def render_in(view_context, &block) before_render if render? - rendered_template = render_template_for(@__vc_variant, __vc_request&.format&.to_sym).to_s + value = nil + + @output_buffer.with_buffer do + @view_context.instance_variable_set(:@virtual_path, virtual_path) + + rendered_template = render_template_for(@__vc_requested_details).to_s - # Avoid allocating new string when output_preamble and output_postamble are blank - if output_preamble.blank? && output_postamble.blank? - rendered_template - else - safe_output_preamble + rendered_template + safe_output_postamble + # Avoid allocating new string when output_preamble and output_postamble are blank + value = if output_preamble.blank? && output_postamble.blank? + rendered_template + else + __vc_safe_output_preamble + rendered_template + __vc_safe_output_postamble + end end + + if ActionView::Base.annotate_rendered_view_with_filenames && current_template.inline_call? && request&.format == :html + identifier = defined?(Rails.root) ? self.class.identifier.sub("#{Rails.root}/", "") : self.class.identifier + value = "".html_safe + value + "".html_safe + end + + value else "" end ensure + view_context.instance_variable_set(:@virtual_path, old_virtual_path) @current_template = old_current_template end @@ -161,7 +200,7 @@ def render_parent_to_string target_render = self.class.instance_variable_get(:@__vc_ancestor_calls)[@__vc_parent_render_level] @__vc_parent_render_level += 1 - target_render.bind_call(self, @__vc_variant) + target_render.bind_call(self, @__vc_requested_details) ensure @__vc_parent_render_level -= 1 end @@ -196,23 +235,17 @@ def render? true end - # Override the ActionView::Base initializer so that components - # do not need to define their own initializers. - # @private - def initialize(*) - end - # Re-use original view_context if we're not rendering a component. # # This prevents an exception when rendering a partial inside of a component that has also been rendered outside # of the component. This is due to the partials compiled template method existing in the parent `view_context`, - # and not the component's `view_context`. + # and not the component's `view_context`. # # @private def render(options = {}, args = {}, &block) if options.respond_to?(:set_original_view_context) options.set_original_view_context(self.__vc_original_view_context) - super + @view_context.render(options, args, &block) else __vc_original_view_context.render(options, args, &block) end @@ -274,13 +307,6 @@ def view_cache_dependencies [] end - # For caching, such as #cache_if - # - # @private - def format - @__vc_variant if defined?(@__vc_variant) - end - # The current request. Use sparingly as doing so introduces coupling that # inhibits encapsulation & reuse, often making testing difficult. # @@ -289,10 +315,9 @@ def request __vc_request end - # Enables consumers to override request/@request - # # @private def __vc_request + # The current request (if present, as mailers/jobs/etc do not have a request) @__vc_request ||= controller.request if controller.respond_to?(:request) end @@ -318,6 +343,10 @@ def content? __vc_render_in_block_provided? || __vc_content_set_by_with_content_defined? end + def format + self.class.__vc_response_format + end + private attr_reader :view_context @@ -330,12 +359,8 @@ def __vc_content_set_by_with_content_defined? defined?(@__vc_content_set_by_with_content) end - def content_evaluated? - defined?(@__vc_content_evaluated) && @__vc_content_evaluated - end - - def maybe_escape_html(text) - return text if __vc_request && !__vc_request.format.html? + def __vc_maybe_escape_html(text) + return text if @current_template && !@current_template.html? return text if text.blank? if text.html_safe? @@ -346,54 +371,18 @@ def maybe_escape_html(text) end end - def safe_output_preamble - maybe_escape_html(output_preamble) do + def __vc_safe_output_preamble + __vc_maybe_escape_html(output_preamble) do Kernel.warn("WARNING: The #{self.class} component was provided an HTML-unsafe preamble. The preamble will be automatically escaped, but you may want to investigate.") end end - def safe_output_postamble - maybe_escape_html(output_postamble) do + def __vc_safe_output_postamble + __vc_maybe_escape_html(output_postamble) do Kernel.warn("WARNING: The #{self.class} component was provided an HTML-unsafe postamble. The postamble will be automatically escaped, but you may want to investigate.") end end - # Set the controller used for testing components: - # - # ```ruby - # config.view_component.test_controller = "MyTestController" - # ``` - # - # Defaults to `nil`. If this is falsy, `"ApplicationController"` is used. Can also be - # configured on a per-test basis using `with_controller_class`. - # - - # Set if render monkey patches should be included or not in Rails <6.1: - # - # ```ruby - # config.view_component.render_monkey_patch_enabled = false - # ``` - # - - # Path for component files - # - # ```ruby - # config.view_component.view_component_path = "app/my_components" - # ``` - # - # Defaults to `nil`. If this is falsy, `app/components` is used. - # - - # Parent class for generated components - # - # ```ruby - # config.view_component.component_parent_class = "MyBaseComponent" - # ``` - # - # Defaults to nil. If this is falsy, generators will use - # "ApplicationComponent" if defined, "ViewComponent::Base" otherwise. - # - # Configuration for generators. # # All options under this namespace default to `false` unless otherwise @@ -451,6 +440,18 @@ def safe_output_postamble # ``` # # Defaults to `false`. + # + # #### ßparent_class + # + # Parent class for generated components + # + # ```ruby + # config.view_component.generate.parent_class = "MyBaseComponent" + # ``` + # + # Defaults to nil. If this is falsy, generators will use + # "ApplicationComponent" if defined, "ViewComponent::Base" otherwise. + # class << self # The file path of the component Ruby file. @@ -519,50 +520,43 @@ def with_collection(collection, spacer_component: nil, **args) Collection.new(self, collection, spacer_component, **args) end + # @private + def __vc_compile(raise_errors: false, force: false) + __vc_compiler.compile(raise_errors: raise_errors, force: force) + end + # @private def inherited(child) # Compile so child will inherit compiled `call_*` template methods that # `compile` defines - compile + __vc_compile # Give the child its own personal #render_template_for to protect against the case when # eager loading is disabled and the parent component is rendered before the child. In # such a scenario, the parent will override ViewComponent::Base#render_template_for, # meaning it will not be called for any children and thus not compile their templates. - if !child.instance_methods(false).include?(:render_template_for) && !child.compiled? + if !child.instance_methods(false).include?(:render_template_for) && !child.__vc_compiled? child.class_eval <<~RUBY, __FILE__, __LINE__ + 1 - def render_template_for(variant = nil, format = nil) + def render_template_for(requested_details) # Force compilation here so the compiler always redefines render_template_for. # This is mostly a safeguard to prevent infinite recursion. - self.class.compile(raise_errors: true, force: true) - # .compile replaces this method; call the new one - render_template_for(variant, format) + self.class.__vc_compile(raise_errors: true, force: true) + # .__vc_compile replaces this method; call the new one + render_template_for(requested_details) end RUBY end - # If Rails application is loaded, add application url_helpers to the component context - # we need to check this to use this gem as a dependency - if defined?(Rails) && Rails.application && !(child < Rails.application.routes.url_helpers) - child.include Rails.application.routes.url_helpers - end - # Derive the source location of the component Ruby file from the call stack. # We need to ignore `inherited` frames here as they indicate that `inherited` # has been re-defined by the consuming application, likely in ApplicationComponent. # We use `base_label` method here instead of `label` to avoid cases where the method # owner is included in a prefix like `ApplicationComponent.inherited`. child.identifier = caller_locations(1, 10).reject { |l| l.base_label == "inherited" }[0].path - - # If Rails application is loaded, removes the first part of the path and the extension. - if defined?(Rails) && Rails.application - child.virtual_path = child.identifier.gsub( - /(.*#{Regexp.quote(ViewComponent::Base.config.view_component_path)})|(\.rb)/, "" - ) - end + child.virtual_path = child.name&.underscore # Set collection parameter to the extended component - child.with_collection_parameter provided_collection_parameter + child.with_collection_parameter(__vc_provided_collection_parameter) if instance_methods(false).include?(:render_template_for) vc_ancestor_calls = defined?(@__vc_ancestor_calls) ? @__vc_ancestor_calls.dup : [] @@ -575,22 +569,17 @@ def render_template_for(variant = nil, format = nil) end # @private - def compiled? - compiler.compiled? + def __vc_compiled? + __vc_compiler.compiled? end # @private - def ensure_compiled - compile unless compiled? + def __vc_ensure_compiled + __vc_compile unless __vc_compiled? end # @private - def compile(raise_errors: false, force: false) - compiler.compile(raise_errors: raise_errors, force: force) - end - - # @private - def compiler + def __vc_compiler @__vc_compiler ||= Compiler.new(self) end @@ -602,8 +591,8 @@ def compiler # # @param parameter [Symbol] The parameter name used when rendering elements of a collection. def with_collection_parameter(parameter) - @provided_collection_parameter = parameter - @initialize_parameters = nil + @__vc_provided_collection_parameter = parameter + @__vc_initialize_parameters = nil end # Strips trailing whitespace from templates before compiling them. @@ -632,18 +621,11 @@ def strip_trailing_whitespace? # is accepted, as support for collection # rendering is optional. # @private - def validate_collection_parameter!(validate_default: false) - parameter = validate_default ? collection_parameter : provided_collection_parameter + def __vc_validate_collection_parameter!(validate_default: false) + parameter = validate_default ? __vc_collection_parameter : __vc_provided_collection_parameter return unless parameter - return if initialize_parameter_names.include?(parameter) || splatted_keyword_argument_present? - - # If Ruby can't parse the component class, then the initialize - # parameters will be empty and ViewComponent will not be able to render - # the component. - if initialize_parameters.empty? - raise EmptyOrInvalidInitializerError.new(name, parameter) - end + return if __vc_initialize_parameter_names.include?(parameter) || __vc_splatted_keyword_argument_present? raise MissingCollectionArgumentError.new(name, parameter) end @@ -652,59 +634,58 @@ def validate_collection_parameter!(validate_default: false) # invalid parameters that could override the framework's # methods. # @private - def validate_initialization_parameters! - return unless initialize_parameter_names.include?(RESERVED_PARAMETER) + def __vc_validate_initialization_parameters! + return unless __vc_initialize_parameter_names.include?(:content) - raise ReservedParameterError.new(name, RESERVED_PARAMETER) + raise ReservedParameterError.new(name, :content) end # @private - def collection_parameter - @provided_collection_parameter ||= name && name.demodulize.underscore.chomp("_component").to_sym + def __vc_collection_parameter + @__vc_provided_collection_parameter ||= name && name.demodulize.underscore.chomp("_component").to_sym end # @private - def collection_counter_parameter - @collection_counter_parameter ||= :"#{collection_parameter}_counter" + def __vc_collection_counter_parameter + @__vc_collection_counter_parameter ||= :"#{__vc_collection_parameter}_counter" end # @private - def counter_argument_present? - initialize_parameter_names.include?(collection_counter_parameter) + def __vc_counter_argument_present? + __vc_initialize_parameter_names.include?(__vc_collection_counter_parameter) end # @private - def collection_iteration_parameter - @collection_iteration_parameter ||= :"#{collection_parameter}_iteration" + def __vc_collection_iteration_parameter + @__vc_collection_iteration_parameter ||= :"#{__vc_collection_parameter}_iteration" end # @private - def iteration_argument_present? - initialize_parameter_names.include?(collection_iteration_parameter) + def __vc_iteration_argument_present? + __vc_initialize_parameter_names.include?(__vc_collection_iteration_parameter) end private - def splatted_keyword_argument_present? - initialize_parameters.flatten.include?(:keyrest) && - !initialize_parameters.include?([:keyrest, :**]) # Un-named splatted keyword args don't count! + def __vc_splatted_keyword_argument_present? + __vc_initialize_parameters.flatten.include?(:keyrest) end - def initialize_parameter_names - @initialize_parameter_names ||= + def __vc_initialize_parameter_names + @__vc_initialize_parameter_names ||= if respond_to?(:attribute_names) attribute_names.map(&:to_sym) else - initialize_parameters.map(&:last) + __vc_initialize_parameters.map(&:last) end end - def initialize_parameters - @initialize_parameters ||= instance_method(:initialize).parameters + def __vc_initialize_parameters + @__vc_initialize_parameters ||= instance_method(:initialize).parameters end - def provided_collection_parameter - @provided_collection_parameter ||= nil + def __vc_provided_collection_parameter + @__vc_provided_collection_parameter ||= nil end end diff --git a/lib/view_component/capture_compatibility.rb b/lib/view_component/capture_compatibility.rb deleted file mode 100644 index 15477b3a3..000000000 --- a/lib/view_component/capture_compatibility.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -module ViewComponent - # CaptureCompatibility is a module that patches #capture to fix issues - # related to ViewComponent and functionality that relies on `capture` - # like forms, capture itself, turbo frames, etc. - # - # This underlying incompatibility with ViewComponent and capture is - # that several features like forms keep a reference to the primary - # `ActionView::Base` instance which has its own @output_buffer. When - # `#capture` is called on the original `ActionView::Base` instance while - # evaluating a block from a ViewComponent the @output_buffer is overridden - # in the ActionView::Base instance, and *not* the component. This results - # in a double render due to `#capture` implementation details. - # - # To resolve the issue, we override `#capture` so that we can delegate - # the `capture` logic to the ViewComponent that created the block. - module CaptureCompatibility - def self.included(base) - return if base < InstanceMethods - - base.class_eval do - alias_method :original_capture, :capture - end - - base.prepend(InstanceMethods) - end - - module InstanceMethods - def capture(*args, &block) - # Handle blocks that originate from C code and raise, such as `&:method` - return original_capture(*args, &block) if block.source_location.nil? - - block_context = block.binding.receiver - - if block_context != self && block_context.class < ActionView::Base - block_context.original_capture(*args, &block) - else - original_capture(*args, &block) - end - end - end - end -end diff --git a/lib/view_component/collection.rb b/lib/view_component/collection.rb index 798e38c25..d0e9234d5 100644 --- a/lib/view_component/collection.rb +++ b/lib/view_component/collection.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "action_view/renderer/collection_renderer" if Rails.version.to_f >= 6.1 +require "action_view/renderer/collection_renderer" module ViewComponent class Collection @@ -9,25 +9,24 @@ class Collection delegate :size, to: :@collection - attr_accessor :__vc_original_view_context - - def set_original_view_context(view_context) - self.__vc_original_view_context = view_context - end - def render_in(view_context, &block) components.map do |component| - component.set_original_view_context(__vc_original_view_context) component.render_in(view_context, &block) end.join(rendered_spacer(view_context)).html_safe end + def each(&block) + components.each(&block) + end + + private + def components return @components if defined? @components iterator = ActionView::PartialIteration.new(@collection.size) - component.validate_collection_parameter!(validate_default: true) + component.__vc_validate_collection_parameter!(validate_default: true) @components = @collection.map do |item| component.new(**component_options(item, iterator)).tap do |component| @@ -36,18 +35,6 @@ def components end end - def each(&block) - components.each(&block) - end - - # Rails expects us to define `format` on all renderables, - # but we do not know the `format` of a ViewComponent until runtime. - def format - nil - end - - private - def initialize(component, object, spacer_component, **options) @component = component @collection = collection_variable(object || []) @@ -64,16 +51,15 @@ def collection_variable(object) end def component_options(item, iterator) - item_options = {component.collection_parameter => item} - item_options[component.collection_counter_parameter] = iterator.index if component.counter_argument_present? - item_options[component.collection_iteration_parameter] = iterator.dup if component.iteration_argument_present? + item_options = {component.__vc_collection_parameter => item} + item_options[component.__vc_collection_counter_parameter] = iterator.index if component.__vc_counter_argument_present? + item_options[component.__vc_collection_iteration_parameter] = iterator.dup if component.__vc_iteration_argument_present? @options.merge(item_options) end def rendered_spacer(view_context) if @spacer_component - @spacer_component.set_original_view_context(__vc_original_view_context) @spacer_component.render_in(view_context) else "" diff --git a/lib/view_component/compiler.rb b/lib/view_component/compiler.rb index c4ab88df4..d972e7611 100644 --- a/lib/view_component/compiler.rb +++ b/lib/view_component/compiler.rb @@ -8,7 +8,7 @@ class Compiler # * true (a blocking mode which ensures thread safety when redefining the `call` method for components, # default in Rails development and test mode) # * false(a non-blocking mode, default in Rails production mode) - class_attribute :development_mode, default: false + class_attribute :__vc_development_mode, default: false def initialize(component) @component = component @@ -30,8 +30,8 @@ def compile(raise_errors: false, force: false) gather_templates - if self.class.development_mode && @templates.any?(&:requires_compiled_superclass?) - @component.superclass.compile(raise_errors: raise_errors) + if self.class.__vc_development_mode && @templates.any?(&:requires_compiled_superclass?) + @component.superclass.__vc_compile(raise_errors: raise_errors) end if template_errors.present? @@ -42,19 +42,45 @@ def compile(raise_errors: false, force: false) end if raise_errors - @component.validate_initialization_parameters! - @component.validate_collection_parameter! + @component.__vc_validate_initialization_parameters! + @component.__vc_validate_collection_parameter! end define_render_template_for - @component.register_default_slots - @component.build_i18n_backend + # Set the format if the component only responds to a single format. + # Unfortunately we cannot determine which format a multi-format + # component will respond to until render time, so those components + # will not set the response format. + # + # TODO: Investigate upstream changes necessary to support multi-format renderables + unique_formats = templates.map(&:format).uniq + @component.__vc_response_format = unique_formats.last if unique_formats.one? + + @component.__vc_register_default_slots + @component.__vc_build_i18n_backend CompileCache.register(@component) end end + # @return all matching compiled templates, in priority order based on the requested details from LookupContext + # + # @param [ActionView::TemplateDetails::Requested] requested_details i.e. locales, formats, variants + def find_templates_for(requested_details) + filtered_templates = @templates.select do |template| + template.details.matches?(requested_details) + end + + if filtered_templates.count > 1 + filtered_templates.sort_by! do |template| + template.details.sort_key_for(requested_details) + end + end + + filtered_templates + end + private attr_reader :templates @@ -64,40 +90,25 @@ def define_render_template_for template.compile_to_component end - method_body = - if @templates.one? - @templates.first.safe_method_name_call - elsif (template = @templates.find(&:inline?)) - template.safe_method_name_call - else - branches = [] - - @templates.each do |template| - conditional = - if template.inline_call? - "variant&.to_sym == #{template.variant.inspect}" - else - [ - template.default_format? ? "(format == #{ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT.inspect} || format.nil?)" : "format == #{template.format.inspect}", - template.variant.nil? ? "variant.nil?" : "variant&.to_sym == #{template.variant.inspect}" - ].join(" && ") - end - - branches << [conditional, template.safe_method_name_call] - end + @component.silence_redefinition_of_method(:render_template_for) - out = branches.each_with_object(+"") do |(conditional, branch_body), memo| - memo << "#{(!memo.present?) ? "if" : "elsif"} #{conditional}\n #{branch_body}\n" + if @templates.one? + template = @templates.first + safe_call = template.safe_method_name_call + @component.define_method(:render_template_for) do |_| + @current_template = template + instance_exec(&safe_call) + end + else + compiler = self + @component.define_method(:render_template_for) do |details| + if (@current_template = compiler.find_templates_for(details).first) + instance_exec(&@current_template.safe_method_name_call) + else + raise MissingTemplateError.new(self.class.name, details) end - out << "else\n #{templates.find { _1.variant.nil? && _1.default_format? }.safe_method_name_call}\nend" end - - @component.silence_redefinition_of_method(:render_template_for) - @component.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def render_template_for(variant = nil, format = nil) - #{method_body} end - RUBY end def template_errors @@ -106,10 +117,8 @@ def template_errors errors << "Couldn't find a template file or inline render method for #{@component}." if @templates.empty? - # We currently allow components to have both an inline call method and a template for a variant, with the - # inline call method overriding the template. We should aim to change this in v4 to instead - # raise an error. - @templates.reject(&:inline_call?) + @templates + .reject { |template| template.inline_call? && !template.defined_on_self? } .map { |template| [template.variant, template.format] } .tally .select { |_, count| count > 1 } @@ -168,30 +177,18 @@ def template_errors def gather_templates @templates ||= - begin + if @component.__vc_inline_template.present? + [Template::Inline.new( + component: @component, + inline_template: @component.__vc_inline_template + )] + else + path_parser = ActionView::Resolver::PathParser.new templates = @component.sidecar_files( ActionView::Template.template_handler_extensions ).map do |path| - # Extract format and variant from template filename - this_format, variant = - File - .basename(path) # "variants_component.html+mini.watch.erb" - .split(".")[1..-2] # ["html+mini", "watch"] - .join(".") # "html+mini.watch" - .split("+") # ["html", "mini.watch"] - .map(&:to_sym) # [:html, :"mini.watch"] - - out = Template.new( - component: @component, - type: :file, - path: path, - lineno: 0, - extension: path.split(".").last, - this_format: this_format.to_s.split(".").last&.to_sym, # strip locale from this_format, see #2113 - variant: variant - ) - - out + details = path_parser.parse(path).details + Template::File.new(component: @component, path: path, details: details) end component_instance_methods_on_self = @component.instance_methods(false) @@ -201,24 +198,10 @@ def gather_templates ).flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call(_|$)/) } .uniq .each do |method_name| - templates << Template.new( - component: @component, - type: :inline_call, - this_format: ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT, - variant: method_name.to_s.include?("call_") ? method_name.to_s.sub("call_", "").to_sym : nil, - method_name: method_name, - defined_on_self: component_instance_methods_on_self.include?(method_name) - ) - end - - if @component.inline_template.present? - templates << Template.new( + templates << Template::InlineCall.new( component: @component, - type: :inline, - path: @component.inline_template.path, - lineno: @component.inline_template.lineno, - source: @component.inline_template.source.dup, - extension: @component.inline_template.language + method_name: method_name, + defined_on_self: component_instance_methods_on_self.include?(method_name) ) end diff --git a/lib/view_component/component_error.rb b/lib/view_component/component_error.rb deleted file mode 100644 index 039e83e16..000000000 --- a/lib/view_component/component_error.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -module ViewComponent - class ComponentError < StandardError - end -end diff --git a/lib/view_component/config.rb b/lib/view_component/config.rb index 256846e90..d3c26f57e 100644 --- a/lib/view_component/config.rb +++ b/lib/view_component/config.rb @@ -13,19 +13,8 @@ class << self def defaults ActiveSupport::OrderedOptions.new.merge!({ generate: default_generate_options, - preview_controller: "ViewComponentsController", - preview_route: "/rails/view_components", - show_previews_source: false, - instrumentation_enabled: false, - use_deprecated_instrumentation_name: true, - render_monkey_patch_enabled: true, - view_component_path: "app/components", - component_parent_class: nil, - show_previews: Rails.env.development? || Rails.env.test?, - preview_paths: default_preview_paths, - test_controller: "ApplicationController", - default_preview_layout: nil, - capture_compatibility_patch_enabled: false + previews: default_previews_options, + instrumentation_enabled: false }) end @@ -36,6 +25,12 @@ def defaults # All options under this namespace default to `false` unless otherwise # stated. # + # #### `#path` + # + # Where to put generated components. Defaults to `app/components`: + # + # config.view_component.generate.path = "lib/components" + # # #### `#sidecar` # # Always generate a component with a sidecar directory: @@ -83,96 +78,56 @@ def defaults # # Required when there is more than one path defined in preview_paths. # Defaults to `""`. If this is blank, the generator will use - # `ViewComponent.config.preview_paths` if defined, + # `ViewComponent.config.previews.paths` if defined, # `"test/components/previews"` otherwise # # #### `#use_component_path_for_rspec_tests` # - # Whether to use the `config.view_component_path` when generating new + # Whether to use `config.generate.path` when generating new # RSpec component tests: # # config.view_component.generate.use_component_path_for_rspec_tests = true # - # When set to `true`, the generator will use the `view_component_path` to + # When set to `true`, the generator will use the `path` to # decide where to generate the new RSpec component test. - # For example, if the `view_component_path` is + # For example, if the `path` is # `app/views/components`, then the generator will create a new spec file # in `spec/views/components/` rather than the default `spec/components/`. - # @!attribute preview_controller - # @return [String] - # The controller used for previewing components. - # Defaults to `ViewComponentsController`. - - # @!attribute preview_route - # @return [String] - # The entry route for component previews. - # Defaults to `"/rails/view_components"`. - - # @!attribute show_previews_source - # @return [Boolean] - # Whether to display source code previews in component previews. - # Defaults to `false`. + # @!attribute previews + # @return [ActiveSupport::OrderedOptions] + # The subset of configuration options relating to previews. + # + # #### `#controller` + # + # The controller used for previewing components. Defaults to `ViewComponentsController`: + # + # config.view_component.previews.controller = "MyPreviewController" + # + # #### `#route` + # + # The entry route for component previews. Defaults to `/rails/view_components`: + # + # config.view_component.previews.route = "/my_previews" + # + # #### `#enabled` + # + # Whether component previews are enabled. Defaults to `true` in development and test environments: + # + # config.view_component.previews.enabled = false + # + # #### `#default_layout` + # + # A custom default layout used for the previews index page and individual previews. Defaults to `nil`: + # + # config.view_component.previews.default_layout = "preview_layout" + # # @!attribute instrumentation_enabled # @return [Boolean] # Whether ActiveSupport notifications are enabled. # Defaults to `false`. - # @!attribute use_deprecated_instrumentation_name - # @return [Boolean] - # Whether ActiveSupport Notifications use the private name `"!render.view_component"` - # or are made more publicly available via `"render.view_component"`. - # Will be removed in next major version. - # Defaults to `true`. - - # @!attribute render_monkey_patch_enabled - # @return [Boolean] Whether the #render method should be monkey patched. - # If this is disabled, use `#render_component` or - # `#render_component_to_string` instead. - # Defaults to `true`. - - # @!attribute view_component_path - # @return [String] - # The path in which components, their templates, and their sidecars should - # be stored. - # Defaults to `"app/components"`. - - # @!attribute component_parent_class - # @return [String] - # The parent class from which generated components will inherit. - # Defaults to `nil`. If this is falsy, generators will use - # `"ApplicationComponent"` if defined, `"ViewComponent::Base"` otherwise. - - # @!attribute show_previews - # @return [Boolean] - # Whether component previews are enabled. - # Defaults to `true` in development and test environments. - - # @!attribute preview_paths - # @return [Array] - # The locations in which component previews will be looked up. - # Defaults to `['test/components/previews']` relative to your Rails root. - - # @!attribute test_controller - # @return [String] - # The controller used for testing components. - # Can also be configured on a per-test basis using `#with_controller_class`. - # Defaults to `ApplicationController`. - - # @!attribute default_preview_layout - # @return [String] - # A custom default layout used for the previews index page and individual - # previews. - # Defaults to `nil`. If this is falsy, `"component_preview"` is used. - - # @!attribute capture_compatibility_patch_enabled - # @return [Boolean] - # Enables the experimental capture compatibility patch that makes ViewComponent - # compatible with forms, capture, and other built-ins. - # previews. - # Defaults to `false`. - def default_preview_paths (default_rails_preview_paths + default_rails_engines_preview_paths).uniq end @@ -200,6 +155,17 @@ def registered_rails_engines_with_previews def default_generate_options options = ActiveSupport::OrderedOptions.new(false) options.preview_path = "" + options.path = "app/components" + options + end + + def default_previews_options + options = ActiveSupport::OrderedOptions.new + options.controller = "ViewComponentsController" + options.route = "/rails/view_components" + options.enabled = Rails.env.development? || Rails.env.test? + options.default_layout = nil + options.paths = default_preview_paths options end end diff --git a/lib/view_component/configurable.rb b/lib/view_component/configurable.rb index 5fe68d21d..b878a7ab3 100644 --- a/lib/view_component/configurable.rb +++ b/lib/view_component/configurable.rb @@ -5,7 +5,7 @@ module Configurable extend ActiveSupport::Concern included do - next if respond_to?(:config) && config.respond_to?(:view_component) && config.respond_to_missing?(:test_controller) + next if respond_to?(:config) && config.respond_to?(:view_component) && config.respond_to_missing?(:instrumentation_enabled) include ActiveSupport::Configurable diff --git a/lib/view_component/deprecation.rb b/lib/view_component/deprecation.rb index bec827c91..d311960cd 100644 --- a/lib/view_component/deprecation.rb +++ b/lib/view_component/deprecation.rb @@ -3,6 +3,6 @@ require "active_support/deprecation" module ViewComponent - DEPRECATION_HORIZON = "4.0.0" + DEPRECATION_HORIZON = "5.0.0" Deprecation = ActiveSupport::Deprecation.new(DEPRECATION_HORIZON, "ViewComponent") end diff --git a/lib/view_component/engine.rb b/lib/view_component/engine.rb index b920eb5d8..73f9cd4d6 100644 --- a/lib/view_component/engine.rb +++ b/lib/view_component/engine.rb @@ -8,159 +8,99 @@ module ViewComponent class Engine < Rails::Engine # :nodoc: config.view_component = ViewComponent::Config.current - if Rails.version.to_f < 8.0 - rake_tasks do - load "view_component/rails/tasks/view_component.rake" - end - else - initializer "view_component.stats_directories" do |app| - require "rails/code_statistics" - - if Rails.root.join(ViewComponent::Base.view_component_path).directory? - Rails::CodeStatistics.register_directory("ViewComponents", ViewComponent::Base.view_component_path) - end - - if Rails.root.join("test/components").directory? - Rails::CodeStatistics.register_directory("ViewComponent tests", "test/components", test_directory: true) - end - end - end - initializer "view_component.set_configs" do |app| options = app.config.view_component - %i[generate preview_controller preview_route show_previews_source].each do |config_option| + %i[generate previews].each do |config_option| options[config_option] ||= ViewComponent::Base.public_send(config_option) end options.instrumentation_enabled = false if options.instrumentation_enabled.nil? - options.render_monkey_patch_enabled = true if options.render_monkey_patch_enabled.nil? - options.show_previews = (Rails.env.development? || Rails.env.test?) if options.show_previews.nil? + options.previews.enabled = (Rails.env.development? || Rails.env.test?) if options.previews.enabled.nil? - if options.show_previews + if options.previews.enabled # This is still necessary because when `config.view_component` is declared, `Rails.root` is unspecified. - options.preview_paths << "#{Rails.root}/test/components/previews" if defined?(Rails.root) && Dir.exist?( + options.previews.paths << "#{Rails.root}/test/components/previews" if defined?(Rails.root) && Dir.exist?( "#{Rails.root}/test/components/previews" ) - - if options.show_previews_source - require "method_source" - - app.config.to_prepare do - MethodSource.instance_variable_set(:@lines_for_file, {}) - end - end end end initializer "view_component.enable_instrumentation" do |app| ActiveSupport.on_load(:view_component) do if app.config.view_component.instrumentation_enabled.present? - # :nocov: Re-executing the below in tests duplicates initializers and causes order-dependent failures. ViewComponent::Base.prepend(ViewComponent::Instrumentation) - if app.config.view_component.use_deprecated_instrumentation_name - ViewComponent::Deprecation.deprecation_warning( - "!render.view_component", - "Use the new instrumentation key `render.view_component` instead. See https://viewcomponent.org/guide/instrumentation.html" - ) - end - # :nocov: end end end - # :nocov: - initializer "view_component.enable_capture_patch" do |app| - ActiveSupport.on_load(:view_component) do - ActionView::Base.include(ViewComponent::CaptureCompatibility) if app.config.view_component.capture_compatibility_patch_enabled - end - end - # :nocov: - initializer "view_component.set_autoload_paths" do |app| options = app.config.view_component - if options.show_previews && !options.preview_paths.empty? - paths_to_add = options.preview_paths - ActiveSupport::Dependencies.autoload_paths + if options.previews.enabled && !options.previews.paths.empty? + paths_to_add = options.previews.paths - ActiveSupport::Dependencies.autoload_paths ActiveSupport::Dependencies.autoload_paths.concat(paths_to_add) if paths_to_add.any? end end - initializer "view_component.eager_load_actions" do - ActiveSupport.on_load(:after_initialize) do - ViewComponent::Base.descendants.each(&:compile) if Rails.application.config.eager_load + initializer "view_component.propshaft_support" do |_app| + ActiveSupport.on_load(:view_component) do + if defined?(Propshaft) + include Propshaft::Helper + end end end - initializer "view_component.monkey_patch_render" do |app| - next if Rails.version.to_f >= 6.1 || !app.config.view_component.render_monkey_patch_enabled - - # :nocov: - ViewComponent::Deprecation.deprecation_warning("Monkey patching `render`", "ViewComponent 4.0 will remove the `render` monkey patch") - - ActiveSupport.on_load(:action_view) do - require "view_component/render_monkey_patch" - ActionView::Base.prepend ViewComponent::RenderMonkeyPatch - end - - ActiveSupport.on_load(:action_controller) do - require "view_component/rendering_monkey_patch" - require "view_component/render_to_string_monkey_patch" - ActionController::Base.prepend ViewComponent::RenderingMonkeyPatch - ActionController::Base.prepend ViewComponent::RenderToStringMonkeyPatch - end - # :nocov: - end + config.after_initialize do |app| + ActiveSupport.on_load(:view_component) do + if defined?(Sprockets::Rails) + include Sprockets::Rails::Helper - initializer "view_component.include_render_component" do |_app| - next if Rails.version.to_f >= 6.1 + # Copy relevant config to VC context + # See: https://github.com/rails/sprockets-rails/blob/266ec49f3c7c96018dd75f9dc4f9b62fe3f7eecf/lib/sprockets/railtie.rb#L245 + self.debug_assets = app.config.assets.debug + self.digest_assets = app.config.assets.digest + self.assets_prefix = app.config.assets.prefix + self.assets_precompile = app.config.assets.precompile - # :nocov: - ViewComponent::Deprecation.deprecation_warning("using `render_component`", "ViewComponent 4.0 will remove `render_component`") + self.assets_environment = app.assets + self.assets_manifest = app.assets_manifest - ActiveSupport.on_load(:action_view) do - require "view_component/render_component_helper" - ActionView::Base.include ViewComponent::RenderComponentHelper - end + self.resolve_assets_with = app.config.assets.resolve_with - ActiveSupport.on_load(:action_controller) do - require "view_component/rendering_component_helper" - require "view_component/render_component_to_string_helper" - ActionController::Base.include ViewComponent::RenderingComponentHelper - ActionController::Base.include ViewComponent::RenderComponentToStringHelper + self.check_precompiled_asset = app.config.assets.check_precompiled_asset + self.unknown_asset_fallback = app.config.assets.unknown_asset_fallback + # Expose the app precompiled asset check to the view + self.precompiled_asset_checker = ->(logical_path) { app.asset_precompiled? logical_path } + end end - # :nocov: end - initializer "static assets" do |app| - if serve_static_preview_assets?(app.config) - app.middleware.use(::ActionDispatch::Static, "#{root}/app/assets/vendor") + initializer "view_component.eager_load_actions" do + ActiveSupport.on_load(:after_initialize) do + ViewComponent::Base.descendants.each(&:__vc_compile) if Rails.application.config.eager_load end end - def serve_static_preview_assets?(app_config) - app_config.view_component.show_previews && app_config.public_file_server.enabled - end - initializer "compiler mode" do |_app| - ViewComponent::Compiler.development_mode = (Rails.env.development? || Rails.env.test?) + ViewComponent::Compiler.__vc_development_mode = (Rails.env.development? || Rails.env.test?) end config.after_initialize do |app| options = app.config.view_component - if options.show_previews + if options.previews.enabled app.routes.prepend do - preview_controller = options.preview_controller.sub(/Controller$/, "").underscore + preview_controller = options.previews.controller.sub(/Controller$/, "").underscore get( - options.preview_route, + options.previews.route, to: "#{preview_controller}#index", as: :preview_view_components, internal: true ) get( - "#{options.preview_route}/*path", + "#{options.previews.route}/*path", to: "#{preview_controller}#previews", as: :preview_view_component, internal: true @@ -174,16 +114,6 @@ def serve_static_preview_assets?(app_config) end end - # :nocov: - if RUBY_VERSION < "3.2.0" - ViewComponent::Deprecation.deprecation_warning("Support for Ruby versions < 3.2.0", "ViewComponent v4 will remove support for Ruby versions < 3.2.0 no earlier than April 1, 2025") - end - - if Rails.version.to_f < 7.1 - ViewComponent::Deprecation.deprecation_warning("Support for Rails versions < 7.1", "ViewComponent v4 will remove support for Rails versions < 7.1 no earlier than April 1, 2025") - end - # :nocov: - app.executor.to_run :before do CompileCache.invalidate! unless ActionView::Base.cache_template_loading end diff --git a/lib/view_component/errors.rb b/lib/view_component/errors.rb index d6bd071ab..b6aa38e4d 100644 --- a/lib/view_component/errors.rb +++ b/lib/view_component/errors.rb @@ -38,6 +38,22 @@ def initialize(example) end end + class MissingTemplateError < StandardError + MESSAGE = + "No templates for COMPONENT match the request DETAIL.\n\n" \ + "To fix this issue, provide a suitable template." + + def initialize(component, request_detail) + detail = { + locale: request_detail.locale, + formats: request_detail.formats, + variants: request_detail.variants, + handlers: request_detail.handlers + } + super(MESSAGE.gsub("COMPONENT", component).gsub("DETAIL", detail.inspect)) + end + end + class DuplicateContentError < StandardError MESSAGE = "It looks like a block was provided after calling `with_content` on COMPONENT, " \ @@ -49,18 +65,6 @@ def initialize(klass_name) end end - class EmptyOrInvalidInitializerError < StandardError - MESSAGE = - "The COMPONENT initializer is empty or invalid. " \ - "It must accept the parameter `PARAMETER` to render it as a collection.\n\n" \ - "To fix this issue, update the initializer to accept `PARAMETER`.\n\n" \ - "See [the collections docs](https://viewcomponent.org/guide/collections.html) for more information on rendering collections." - - def initialize(klass_name, parameter) - super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("PARAMETER", parameter.to_s)) - end - end - class MissingCollectionArgumentError < StandardError MESSAGE = "The initializer for COMPONENT doesn't accept the parameter `PARAMETER`, " \ @@ -202,28 +206,6 @@ class ControllerCalledBeforeRenderError < BaseError "`#controller` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void)." end - # :nocov: - class NoMatchingTemplatesForPreviewError < StandardError - MESSAGE = "Found 0 matches for templates for TEMPLATE_IDENTIFIER." - - def initialize(template_identifier) - super(MESSAGE.gsub("TEMPLATE_IDENTIFIER", template_identifier)) - end - end - - class MultipleMatchingTemplatesForPreviewError < StandardError - MESSAGE = "Found multiple templates for TEMPLATE_IDENTIFIER." - - def initialize(template_identifier) - super(MESSAGE.gsub("TEMPLATE_IDENTIFIER", template_identifier)) - end - end - # :nocov: - - class SystemTestControllerOnlyAllowedInTestError < BaseError - MESSAGE = "ViewComponent SystemTest controller must only be called in a test environment for security reasons." - end - class SystemTestControllerNefariousPathError < BaseError MESSAGE = "ViewComponent SystemTest controller attempted to load a file outside of the expected directory." end diff --git a/lib/view_component/inline_template.rb b/lib/view_component/inline_template.rb index 1b43d88e7..06152e99b 100644 --- a/lib/view_component/inline_template.rb +++ b/lib/view_component/inline_template.rb @@ -32,23 +32,22 @@ def method_missing(method, *args) @__vc_inline_template_defined = true end - ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true) def respond_to_missing?(method, include_all = false) method.end_with?("_template") || super end - def inline_template + def __vc_inline_template @__vc_inline_template if defined?(@__vc_inline_template) end - def inline_template_language + def __vc_inline_template_language @__vc_inline_template_language if defined?(@__vc_inline_template_language) end def inherited(subclass) super - subclass.instance_variable_set(:@__vc_inline_template_language, inline_template_language) + subclass.instance_variable_set(:@__vc_inline_template_language, __vc_inline_template_language) end end end diff --git a/lib/view_component/instrumentation.rb b/lib/view_component/instrumentation.rb index d63efd283..2b50d84f2 100644 --- a/lib/view_component/instrumentation.rb +++ b/lib/view_component/instrumentation.rb @@ -5,12 +5,14 @@ module ViewComponent # :nodoc: module Instrumentation def self.included(mod) - mod.prepend(self) unless ancestors.include?(ViewComponent::Instrumentation) + mod.prepend(self) unless self <= ViewComponent::Instrumentation end def render_in(view_context, &block) + return super if !Rails.application.config.view_component.instrumentation_enabled.present? + ActiveSupport::Notifications.instrument( - notification_name, + "render.view_component", { name: self.class.name, identifier: self.class.identifier @@ -19,13 +21,5 @@ def render_in(view_context, &block) super end end - - private - - def notification_name - return "!render.view_component" if ViewComponent::Base.config.use_deprecated_instrumentation_name - - "render.view_component" - end end end diff --git a/lib/view_component/preview.rb b/lib/view_component/preview.rb index 2d3c84465..64a1c0885 100644 --- a/lib/view_component/preview.rb +++ b/lib/view_component/preview.rb @@ -30,12 +30,10 @@ def render_with_template(template: nil, locals: {}) } end - alias_method :render_component, :render - class << self # Returns all component preview classes. def all - load_previews + __vc_load_previews descendants end @@ -94,13 +92,8 @@ def preview_example_template_path(example) .sub(/\..*$/, "") end - # Returns the method body for the example from the preview file. - def preview_source(example) - source = instance_method(example.to_sym).source.split("\n") - source[1...(source.size - 1)].join("\n") - end - - def load_previews + # @private + def __vc_load_previews Array(preview_paths).each do |preview_path| Dir["#{preview_path}/**/*preview.rb"].sort.each { |file| require_dependency file } end @@ -109,7 +102,7 @@ def load_previews private def preview_paths - Base.preview_paths + Base.previews.paths end end end diff --git a/lib/view_component/rails/tasks/view_component.rake b/lib/view_component/rails/tasks/view_component.rake deleted file mode 100644 index ae8662cd1..000000000 --- a/lib/view_component/rails/tasks/view_component.rake +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -task stats: "view_component:statsetup" - -namespace :view_component do - task :statsetup do - # :nocov: - require "rails/code_statistics" - - if Rails.root.join(ViewComponent::Base.view_component_path).directory? - ::STATS_DIRECTORIES << ["ViewComponents", ViewComponent::Base.view_component_path] - end - - if Rails.root.join("test/components").directory? - ::STATS_DIRECTORIES << ["ViewComponent tests", "test/components"] - CodeStatistics::TEST_TYPES << "ViewComponent tests" - end - # :nocov: - end -end diff --git a/lib/view_component/render_component_helper.rb b/lib/view_component/render_component_helper.rb deleted file mode 100644 index 945cd539d..000000000 --- a/lib/view_component/render_component_helper.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -module ViewComponent - module RenderComponentHelper # :nodoc: - def render_component(component, &block) - component.set_original_view_context(__vc_original_view_context) if is_a?(ViewComponent::Base) - component.render_in(self, &block) - end - end -end diff --git a/lib/view_component/render_component_to_string_helper.rb b/lib/view_component/render_component_to_string_helper.rb deleted file mode 100644 index dff55587c..000000000 --- a/lib/view_component/render_component_to_string_helper.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module ViewComponent - module RenderComponentToStringHelper # :nodoc: - def render_component_to_string(component) - component.render_in(view_context) - end - end -end diff --git a/lib/view_component/render_monkey_patch.rb b/lib/view_component/render_monkey_patch.rb deleted file mode 100644 index 0d05ac394..000000000 --- a/lib/view_component/render_monkey_patch.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module ViewComponent - module RenderMonkeyPatch # :nodoc: - def render(options = {}, args = {}, &block) - if options.respond_to?(:render_in) - options.render_in(self, &block) - else - super - end - end - end -end diff --git a/lib/view_component/render_to_string_monkey_patch.rb b/lib/view_component/render_to_string_monkey_patch.rb deleted file mode 100644 index 325646f07..000000000 --- a/lib/view_component/render_to_string_monkey_patch.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module ViewComponent - module RenderToStringMonkeyPatch # :nodoc: - def render_to_string(options = {}, args = {}) - if options.respond_to?(:render_in) - options.render_in(view_context) - else - super - end - end - end -end diff --git a/lib/view_component/rendering_component_helper.rb b/lib/view_component/rendering_component_helper.rb deleted file mode 100644 index dc7777efb..000000000 --- a/lib/view_component/rendering_component_helper.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module ViewComponent - module RenderingComponentHelper # :nodoc: - def render_component(component) - self.response_body = component.render_in(view_context) - end - end -end diff --git a/lib/view_component/rendering_monkey_patch.rb b/lib/view_component/rendering_monkey_patch.rb deleted file mode 100644 index ac7df3380..000000000 --- a/lib/view_component/rendering_monkey_patch.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module ViewComponent - module RenderingMonkeyPatch # :nodoc: - def render(options = {}, args = {}) - if options.respond_to?(:render_in) - self.response_body = options.render_in(view_context) - else - super - end - end - end -end diff --git a/lib/view_component/request_details.rb b/lib/view_component/request_details.rb new file mode 100644 index 000000000..c502ce1eb --- /dev/null +++ b/lib/view_component/request_details.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module ViewComponent + # LookupContext computes and encapsulates @details for each request + # so that it doesn't need to be recomputed on each partial render. + # This data is wrapped in ActionView::TemplateDetails::Requested and + # used by instances of ActionView::Resolver to choose which template + # best matches the request. + # + # ActionView considers this logic internal to template/partial resolution. + # We're exposing it to the compiler via `refine` so that ViewComponent + # can match Rails' template picking logic. + module RequestDetails + refine ActionView::LookupContext do + # Return an abstraction for matching and sorting available templates + # based on the current lookup context details. + # + # @return ActionView::TemplateDetails::Requested + # @see ActionView::LookupContext#detail_args_for + # @see ActionView::FileSystemResolver#_find_all + def vc_requested_details(user_details = {}) + # The hash `user_details` would normally be the standard arguments that + # `render` accepts, but there's currently no mechanism for users to + # provide these when calling render on a ViewComponent. + details, cached = detail_args_for(user_details) + cached || ActionView::TemplateDetails::Requested.new(**details) + end + end + end +end diff --git a/lib/view_component/slot.rb b/lib/view_component/slot.rb index 205437040..d5c745ec9 100644 --- a/lib/view_component/slot.rb +++ b/lib/view_component/slot.rb @@ -58,15 +58,7 @@ def to_s if defined?(@__vc_content_block) # render_in is faster than `parent.render` @__vc_component_instance.render_in(view_context) do |*args| - return @__vc_content_block.call(*args) if @__vc_content_block&.source_location.nil? - - block_context = @__vc_content_block.binding.receiver - - if block_context.class < ActionView::Base - block_context.capture(*args, &@__vc_content_block) - else - @__vc_content_block.call(*args) - end + @__vc_content_block.call(*args) end else @__vc_component_instance.render_in(view_context) @@ -101,15 +93,12 @@ def to_s # end # end # - def method_missing(symbol, *args, &block) - @__vc_component_instance.public_send(symbol, *args, &block) + def method_missing(symbol, *args, **kwargs, &block) + @__vc_component_instance.public_send(symbol, *args, **kwargs, &block) end - ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true) def html_safe? - # :nocov: to_s.html_safe? - # :nocov: end def respond_to_missing?(symbol, include_all = false) diff --git a/lib/view_component/slotable.rb b/lib/view_component/slotable.rb index 365a30c0f..ac78b320e 100644 --- a/lib/view_component/slotable.rb +++ b/lib/view_component/slotable.rb @@ -12,12 +12,10 @@ module Slotable singular: %i[content render].freeze, plural: %i[contents renders].freeze }.freeze + private_constant :RESERVED_NAMES - # Setup component slot state included do - # Hash of registered Slots - class_attribute :registered_slots - self.registered_slots = {} + class_attribute :registered_slots, default: {} end class_methods do @@ -75,26 +73,25 @@ module Slotable # # <%= render_inline(MyComponent.new.with_header_content("Foo")) %> def renders_one(slot_name, callable = nil) - validate_singular_slot_name(slot_name) + __vc_validate_singular_slot_name(slot_name) if callable.is_a?(Hash) && callable.key?(:types) - register_polymorphic_slot(slot_name, callable[:types], collection: false) + __vc_register_polymorphic_slot(slot_name, callable[:types], collection: false) else - validate_plural_slot_name(ActiveSupport::Inflector.pluralize(slot_name).to_sym) + __vc_validate_plural_slot_name(ActiveSupport::Inflector.pluralize(slot_name).to_sym) setter_method_name = :"with_#{slot_name}" - define_method setter_method_name do |*args, &block| - set_slot(slot_name, nil, *args, &block) + define_method setter_method_name do |*args, **kwargs, &block| + __vc_set_slot(slot_name, nil, *args, **kwargs, &block) end - ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true) self::GeneratedSlotMethods.define_method slot_name do - get_slot(slot_name) + __vc_get_slot(slot_name) end self::GeneratedSlotMethods.define_method :"#{slot_name}?" do - get_slot(slot_name).present? + __vc_get_slot(slot_name).present? end define_method :"with_#{slot_name}_content" do |content| @@ -103,7 +100,7 @@ def renders_one(slot_name, callable = nil) self end - register_slot(slot_name, collection: false, callable: callable) + __vc_register_slot(slot_name, collection: false, callable: callable) end end @@ -145,20 +142,19 @@ def renders_one(slot_name, callable = nil) # <% end %> # <% end %> def renders_many(slot_name, callable = nil) - validate_plural_slot_name(slot_name) + __vc_validate_plural_slot_name(slot_name) if callable.is_a?(Hash) && callable.key?(:types) - register_polymorphic_slot(slot_name, callable[:types], collection: true) + __vc_register_polymorphic_slot(slot_name, callable[:types], collection: true) else singular_name = ActiveSupport::Inflector.singularize(slot_name) - validate_singular_slot_name(ActiveSupport::Inflector.singularize(slot_name).to_sym) + __vc_validate_singular_slot_name(ActiveSupport::Inflector.singularize(slot_name).to_sym) setter_method_name = :"with_#{singular_name}" - define_method setter_method_name do |*args, &block| - set_slot(slot_name, nil, *args, &block) + define_method setter_method_name do |*args, **kwargs, &block| + __vc_set_slot(slot_name, nil, *args, **kwargs, &block) end - ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true) define_method :"with_#{singular_name}_content" do |content| send(setter_method_name) { content.to_s } @@ -169,22 +165,22 @@ def renders_many(slot_name, callable = nil) define_method :"with_#{slot_name}" do |collection_args = nil, &block| collection_args.map do |args| if args.respond_to?(:to_hash) - set_slot(slot_name, nil, **args, &block) + __vc_set_slot(slot_name, nil, **args, &block) else - set_slot(slot_name, nil, *args, &block) + __vc_set_slot(slot_name, nil, *args, &block) end end end self::GeneratedSlotMethods.define_method slot_name do - get_slot(slot_name) + __vc_get_slot(slot_name) end self::GeneratedSlotMethods.define_method :"#{slot_name}?" do - get_slot(slot_name).present? + __vc_get_slot(slot_name).present? end - register_slot(slot_name, collection: true, callable: callable) + __vc_register_slot(slot_name, collection: true, callable: callable) end end @@ -215,13 +211,30 @@ def inherited(child) super end - def register_polymorphic_slot(slot_name, types, collection:) + # @private + # Called by the compiler, as instance methods are not defined when slots are first registered + def __vc_register_default_slots + registered_slots.each do |slot_name, config| + default_method_name = :"default_#{slot_name}" + config[:default_method] = instance_methods.find { |method_name| method_name == default_method_name } + + registered_slots[slot_name] = config + end + end + + private + + def __vc_register_slot(slot_name, **kwargs) + registered_slots[slot_name] = __vc_define_slot(slot_name, **kwargs) + end + + def __vc_register_polymorphic_slot(slot_name, types, collection:) self::GeneratedSlotMethods.define_method(slot_name) do - get_slot(slot_name) + __vc_get_slot(slot_name) end self::GeneratedSlotMethods.define_method(:"#{slot_name}?") do - get_slot(slot_name).present? + __vc_get_slot(slot_name).present? end renderable_hash = types.each_with_object({}) do |(poly_type, poly_attributes_or_callable), memo| @@ -240,7 +253,7 @@ def register_polymorphic_slot(slot_name, types, collection:) "#{slot_name}_#{poly_type}" end - memo[poly_type] = define_slot( + memo[poly_type] = __vc_define_slot( poly_slot_name, collection: collection, callable: poly_callable ) @@ -250,10 +263,9 @@ def register_polymorphic_slot(slot_name, types, collection:) raise AlreadyDefinedPolymorphicSlotSetterError.new(setter_method_name, poly_slot_name) end - define_method(setter_method_name) do |*args, &block| - set_polymorphic_slot(slot_name, poly_type, *args, &block) + define_method(setter_method_name) do |*args, **kwargs, &block| + __vc_set_polymorphic_slot(slot_name, poly_type, *args, **kwargs, &block) end - ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true) define_method :"with_#{poly_slot_name}_content" do |content| send(setter_method_name) { content.to_s } @@ -268,23 +280,7 @@ def register_polymorphic_slot(slot_name, types, collection:) } end - # Called by the compiler, as instance methods are not defined when slots are first registered - def register_default_slots - registered_slots.each do |slot_name, config| - default_method_name = :"default_#{slot_name}" - config[:default_method] = instance_methods.find { |method_name| method_name == default_method_name } - - registered_slots[slot_name] = config - end - end - - private - - def register_slot(slot_name, **kwargs) - registered_slots[slot_name] = define_slot(slot_name, **kwargs) - end - - def define_slot(slot_name, collection:, callable:) + def __vc_define_slot(slot_name, collection:, callable:) slot = {collection: collection} return slot unless callable @@ -307,18 +303,18 @@ def define_slot(slot_name, collection:, callable:) slot end - def validate_plural_slot_name(slot_name) + def __vc_validate_plural_slot_name(slot_name) if RESERVED_NAMES[:plural].include?(slot_name.to_sym) raise ReservedPluralSlotNameError.new(name, slot_name) end - raise_if_slot_name_uncountable(slot_name) - raise_if_slot_conflicts_with_call(slot_name) - raise_if_slot_ends_with_question_mark(slot_name) - raise_if_slot_registered(slot_name) + __vc_raise_if_slot_name_uncountable(slot_name) + __vc_raise_if_slot_conflicts_with_call(slot_name) + __vc_raise_if_slot_ends_with_question_mark(slot_name) + __vc_raise_if_slot_registered(slot_name) end - def validate_singular_slot_name(slot_name) + def __vc_validate_singular_slot_name(slot_name) if slot_name.to_sym == :content raise ContentSlotNameError.new(name) end @@ -327,28 +323,28 @@ def validate_singular_slot_name(slot_name) raise ReservedSingularSlotNameError.new(name, slot_name) end - raise_if_slot_conflicts_with_call(slot_name) - raise_if_slot_ends_with_question_mark(slot_name) - raise_if_slot_registered(slot_name) + __vc_raise_if_slot_conflicts_with_call(slot_name) + __vc_raise_if_slot_ends_with_question_mark(slot_name) + __vc_raise_if_slot_registered(slot_name) end - def raise_if_slot_registered(slot_name) + def __vc_raise_if_slot_registered(slot_name) if registered_slots.key?(slot_name) raise RedefinedSlotError.new(name, slot_name) end end - def raise_if_slot_ends_with_question_mark(slot_name) + def __vc_raise_if_slot_ends_with_question_mark(slot_name) raise SlotPredicateNameError.new(name, slot_name) if slot_name.to_s.end_with?("?") end - def raise_if_slot_conflicts_with_call(slot_name) + def __vc_raise_if_slot_conflicts_with_call(slot_name) if slot_name.start_with?("call_") raise InvalidSlotNameError, "Slot cannot start with 'call_'. Please rename #{slot_name}" end end - def raise_if_slot_name_uncountable(slot_name) + def __vc_raise_if_slot_name_uncountable(slot_name) slot_name = slot_name.to_s if slot_name.pluralize == slot_name.singularize raise UncountableSlotNameError.new(name, slot_name) @@ -356,22 +352,32 @@ def raise_if_slot_name_uncountable(slot_name) end end - def get_slot(slot_name) - content unless content_evaluated? # ensure content is loaded so slots will be defined - - slot = self.class.registered_slots[slot_name] + def __vc_get_slot(slot_name) @__vc_set_slots ||= {} + content unless defined?(@__vc_content_evaluated) && @__vc_content_evaluated # ensure content is loaded so slots will be defined - if @__vc_set_slots[slot_name] - return @__vc_set_slots[slot_name] - end + # If the slot is set, return it + return @__vc_set_slots[slot_name] if @__vc_set_slots[slot_name] + + # If there is a default method for the slot, call it + if (default_method = registered_slots[slot_name][:default_method]) + renderable_value = send(default_method) + slot = Slot.new(self) - if slot[:collection] + if renderable_value.respond_to?(:render_in) + slot.__vc_component_instance = renderable_value + else + slot.__vc_content = renderable_value + end + + slot + elsif self.class.registered_slots[slot_name][:collection] + # If empty slot is a collection, return an empty array [] end end - def set_slot(slot_name, slot_definition = nil, *args, &block) + def __vc_set_slot(slot_name, slot_definition = nil, *args, **kwargs, &block) slot_definition ||= self.class.registered_slots[slot_name] slot = Slot.new(self) @@ -388,11 +394,11 @@ def set_slot(slot_name, slot_definition = nil, *args, &block) # If class if slot_definition[:renderable] - slot.__vc_component_instance = slot_definition[:renderable].new(*args) + slot.__vc_component_instance = slot_definition[:renderable].new(*args, **kwargs) # If class name as a string elsif slot_definition[:renderable_class_name] slot.__vc_component_instance = - self.class.const_get(slot_definition[:renderable_class_name]).new(*args) + self.class.const_get(slot_definition[:renderable_class_name]).new(*args, **kwargs) # If passed a lambda elsif slot_definition[:renderable_function] # Use `bind(self)` to ensure lambda is executed in the context of the @@ -401,11 +407,11 @@ def set_slot(slot_name, slot_definition = nil, *args, &block) renderable_function = slot_definition[:renderable_function].bind(self) renderable_value = if block - renderable_function.call(*args) do |*rargs| + renderable_function.call(*args, **kwargs) do |*rargs| view_context.capture(*rargs, &block) end else - renderable_function.call(*args) + renderable_function.call(*args, **kwargs) end # Function calls can return components, so if it's a component handle it specially @@ -427,9 +433,8 @@ def set_slot(slot_name, slot_definition = nil, *args, &block) slot end - ruby2_keywords(:set_slot) if respond_to?(:ruby2_keywords, true) - def set_polymorphic_slot(slot_name, poly_type = nil, *args, &block) + def __vc_set_polymorphic_slot(slot_name, poly_type = nil, *args, **kwargs, &block) slot_definition = self.class.registered_slots[slot_name] if !slot_definition[:collection] && defined?(@__vc_set_slots) && @__vc_set_slots[slot_name] @@ -438,8 +443,7 @@ def set_polymorphic_slot(slot_name, poly_type = nil, *args, &block) poly_def = slot_definition[:renderable_hash][poly_type] - set_slot(slot_name, poly_def, *args, &block) + __vc_set_slot(slot_name, poly_def, *args, **kwargs, &block) end - ruby2_keywords(:set_polymorphic_slot) if respond_to?(:ruby2_keywords, true) end end diff --git a/lib/view_component/slotable_default.rb b/lib/view_component/slotable_default.rb deleted file mode 100644 index 538622f01..000000000 --- a/lib/view_component/slotable_default.rb +++ /dev/null @@ -1,20 +0,0 @@ -module ViewComponent - module SlotableDefault - def get_slot(slot_name) - @__vc_set_slots ||= {} - - return super unless !@__vc_set_slots[slot_name] && (default_method = registered_slots[slot_name][:default_method]) - - renderable_value = send(default_method) - slot = Slot.new(self) - - if renderable_value.respond_to?(:render_in) - slot.__vc_component_instance = renderable_value - else - slot.__vc_content = renderable_value - end - - slot - end - end -end diff --git a/lib/view_component/system_spec_helpers.rb b/lib/view_component/system_spec_helpers.rb new file mode 100644 index 000000000..044e7ae5f --- /dev/null +++ b/lib/view_component/system_spec_helpers.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module ViewComponent + module SystemSpecHelpers + include SystemTestHelpers + + def page + Capybara.current_session + end + end +end diff --git a/lib/view_component/system_test_helpers.rb b/lib/view_component/system_test_helpers.rb index 351df67fd..c5677e63b 100644 --- a/lib/view_component/system_test_helpers.rb +++ b/lib/view_component/system_test_helpers.rb @@ -4,7 +4,6 @@ module ViewComponent module SystemTestHelpers include TestHelpers - # # Returns a block that can be used to visit the path of the inline rendered component. # @param fragment [Nokogiri::Fragment] The fragment returned from `render_inline`. # @param layout [String] The (optional) layout to use. @@ -18,7 +17,7 @@ def with_rendered_component_path(fragment, layout: false, &block) file.write(vc_test_controller.render_to_string(html: fragment.to_html.html_safe, layout: layout)) file.rewind - block.call("/_system_test_entrypoint?file=#{file.path.split("/").last}") + yield("/_system_test_entrypoint?file=#{file.path.split("/").last}") ensure file.unlink end diff --git a/lib/view_component/template.rb b/lib/view_component/template.rb index ebdeb0e87..3b0ab34e2 100644 --- a/lib/view_component/template.rb +++ b/lib/view_component/template.rb @@ -2,69 +2,115 @@ module ViewComponent class Template + DEFAULT_FORMAT = :html + private_constant :DEFAULT_FORMAT + DataWithSource = Struct.new(:format, :identifier, :short_identifier, :type, keyword_init: true) - DataNoSource = Struct.new(:source, :identifier, :type, keyword_init: true) - - attr_reader :variant, :this_format, :type - - def initialize( - component:, - type:, - this_format: nil, - variant: nil, - lineno: nil, - path: nil, - extension: nil, - source: nil, - method_name: nil, - defined_on_self: true - ) + + attr_reader :details, :path + + delegate :virtual_path, to: :@component + delegate :format, :variant, to: :@details + + def initialize(component:, details:, lineno: nil, path: nil) @component = component - @type = type - @this_format = this_format - @variant = variant&.to_sym + @details = details @lineno = lineno @path = path - @extension = extension - @source = source - @method_name = method_name - @defined_on_self = defined_on_self - - @source_originally_nil = @source.nil? - - @call_method_name = - if @method_name - @method_name - else - out = +"call" - out << "_#{normalized_variant_name}" if @variant.present? - out << "_#{@this_format}" if @this_format.present? && @this_format != ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT - out + end + + class File < Template + def initialize(component:, details:, path:) + super( + component: component, + details: details, + path: path, + lineno: 0 + ) + end + + def type + :file + end + + # Load file each time we look up #source in case the file has been modified + def source + ::File.read(@path) + end + end + + class Inline < Template + attr_reader :source + + def initialize(component:, inline_template:) + details = ActionView::TemplateDetails.new(nil, inline_template.language.to_sym, DEFAULT_FORMAT, nil) + + super( + component: component, + details: details, + path: inline_template.path, + lineno: inline_template.lineno, + ) + + @source = inline_template.source.dup + end + + def type + :inline + end + end + + class InlineCall < Template + def initialize(component:, method_name:, defined_on_self:) + variant = method_name.to_s.include?("call_") ? method_name.to_s.sub("call_", "").to_sym : nil + details = ActionView::TemplateDetails.new(nil, nil, DEFAULT_FORMAT, variant) + + super(component: component, details: details) + + @call_method_name = method_name + @defined_on_self = defined_on_self + end + + def type + :inline_call + end + + def compile_to_component + @component.define_method(safe_method_name, @component.instance_method(@call_method_name)) + end + + def safe_method_name_call + m = safe_method_name + proc do + __vc_maybe_escape_html(send(m)) do + Kernel.warn("WARNING: The #{self.class} component rendered HTML-unsafe output. " \ + "The output will be automatically escaped, but you may want to investigate.") + end end + end + + def defined_on_self? + @defined_on_self + end end def compile_to_component - if !inline_call? - @component.silence_redefinition_of_method(@call_method_name) + @component.silence_redefinition_of_method(call_method_name) - # rubocop:disable Style/EvalWithLocation - @component.class_eval <<-RUBY, @path, @lineno - def #{@call_method_name} + # rubocop:disable Style/EvalWithLocation + @component.class_eval <<~RUBY, @path, @lineno + def #{call_method_name} #{compiled_source} end - RUBY - # rubocop:enable Style/EvalWithLocation - end + RUBY + # rubocop:enable Style/EvalWithLocation @component.define_method(safe_method_name, @component.instance_method(@call_method_name)) end def safe_method_name_call - return safe_method_name unless inline_call? - - "maybe_escape_html(#{safe_method_name}) " \ - "{ Kernel.warn(\"WARNING: The #{@component} component rendered HTML-unsafe output. " \ - "The output will be automatically escaped, but you may want to investigate.\") } " + m = safe_method_name + proc { send(m) } end def requires_compiled_superclass? @@ -72,63 +118,40 @@ def requires_compiled_superclass? end def inline_call? - @type == :inline_call - end - - def inline? - @type == :inline + type == :inline_call end def default_format? - @this_format == ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT + format.nil? || format == DEFAULT_FORMAT end + alias_method :html?, :default_format? - def format - @this_format + def call_method_name + @call_method_name ||= + ["call", (normalized_variant_name if variant.present?), (format unless default_format?)] + .compact.join("_").to_sym end def safe_method_name - "_#{@call_method_name}_#{@component.name.underscore.gsub("/", "__")}" + "_#{call_method_name}_#{@component.name.underscore.gsub("/", "__")}" end def normalized_variant_name - @variant.to_s.gsub("-", "__").gsub(".", "___") - end - - def defined_on_self? - @defined_on_self + variant.to_s.gsub("-", "__") end private - def source - if @source_originally_nil - # Load file each time we look up #source in case the file has been modified - File.read(@path) - else - @source - end - end - def compiled_source - handler = ActionView::Template.handler_for_extension(@extension) + handler = details.handler_class this_source = source this_source.rstrip! if @component.strip_trailing_whitespace? short_identifier = defined?(Rails.root) ? @path.sub("#{Rails.root}/", "") : @path - type = ActionView::Template::Types[@this_format] + format = self.format || DEFAULT_FORMAT + type = ActionView::Template::Types[format] - if handler.method(:call).parameters.length > 1 - handler.call( - DataWithSource.new(format: @this_format, identifier: @path, short_identifier: short_identifier, type: type), - this_source - ) - # :nocov: - # TODO: Remove in v4 - else - handler.call(DataNoSource.new(source: this_source, identifier: @path, type: type)) - end - # :nocov: + handler.call(DataWithSource.new(format:, identifier: @path, short_identifier:, type:), this_source) end end end diff --git a/lib/view_component/test_helpers.rb b/lib/view_component/test_helpers.rb index 3677763fb..60a08e0d7 100644 --- a/lib/view_component/test_helpers.rb +++ b/lib/view_component/test_helpers.rb @@ -18,18 +18,7 @@ def refute_component_rendered def assert_component_rendered assert_selector("body") end - rescue LoadError - # We don't have a test case for running an application without capybara installed. - # It's probably fine to leave this without coverage. - # :nocov: - if ENV["DEBUG"] - warn( - "WARNING in `ViewComponent::TestHelpers`: Add `capybara` " \ - "to Gemfile to use Capybara assertions." - ) - end - - # :nocov: + rescue LoadError # We don't have a test case for running an application without capybara installed. end # Returns the result of a render_inline call. @@ -46,21 +35,12 @@ def assert_component_rendered # ``` # # @param component [ViewComponent::Base, ViewComponent::Collection] The instance of the component to be rendered. - # @return [Nokogiri::HTML] + # @return [Nokogiri::HTML5] def render_inline(component, **args, &block) @page = nil - @rendered_content = - if Rails.version.to_f >= 6.1 - vc_test_controller.view_context.render(component, args, &block) - - # :nocov: - else - vc_test_controller.view_context.render_component(component, &block) - end + @rendered_content = vc_test_controller.view_context.render(component, args, &block) - # :nocov: - - Nokogiri::HTML.fragment(@rendered_content) + Nokogiri::HTML5.fragment(@rendered_content) end # `JSON.parse`-d component output. @@ -91,9 +71,9 @@ def rendered_json # @param name [String] The name of the preview to be rendered. # @param from [ViewComponent::Preview] The class of the preview to be rendered. # @param params [Hash] Parameters to be passed to the preview. - # @return [Nokogiri::HTML] + # @return [Nokogiri::HTML5] def render_preview(name, from: __vc_test_helpers_preview_class, params: {}) - previews_controller = __vc_test_helpers_build_controller(Rails.application.config.view_component.preview_controller.constantize) + previews_controller = __vc_test_helpers_build_controller(Rails.application.config.view_component.previews.controller.constantize) # From what I can tell, it's not possible to overwrite all request parameters # at once, so we set them individually here. @@ -107,7 +87,7 @@ def render_preview(name, from: __vc_test_helpers_preview_class, params: {}) @rendered_content = result - Nokogiri::HTML.fragment(@rendered_content) + Nokogiri::HTML5.fragment(@rendered_content) end # Execute the given block in the view context (using `instance_exec`). @@ -121,12 +101,11 @@ def render_preview(name, from: __vc_test_helpers_preview_class, params: {}) # # assert_text("Hello, World!") # ``` - def render_in_view_context(*args, &block) + def render_in_view_context(...) @page = nil - @rendered_content = vc_test_controller.view_context.instance_exec(*args, &block) - Nokogiri::HTML.fragment(@rendered_content) + @rendered_content = vc_test_controller.view_context.instance_exec(...) + Nokogiri::HTML5.fragment(@rendered_content) end - ruby2_keywords(:render_in_view_context) if respond_to?(:ruby2_keywords, true) # Set the Action Pack request variant for the given block: # @@ -136,11 +115,11 @@ def render_in_view_context(*args, &block) # end # ``` # - # @param variant [Symbol] The variant to be set for the provided block. - def with_variant(variant) + # @param variants [Symbol[]] The variants to be set for the provided block. + def with_variant(*variants) old_variants = vc_test_controller.view_context.lookup_context.variants - vc_test_controller.view_context.lookup_context.variants = variant + vc_test_controller.view_context.lookup_context.variants += variants yield ensure vc_test_controller.view_context.lookup_context.variants = old_variants @@ -173,9 +152,14 @@ def with_controller_class(klass) # end # ``` # - # @param format [Symbol] The format to be set for the provided block. - def with_format(format) - with_request_url("/", format: format) { yield } + # @param formats [Symbol[]] The format(s) to be set for the provided block. + def with_format(*formats) + old_formats = vc_test_controller.view_context.lookup_context.formats + + vc_test_controller.view_context.lookup_context.formats = formats + yield + ensure + vc_test_controller.view_context.lookup_context.formats = old_formats end # Set the URL of the current request (such as when using request-dependent path helpers): @@ -205,7 +189,7 @@ def with_format(format) # @param full_path [String] The path to set for the current request. # @param host [String] The host to set for the current request. # @param method [String] The request method to set for the current request. - def with_request_url(full_path, host: nil, method: nil, format: ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT) + def with_request_url(full_path, host: nil, method: nil) old_request_host = vc_test_request.host old_request_method = vc_test_request.request_method old_request_path_info = vc_test_request.path_info @@ -225,7 +209,6 @@ def with_request_url(full_path, host: nil, method: nil, format: ViewComponent::B vc_test_request.set_header("action_dispatch.request.query_parameters", Rack::Utils.parse_nested_query(query).with_indifferent_access) vc_test_request.set_header(Rack::QUERY_STRING, query) - vc_test_request.format = format yield ensure vc_test_request.host = old_request_host @@ -250,7 +233,20 @@ def with_request_url(full_path, host: nil, method: nil, format: ViewComponent::B # # @return [ActionController::Base] def vc_test_controller - @vc_test_controller ||= __vc_test_helpers_build_controller(Base.test_controller.constantize) + @vc_test_controller ||= __vc_test_helpers_build_controller(vc_test_controller_class) + end + + # Set the controller used by `render_inline`: + # + # ```ruby + # def vc_test_controller_class + # MyTestController + # end + # ``` + def vc_test_controller_class + return @__vc_test_controller_class if defined?(@__vc_test_controller_class) + + defined?(ApplicationController) ? ApplicationController : ActionController::Base end # Access the request used by `render_inline`: @@ -284,11 +280,9 @@ def __vc_test_helpers_build_controller(klass) def __vc_test_helpers_preview_class result = if respond_to?(:described_class) - # :nocov: - raise "`render_preview` expected a described_class, but it is nil." if described_class.nil? + raise ArgumentError.new("`render_preview` expected a described_class, but it is nil.") if described_class.nil? "#{described_class}Preview" - # :nocov: else self.class.name.gsub("Test", "Preview") end @@ -296,6 +290,5 @@ def __vc_test_helpers_preview_class rescue NameError raise NameError, "`render_preview` expected to find #{result}, but it does not exist." end - # :nocov: end end diff --git a/lib/view_component/translatable.rb b/lib/view_component/translatable.rb index 422218207..815a1325e 100644 --- a/lib/view_component/translatable.rb +++ b/lib/view_component/translatable.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "erb" -require "set" require "i18n" require "active_support/concern" @@ -10,19 +9,22 @@ module Translatable extend ActiveSupport::Concern HTML_SAFE_TRANSLATION_KEY = /(?:_|\b)html\z/ + private_constant :HTML_SAFE_TRANSLATION_KEY + TRANSLATION_EXTENSIONS = %w[yml yaml].freeze + private_constant :TRANSLATION_EXTENSIONS included do - class_attribute :i18n_backend, instance_writer: false, instance_predicate: false + class_attribute :__vc_i18n_backend, instance_writer: false, instance_predicate: false end class_methods do - def i18n_scope - @i18n_scope ||= virtual_path.sub(%r{^/}, "").gsub(%r{/_?}, ".") + def __vc_i18n_scope + @__vc_i18n_scope ||= virtual_path.sub(%r{^/}, "").gsub(%r{/_?}, ".") end - def build_i18n_backend - return if compiled? + def __vc_build_i18n_backend + return if __vc_compiled? # We need to load the translations files from the ancestors so a component # can inherit translations from its parent and is able to overwrite them. @@ -33,31 +35,31 @@ def build_i18n_backend end # In development it will become nil if the translations file is removed - self.i18n_backend = if translation_files.any? + self.__vc_i18n_backend = if translation_files.any? I18nBackend.new( - i18n_scope: i18n_scope, + scope: __vc_i18n_scope, load_paths: translation_files ) end end - def i18n_key(key, scope = nil) + def __vc_i18n_key(key, scope = nil) scope = scope.join(".") if scope.is_a? Array key = key&.to_s unless key.is_a?(String) key = "#{scope}.#{key}" if scope - key = "#{i18n_scope}#{key}" if key.start_with?(".") + key = "#{__vc_i18n_scope}#{key}" if key.start_with?(".") key end def translate(key = nil, **options) return key.map { |k| translate(k, **options) } if key.is_a?(Array) - ensure_compiled + __vc_ensure_compiled locale = options.delete(:locale) || ::I18n.locale - key = i18n_key(key, options.delete(:scope)) + key = __vc_i18n_key(key, options.delete(:scope)) - i18n_backend.translate(locale, key, options) + __vc_i18n_backend.translate(locale, key, options) end alias_method :t, :translate @@ -66,18 +68,18 @@ def translate(key = nil, **options) class I18nBackend < ::I18n::Backend::Simple EMPTY_HASH = {}.freeze - def initialize(i18n_scope:, load_paths:) - @i18n_scope = i18n_scope.split(".").map(&:to_sym) - @load_paths = load_paths + def initialize(scope:, load_paths:) + @__vc_i18n_scope = scope.split(".").map(&:to_sym) + @__vc_load_paths = load_paths end # Ensure the Simple backend won't load paths from ::I18n.load_path def load_translations - super(@load_paths) + super(@__vc_load_paths) end def scope_data(data) - @i18n_scope.reverse_each do |part| + @__vc_i18n_scope.reverse_each do |part| data = {part => data} end data @@ -91,44 +93,43 @@ def store_translations(locale, data, options = EMPTY_HASH) def translate(key = nil, **options) raise ViewComponent::TranslateCalledBeforeRenderError if view_context.nil? - return super unless i18n_backend + return @view_context.translate(key, **options) unless __vc_i18n_backend return key.map { |k| translate(k, **options) } if key.is_a?(Array) locale = options.delete(:locale) || ::I18n.locale - key = self.class.i18n_key(key, options.delete(:scope)) + key = self.class.__vc_i18n_key(key, options.delete(:scope)) as_html = HTML_SAFE_TRANSLATION_KEY.match?(key) - html_escape_translation_options!(options) if as_html + __vc_html_escape_translation_options!(options) if as_html - if key.start_with?(i18n_scope + ".") + if key.start_with?(__vc_i18n_scope + ".") translated = catch(:exception) do - i18n_backend.translate(locale, key, options) + __vc_i18n_backend.translate(locale, key, options) end # Fallback to the global translations if translated.is_a? ::I18n::MissingTranslation - return super(key, locale: locale, **options) + return @view_context.translate(key, locale: locale, **options) end - translated = html_safe_translation(translated) if as_html + translated = __vc_html_safe_translation(translated) if as_html translated else - super(key, locale: locale, **options) + @view_context.translate(key, locale: locale, **options) end end alias_method :t, :translate - # Exposes .i18n_scope as an instance method - def i18n_scope - self.class.i18n_scope + def __vc_i18n_scope + self.class.__vc_i18n_scope end private - def html_safe_translation(translation) + def __vc_html_safe_translation(translation) if translation.respond_to?(:map) - translation.map { |element| html_safe_translation(element) } + translation.map { |element| __vc_html_safe_translation(element) } else # It's assumed here that objects loaded by the i18n backend will respond to `#html_safe?`. # It's reasonable that if we're in Rails, `active_support/core_ext/string/output_safety.rb` @@ -137,7 +138,7 @@ def html_safe_translation(translation) end end - def html_escape_translation_options!(options) + def __vc_html_escape_translation_options!(options) options.except(*::I18n::RESERVED_KEYS).each do |name, value| next if name == :count && value.is_a?(Numeric) diff --git a/lib/view_component/use_helpers.rb b/lib/view_component/use_helpers.rb deleted file mode 100644 index 47830dd1e..000000000 --- a/lib/view_component/use_helpers.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -module ViewComponent::UseHelpers - extend ActiveSupport::Concern - - class_methods do - def use_helpers(*args, from: nil, prefix: false) - args.each { |helper_method| use_helper(helper_method, from: from, prefix: prefix) } - end - - def use_helper(helper_method, from: nil, prefix: false) - ViewComponent::Deprecation.deprecation_warning( - "use_helper(s)", - "Use `include MyHelper` or `helpers.` proxy instead." - ) - - helper_method_name = full_helper_method_name(helper_method, prefix: prefix, source: from) - - class_eval(<<-RUBY, __FILE__, __LINE__ + 1) - def #{helper_method_name}(*args, &block) - raise HelpersCalledBeforeRenderError if view_context.nil? - - #{define_helper(helper_method: helper_method, source: from)} - end - RUBY - ruby2_keywords(helper_method_name) if respond_to?(:ruby2_keywords, true) - end - - private - - def full_helper_method_name(helper_method, prefix: false, source: nil) - return helper_method unless prefix.present? - - if !!prefix == prefix - "#{source.to_s.underscore}_#{helper_method}" - else - "#{prefix}_#{helper_method}" - end - end - - def define_helper(helper_method:, source:) - return "__vc_original_view_context.#{helper_method}(*args, &block)" unless source.present? - - "#{source}.instance_method(:#{helper_method}).bind(self).call(*args, &block)" - end - end -end diff --git a/lib/view_component/version.rb b/lib/view_component/version.rb index bb8114fe6..891c3b34e 100644 --- a/lib/view_component/version.rb +++ b/lib/view_component/version.rb @@ -2,10 +2,10 @@ module ViewComponent module VERSION - MAJOR = 3 - MINOR = 23 - PATCH = 2 - PRE = nil + MAJOR = 4 + MINOR = 0 + PATCH = 0 + PRE = "rc1" STRING = [MAJOR, MINOR, PATCH, PRE].compact.join(".") end diff --git a/performance/partial_benchmark.rb b/performance/partial_benchmark.rb index 5de7014ee..d7d8e179c 100644 --- a/performance/partial_benchmark.rb +++ b/performance/partial_benchmark.rb @@ -9,6 +9,8 @@ ENV["RAILS_ENV"] = "production" require File.expand_path("../test/sandbox/config/environment.rb", __dir__) +Rails.logger.level = 1 + module Performance require_relative "components/name_component" require_relative "components/nested_name_component" diff --git a/performance/views/benchmarks/_partial.html.erb b/performance/views/benchmarks/_partial.html.erb index 8c4724648..6a642003b 100644 --- a/performance/views/benchmarks/_partial.html.erb +++ b/performance/views/benchmarks/_partial.html.erb @@ -1,5 +1,5 @@

hello <%= name %>

<% 50.times do %> - <%= render 'nested', name: name, nested: false %> + <%= render "nested", name: name, nested: false %> <% end %> diff --git a/script/replicate-bug b/script/replicate-bug index 0121081f4..1851b5cd1 100755 --- a/script/replicate-bug +++ b/script/replicate-bug @@ -12,7 +12,7 @@ bundle add view_component --git https://github.com/viewcomponent/view_component # Generate a controller rails g controller Home index # Generate ApplicationComponent, assuming most folks use it. -rails g component ApplicationComponent --template_engine=no_template +rails g view_component ApplicationComponent --template_engine=no_template # Root to the index action on HomeController cat << 'ROUTES' > 'config/routes.rb' Rails.application.routes.draw do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f9e4f3206..9d462e55a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,14 +4,6 @@ require "simplecov-console" require "rails/version" -if ENV["MEASURE_COVERAGE"] - SimpleCov.start do - command_name "RSpec-rails#{Rails::VERSION::STRING}-ruby#{RUBY_VERSION}" - - formatter SimpleCov::Formatter::Console - end -end - require "bundler/setup" # Configure Rails Environment @@ -23,6 +15,18 @@ require File.expand_path("../sandbox/config/environment.rb", __FILE__) require "rspec/rails" +require "capybara/cuprite" + +# Rails registers its own driver named "cuprite" which will overwrite the one we +# register here. Avoid the problem by registering the driver with a distinct name. +Capybara.register_driver(:system_test_driver) do |app| + # Add the process_timeout option to prevent failures due to the browser + # taking too long to start up. + Capybara::Cuprite::Driver.new(app, {process_timeout: 60, timeout: 30}) +end + RSpec.configure do |config| config.include ViewComponent::TestHelpers + config.include ViewComponent::SystemSpecHelpers, type: :feature + config.include ViewComponent::SystemSpecHelpers, type: :system end diff --git a/test/sandbox/app/components/asset_component.html.erb b/test/sandbox/app/components/asset_component.html.erb index 252f1e94d..3cd5a0790 100644 --- a/test/sandbox/app/components/asset_component.html.erb +++ b/test/sandbox/app/components/asset_component.html.erb @@ -1 +1,2 @@
<%= asset_url("application.css") %>
+
<%= request %>
diff --git a/test/sandbox/app/components/collection_parameter_with_active_model_component.rb b/test/sandbox/app/components/collection_parameter_with_active_model_component.rb index 20b180f34..664f8ea69 100644 --- a/test/sandbox/app/components/collection_parameter_with_active_model_component.rb +++ b/test/sandbox/app/components/collection_parameter_with_active_model_component.rb @@ -7,6 +7,9 @@ class CollectionParameterWithActiveModelComponent < ViewComponent::Base with_collection_parameter :name + def initialize(name:) + end + def call end end diff --git a/test/sandbox/app/components/composable_slots_component.rb b/test/sandbox/app/components/composable_slots_component.rb deleted file mode 100644 index c738d0693..000000000 --- a/test/sandbox/app/components/composable_slots_component.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -class ComposableSlotsComponent < ViewComponent::Base - delegate :title, to: :@parent - - def initialize - @parent = SlotsWithoutContentBlockComponent.new - end - - def call - content_tag :div, class: "composable-slot-component" do - capture do - render @parent - end - end - end -end diff --git a/test/sandbox/app/components/conditional_content_with_arg_component.rb b/test/sandbox/app/components/conditional_content_with_arg_component.rb new file mode 100644 index 000000000..cf6aa000f --- /dev/null +++ b/test/sandbox/app/components/conditional_content_with_arg_component.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class ConditionalContentWithArgComponent < ViewComponent::Base + def initialize(description: nil) + @description = description + end + + def call + description&.html_safe + end + + private + + def description + @description || content + end +end diff --git a/test/sandbox/app/components/container_component.rb b/test/sandbox/app/components/container_component.rb index 471502918..f06f9428d 100644 --- a/test/sandbox/app/components/container_component.rb +++ b/test/sandbox/app/components/container_component.rb @@ -2,10 +2,6 @@ class ContainerComponent < ViewComponent::Base def call - if Rails.application.config.view_component.render_monkey_patch_enabled || Rails.version.to_f >= 6.1 - render HelpersProxyComponent.new - else - render_component HelpersProxyComponent.new - end + render HelpersProxyComponent.new end end diff --git a/test/sandbox/app/components/content_tag_component.html.erb b/test/sandbox/app/components/content_tag_component.html.erb index 6c6cc11da..c8b677c80 100644 --- a/test/sandbox/app/components/content_tag_component.html.erb +++ b/test/sandbox/app/components/content_tag_component.html.erb @@ -1,3 +1,3 @@ -<%= helpers.my_helper_method({ foo: :bar }) do %> +<%= helpers.my_helper_method({foo: :bar}) do %>

Content inside helper method

<% end %> diff --git a/test/sandbox/app/components/controller_inline_component.html.erb b/test/sandbox/app/components/controller_inline_component.html.erb index 483b2338e..38ead886a 100644 --- a/test/sandbox/app/components/controller_inline_component.html.erb +++ b/test/sandbox/app/components/controller_inline_component.html.erb @@ -1 +1 @@ -<%= render partial: "integration_examples/controller_inline", locals: { message: @message } %> +<%= render partial: "integration_examples/controller_inline", locals: {message: @message} %> diff --git a/test/sandbox/app/components/controller_inline_with_block_component.html.erb b/test/sandbox/app/components/controller_inline_with_block_component.html.erb index 13e2f2ba8..2994c6510 100644 --- a/test/sandbox/app/components/controller_inline_with_block_component.html.erb +++ b/test/sandbox/app/components/controller_inline_with_block_component.html.erb @@ -1,4 +1,4 @@ -<%= render partial: "integration_examples/controller_inline", locals: { message: @message } %> +<%= render partial: "integration_examples/controller_inline", locals: {message: @message} %> <%= slot %> diff --git a/test/sandbox/app/components/current_template_component.html.erb b/test/sandbox/app/components/current_template_component.html.erb new file mode 100644 index 000000000..10d4b4e1a --- /dev/null +++ b/test/sandbox/app/components/current_template_component.html.erb @@ -0,0 +1 @@ +<%= current_template.path %> diff --git a/test/sandbox/app/components/current_template_component.rb b/test/sandbox/app/components/current_template_component.rb new file mode 100644 index 000000000..4978dafd3 --- /dev/null +++ b/test/sandbox/app/components/current_template_component.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class CurrentTemplateComponent < ViewComponent::Base +end diff --git a/test/sandbox/app/components/editorb_component.html.erb b/test/sandbox/app/components/editorb_component.html.erb index 992e9a6b7..e185fc9d4 100644 --- a/test/sandbox/app/components/editorb_component.html.erb +++ b/test/sandbox/app/components/editorb_component.html.erb @@ -1 +1 @@ -<%= t('.title') %> +<%= t(".title") %> diff --git a/test/sandbox/app/components/example_module/translatable_module_component.html.erb b/test/sandbox/app/components/example_module/translatable_module_component.html.erb new file mode 100644 index 000000000..02b45438c --- /dev/null +++ b/test/sandbox/app/components/example_module/translatable_module_component.html.erb @@ -0,0 +1,10 @@ +

<%= t(".hello") %>

+

<%= t(".from.sidecar") %>

+

<%= t("from.rails") %>

+ +

<%= ::I18n.t("hello") %>

+

<%= ::I18n.t("from.rails") %>

+ +

<%= helpers.t("hello") %>

+

<%= helpers.t("from.rails") %>

+

<%= helpers.t(".relative_rails_key") %>

diff --git a/test/sandbox/app/components/example_module/translatable_module_component.rb b/test/sandbox/app/components/example_module/translatable_module_component.rb new file mode 100644 index 000000000..bcf9e2598 --- /dev/null +++ b/test/sandbox/app/components/example_module/translatable_module_component.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ExampleModule + class TranslatableModuleComponent < ViewComponent::Base + end +end diff --git a/test/sandbox/app/components/example_module/translatable_module_component.yml b/test/sandbox/app/components/example_module/translatable_module_component.yml new file mode 100644 index 000000000..cc741b517 --- /dev/null +++ b/test/sandbox/app/components/example_module/translatable_module_component.yml @@ -0,0 +1,21 @@ +en: + hello: "Hello from sidecar translations!" + + hello_html: "Hello from sidecar translations!" + + interpolated_html: "There are %{horse_count} horses in the barn!" + + html: "hello world!" + + from: + sidecar: This is coming from the sidecar + + list: + - This + - returns + - a list + + list_html: + - This + - returns + - a list with embedded HTML diff --git a/test/sandbox/app/components/monkey_patch_disabled_component.html.erb b/test/sandbox/app/components/initialize_super_component.html.erb similarity index 100% rename from test/sandbox/app/components/monkey_patch_disabled_component.html.erb rename to test/sandbox/app/components/initialize_super_component.html.erb diff --git a/test/sandbox/app/components/initialize_super_component.rb b/test/sandbox/app/components/initialize_super_component.rb new file mode 100644 index 000000000..5d5a9258c --- /dev/null +++ b/test/sandbox/app/components/initialize_super_component.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class InitializeSuperComponent < ViewComponent::Base + def initialize + super + end +end diff --git a/test/sandbox/app/components/inline_sidecar_conflict_component.html.erb b/test/sandbox/app/components/inline_sidecar_conflict_component.html.erb new file mode 100644 index 000000000..af5626b4a --- /dev/null +++ b/test/sandbox/app/components/inline_sidecar_conflict_component.html.erb @@ -0,0 +1 @@ +Hello, world! diff --git a/test/sandbox/app/components/inline_sidecar_conflict_component.rb b/test/sandbox/app/components/inline_sidecar_conflict_component.rb new file mode 100644 index 000000000..f7ab1253f --- /dev/null +++ b/test/sandbox/app/components/inline_sidecar_conflict_component.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class InlineSidecarConflictComponent < ViewComponent::Base + def call + "Hi!" + end +end diff --git a/test/sandbox/app/components/item_component.rb b/test/sandbox/app/components/item_component.rb new file mode 100644 index 000000000..6794ea7c9 --- /dev/null +++ b/test/sandbox/app/components/item_component.rb @@ -0,0 +1,11 @@ +require "dry-initializer" + +class ItemComponent < ViewComponent::Base + extend Dry::Initializer + + option :item + + erb_template <<~ERB + <%= item.name %> + ERB +end diff --git a/test/sandbox/app/components/last_item_component.rb b/test/sandbox/app/components/last_item_component.rb new file mode 100644 index 000000000..0995cbca7 --- /dev/null +++ b/test/sandbox/app/components/last_item_component.rb @@ -0,0 +1,24 @@ +class LastItemComponent < ViewComponent::Base + renders_many :items, "BreadcrumbItemComponent" + + def call + tag.ul do + items.last.active = true + safe_join(items) + end + end + + class BreadcrumbItemComponent < ViewComponent::Base + attr_writer :active + + def initialize(item) + @item = item + end + + def call + html_class = +"breadcrumb" + html_class << " active" if @active + tag.li(@item, class: html_class) + end + end +end diff --git a/test/sandbox/app/components/missing_collection_parameter_name_component.rb b/test/sandbox/app/components/missing_collection_parameter_name_component.rb index f69aec869..bc39741df 100644 --- a/test/sandbox/app/components/missing_collection_parameter_name_component.rb +++ b/test/sandbox/app/components/missing_collection_parameter_name_component.rb @@ -3,8 +3,6 @@ class MissingCollectionParameterNameComponent < ViewComponent::Base with_collection_parameter :foo - # rubocop:disable Style/RedundantInitialize def initialize(bar:) end - # rubocop:enable Style/RedundantInitialize end diff --git a/test/sandbox/app/components/monkey_patch_disabled_component.rb b/test/sandbox/app/components/monkey_patch_disabled_component.rb deleted file mode 100644 index f01f73f31..000000000 --- a/test/sandbox/app/components/monkey_patch_disabled_component.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -class MonkeyPatchDisabledComponent < ViewComponent::Base -end diff --git a/test/sandbox/app/components/nested_shared_state/table_component.html.erb b/test/sandbox/app/components/nested_shared_state/table_component.html.erb index 727bccd0c..9fa9fd10c 100644 --- a/test/sandbox/app/components/nested_shared_state/table_component.html.erb +++ b/test/sandbox/app/components/nested_shared_state/table_component.html.erb @@ -1,3 +1,3 @@ -<%= content_tag(:div, class: 'table') do %> +<%= content_tag(:div, class: "table") do %> <%= header %> <% end %> diff --git a/test/sandbox/app/components/only_variant_component.html+phone.erb b/test/sandbox/app/components/only_variant_component.html+phone.erb new file mode 100644 index 000000000..a766c50a5 --- /dev/null +++ b/test/sandbox/app/components/only_variant_component.html+phone.erb @@ -0,0 +1 @@ +Phone diff --git a/test/sandbox/app/components/only_variant_component.rb b/test/sandbox/app/components/only_variant_component.rb new file mode 100644 index 000000000..5a3fe8fd5 --- /dev/null +++ b/test/sandbox/app/components/only_variant_component.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class OnlyVariantComponent < ViewComponent::Base +end diff --git a/test/sandbox/app/components/product_reader_oops_component.html.erb b/test/sandbox/app/components/product_reader_oops_component.html.erb deleted file mode 100644 index 1038ae1b1..000000000 --- a/test/sandbox/app/components/product_reader_oops_component.html.erb +++ /dev/null @@ -1 +0,0 @@ -
Hello, World!
diff --git a/test/sandbox/app/components/product_reader_oops_component.rb b/test/sandbox/app/components/product_reader_oops_component.rb deleted file mode 100644 index ad3361102..000000000 --- a/test/sandbox/app/components/product_reader_oops_component.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -# This code intentionally has a bug where there is an extra comma after the -# :notice reader. `Kernel.silence_warnings` is in place to avoid Ruby emitting -# warnings in test output. - -# rubocop:disable all -Kernel.silence_warnings do - class ProductReaderOopsComponent < ViewComponent::Base - attr_reader :product, :notice, - - def initialize(product_reader_oops:) - end - end -end -# rubocop:enable all diff --git a/test/sandbox/app/components/renders_non_component.html.erb b/test/sandbox/app/components/renders_non_component.html.erb deleted file mode 100644 index a5a7b1a57..000000000 --- a/test/sandbox/app/components/renders_non_component.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render(@not_a_component) %> diff --git a/test/sandbox/app/components/renders_non_component.rb b/test/sandbox/app/components/renders_non_component.rb deleted file mode 100644 index 639d1b5af..000000000 --- a/test/sandbox/app/components/renders_non_component.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -class RendersNonComponent < ViewComponent::Base - class NotAComponent - attr_reader :render_in_view_context, :original_view_context - - def render_in(view_context) - @render_in_view_context = view_context - "I'm not a component".html_safe - end - - def set_original_view_context(view_context) - @original_view_context = view_context - end - end - - def initialize(not_a_component:) - @not_a_component = not_a_component - end -end diff --git a/test/sandbox/app/components/request_param_component.rb b/test/sandbox/app/components/request_param_component.rb index b1601f0d4..d40ad7297 100644 --- a/test/sandbox/app/components/request_param_component.rb +++ b/test/sandbox/app/components/request_param_component.rb @@ -4,6 +4,6 @@ def initialize(request:) end def call - @request + @request.html_safe end end diff --git a/test/sandbox/app/components/slotable_default_component.rb b/test/sandbox/app/components/slotable_default_component.rb index 83d066fac..c32660d91 100644 --- a/test/sandbox/app/components/slotable_default_component.rb +++ b/test/sandbox/app/components/slotable_default_component.rb @@ -1,6 +1,4 @@ class SlotableDefaultComponent < ViewComponent::Base - include ViewComponent::SlotableDefault - erb_template <<~ERB

<%= header %>

ERB diff --git a/test/sandbox/app/components/slotable_default_instance_component.rb b/test/sandbox/app/components/slotable_default_instance_component.rb index 291c54fe6..012016eec 100644 --- a/test/sandbox/app/components/slotable_default_instance_component.rb +++ b/test/sandbox/app/components/slotable_default_instance_component.rb @@ -1,6 +1,4 @@ class SlotableDefaultInstanceComponent < ViewComponent::Base - include ViewComponent::SlotableDefault - erb_template <<~ERB

<%= header %>

ERB diff --git a/test/sandbox/app/components/translations_component.html.erb b/test/sandbox/app/components/translations_component.html.erb index 728d17b2b..734c17caa 100644 --- a/test/sandbox/app/components/translations_component.html.erb +++ b/test/sandbox/app/components/translations_component.html.erb @@ -1,4 +1,4 @@
-

<%= t('.title') %>

-

<%= t('translations_component.subtitle') %>

+

<%= t(".title") %>

+

<%= t("translations_component.subtitle") %>

diff --git a/test/sandbox/app/components/turbo_content_type_component.html.erb b/test/sandbox/app/components/turbo_content_type_component.html.erb new file mode 100644 index 000000000..966333c79 --- /dev/null +++ b/test/sandbox/app/components/turbo_content_type_component.html.erb @@ -0,0 +1,13 @@ +<%= content_tag("turbo-frame", id: "test-frame") do %> + <% if show_form %> + <%= form_tag("/submit", method: :post, data: {turbo_frame: "test-frame"}) do %> +
+

<%= @message %>

+ <%= text_field_tag("user_message", "", value: "Test message", autocomplete: "off") %> + <%= submit_tag("Submit", id: "submit") %> +
+ <% end %> + <% else %> + <%= content_tag(:div, @message, id: "result") %> + <% end %> +<% end %> diff --git a/test/sandbox/app/components/turbo_content_type_component.rb b/test/sandbox/app/components/turbo_content_type_component.rb new file mode 100644 index 000000000..31bc09473 --- /dev/null +++ b/test/sandbox/app/components/turbo_content_type_component.rb @@ -0,0 +1,10 @@ +class TurboContentTypeComponent < ViewComponent::Base + def initialize(message: "Enter a message:", show_form: true) + @message = message + @show_form = show_form + end + + private + + attr_reader :message, :show_form +end diff --git a/test/sandbox/app/components/turbo_stream_component.html.erb b/test/sandbox/app/components/turbo_stream_component.html.erb index bfcb1ae0e..c1ffb202f 100644 --- a/test/sandbox/app/components/turbo_stream_component.html.erb +++ b/test/sandbox/app/components/turbo_stream_component.html.erb @@ -1 +1 @@ -<%= helpers.turbo_stream.update 'area1' do %>Hello, world!<% end %> +<%= turbo_stream.update "area1" do %>Hello, world!<% end %> diff --git a/test/sandbox/app/components/turbo_stream_component.rb b/test/sandbox/app/components/turbo_stream_component.rb index c284b3dcf..66424cdf4 100644 --- a/test/sandbox/app/components/turbo_stream_component.rb +++ b/test/sandbox/app/components/turbo_stream_component.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true class TurboStreamComponent < ViewComponent::Base + include Turbo::StreamsHelper end diff --git a/test/sandbox/app/components/turbo_stream_format_component.html+custom.erb b/test/sandbox/app/components/turbo_stream_format_component.html+custom.erb new file mode 100644 index 000000000..2a019d410 --- /dev/null +++ b/test/sandbox/app/components/turbo_stream_format_component.html+custom.erb @@ -0,0 +1 @@ +Hi turbo stream custom! diff --git a/test/sandbox/app/components/turbo_stream_format_component.html.erb b/test/sandbox/app/components/turbo_stream_format_component.html.erb new file mode 100644 index 000000000..f6e0d0b4f --- /dev/null +++ b/test/sandbox/app/components/turbo_stream_format_component.html.erb @@ -0,0 +1 @@ +Hi turbo stream! diff --git a/test/sandbox/app/components/turbo_stream_format_component.rb b/test/sandbox/app/components/turbo_stream_format_component.rb new file mode 100644 index 000000000..ea4a3d0fc --- /dev/null +++ b/test/sandbox/app/components/turbo_stream_format_component.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class TurboStreamFormatComponent < ViewComponent::Base +end diff --git a/test/sandbox/app/components/unreferenced_component.html.erb b/test/sandbox/app/components/unreferenced_component.html.erb deleted file mode 100644 index 1038ae1b1..000000000 --- a/test/sandbox/app/components/unreferenced_component.html.erb +++ /dev/null @@ -1 +0,0 @@ -
Hello, World!
diff --git a/test/sandbox/app/components/unreferenced_component.rb b/test/sandbox/app/components/unreferenced_component.rb deleted file mode 100644 index 7eb74474d..000000000 --- a/test/sandbox/app/components/unreferenced_component.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -class UnreferencedComponent < ViewComponent::Base -end diff --git a/test/sandbox/app/components/url_for_component.html.erb b/test/sandbox/app/components/url_for_component.html.erb index fa294eb1a..7022a10ec 100644 --- a/test/sandbox/app/components/url_for_component.html.erb +++ b/test/sandbox/app/components/url_for_component.html.erb @@ -1 +1 @@ -<%= url_for(only_path: @only_path, params: { key: "value" }.merge(request.query_parameters)) %> +<%= url_for(only_path: @only_path, params: {key: "value"}.merge(request.query_parameters)) %> diff --git a/test/sandbox/app/components/url_for_mailer_component.html.erb b/test/sandbox/app/components/url_for_mailer_component.html.erb new file mode 100644 index 000000000..84c14ebd4 --- /dev/null +++ b/test/sandbox/app/components/url_for_mailer_component.html.erb @@ -0,0 +1 @@ +<%= helpers.url_for(Post.new) %> diff --git a/test/sandbox/app/components/url_for_mailer_component.rb b/test/sandbox/app/components/url_for_mailer_component.rb new file mode 100644 index 000000000..39e45fb39 --- /dev/null +++ b/test/sandbox/app/components/url_for_mailer_component.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class UrlForMailerComponent < ViewComponent::Base +end diff --git a/test/sandbox/app/components/use_helper_macro_component.html.erb b/test/sandbox/app/components/use_helper_macro_component.html.erb deleted file mode 100644 index 8cd3170c9..000000000 --- a/test/sandbox/app/components/use_helper_macro_component.html.erb +++ /dev/null @@ -1,23 +0,0 @@ -
- <%= message %> -
- -
- <%= message_with_args('macro helper method') %> -
- -
- <%= message_with_kwargs(name: 'macro kwargs helper method') %> -
- -
- <%= macro_helper_message_with_prefix('macro prefix helper method') %> -
- -
- <%= named_message_with_named_prefix('macro named prefix helper method') %> -
- -
- <%= block_content %> -
diff --git a/test/sandbox/app/components/use_helper_macro_component.rb b/test/sandbox/app/components/use_helper_macro_component.rb deleted file mode 100644 index f697e8540..000000000 --- a/test/sandbox/app/components/use_helper_macro_component.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -class UseHelperMacroComponent < ViewComponent::Base - use_helper :message, from: MacroHelper - use_helper :message_with_args, from: MacroHelper - use_helper :message_with_kwargs, from: MacroHelper - use_helper :message_with_prefix, from: MacroHelper, prefix: true - use_helper :message_with_block, from: MacroHelper - use_helper :message_with_named_prefix, from: MacroHelper, prefix: :named - - def block_content - message_with_block { "Hello block helper method" } - end -end diff --git a/test/sandbox/app/components/use_helpers_component.html.erb b/test/sandbox/app/components/use_helpers_component.html.erb deleted file mode 100644 index f24542007..000000000 --- a/test/sandbox/app/components/use_helpers_component.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -
- <%= message %> -
diff --git a/test/sandbox/app/components/use_helpers_component.rb b/test/sandbox/app/components/use_helpers_component.rb deleted file mode 100644 index 752e83f0c..000000000 --- a/test/sandbox/app/components/use_helpers_component.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -class UseHelpersComponent < ViewComponent::Base - use_helper :message -end diff --git a/test/sandbox/app/components/use_helpers_macro_component.html.erb b/test/sandbox/app/components/use_helpers_macro_component.html.erb deleted file mode 100644 index 6598497fd..000000000 --- a/test/sandbox/app/components/use_helpers_macro_component.html.erb +++ /dev/null @@ -1,23 +0,0 @@ -
- <%= message %> -
- -
- <%= message_with_args('macro helper method') %> -
- -
- <%= message_with_kwargs(name: 'macro kwargs helper method') %> -
- -
- <%= block_content %> -
- -
- <%= macro_helper_message_with_args('macro prefix helper method') %> -
- -
- <%= named_message_with_args('macro named prefix helper method') %> -
diff --git a/test/sandbox/app/components/use_helpers_macro_component.rb b/test/sandbox/app/components/use_helpers_macro_component.rb deleted file mode 100644 index fdbe12b06..000000000 --- a/test/sandbox/app/components/use_helpers_macro_component.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -class UseHelpersMacroComponent < ViewComponent::Base - use_helpers :message, :message_with_args, :message_with_kwargs, :message_with_block, from: MacroHelper - - use_helpers :message_with_args, from: MacroHelper, prefix: true - - use_helpers :message_with_args, from: MacroHelper, prefix: :named - - def block_content - message_with_block { "Hello block helper method" } - end -end diff --git a/test/sandbox/app/components/variants_component.html+mini.watch.erb b/test/sandbox/app/components/variants_component.html+mini.watch.erb deleted file mode 100644 index b44d8505e..000000000 --- a/test/sandbox/app/components/variants_component.html+mini.watch.erb +++ /dev/null @@ -1 +0,0 @@ -Mini Watch with dot diff --git a/test/sandbox/app/controllers/integration_examples_controller.rb b/test/sandbox/app/controllers/integration_examples_controller.rb index 54c7c4068..686328846 100644 --- a/test/sandbox/app/controllers/integration_examples_controller.rb +++ b/test/sandbox/app/controllers/integration_examples_controller.rb @@ -23,23 +23,19 @@ def controller_inline_baseline end def controller_to_string - # Ensures render_to_string_monkey_patch.rb correctly calls `super` when - # not rendering a component: - render_to_string("integration_examples/_controller_inline", locals: {message: "bar"}) - render(plain: render_to_string(ControllerInlineComponent.new(message: "bar"))) end - def controller_inline_render_component - render_component(ControllerInlineComponent.new(message: "bar")) + def controller_inline_baseline_with_layout + render(ControllerInlineComponent.new(message: "bar"), layout: "application") end - def helpers_proxy_component - render(plain: render_to_string(HelpersProxyComponent.new)) + def controller_to_string_with_layout + render(plain: render_to_string(ControllerInlineComponent.new(message: "bar"), layout: "application")) end - def controller_to_string_render_component - render(plain: render_component_to_string(ControllerInlineComponent.new(message: "bar"))) + def helpers_proxy_component + render(plain: render_to_string(HelpersProxyComponent.new)) end def products @@ -79,4 +75,11 @@ def multiple_formats_component def turbo_stream respond_to { |format| format.turbo_stream { render TurboStreamComponent.new } } end + + def submit + render TurboContentTypeComponent.new( + message: "Submitted", + show_form: false + ) + end end diff --git a/test/sandbox/app/helpers/macro_helper.rb b/test/sandbox/app/helpers/macro_helper.rb deleted file mode 100644 index 1b308a080..000000000 --- a/test/sandbox/app/helpers/macro_helper.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module MacroHelper - def message - "Hello helper method" - end - - def message_with_args(name) - "Hello #{name}" - end - - def message_with_kwargs(name:) - "Hello #{name}" - end - - def message_with_prefix(name) - "Hello #{name}" - end - - def message_with_named_prefix(name) - "Hello #{name}" - end - - def message_with_block - yield - end -end diff --git a/test/sandbox/app/mailers/test_mailer.rb b/test/sandbox/app/mailers/test_mailer.rb index 1446646d8..eccaf1fa7 100644 --- a/test/sandbox/app/mailers/test_mailer.rb +++ b/test/sandbox/app/mailers/test_mailer.rb @@ -8,4 +8,20 @@ def test_email subject: "Testing ViewComponent in ActionMailer" ) end + + def test_asset_email + mail( + from: "no-reply@example.com", + to: "test@example.com", + subject: "Testing ViewComponent with Assets in ActionMailer" + ) + end + + def test_url_email + mail( + from: "no-reply@example.com", + to: "test@example.com", + subject: "Testing ViewComponent with url_for in ActionMailer" + ) + end end diff --git a/test/sandbox/app/views/integration_examples/cached_capture.html.erb b/test/sandbox/app/views/integration_examples/cached_capture.html.erb index 11931f9aa..e54b71f10 100644 --- a/test/sandbox/app/views/integration_examples/cached_capture.html.erb +++ b/test/sandbox/app/views/integration_examples/cached_capture.html.erb @@ -1,5 +1,5 @@ <% cache ["true"] do %> - <%= render CapturingComponent.new(message: "message") do %> + <%= render CapturingComponent.new do %>
Hello
<% end %> <% end %> diff --git a/test/sandbox/app/views/integration_examples/render_component.html.erb b/test/sandbox/app/views/integration_examples/render_component.html.erb deleted file mode 100644 index 29a63eab4..000000000 --- a/test/sandbox/app/views/integration_examples/render_component.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render_component ErbComponent.new(message: "bar") %> diff --git a/test/sandbox/app/views/integration_examples/slotable_default_override.html.erb b/test/sandbox/app/views/integration_examples/slotable_default_override.html.erb new file mode 100644 index 000000000..1090adde2 --- /dev/null +++ b/test/sandbox/app/views/integration_examples/slotable_default_override.html.erb @@ -0,0 +1,5 @@ +<%= render(SlotableDefaultComponent.new) do |c| %> + <% c.with_header do %> + foo + <% end %> +<% end %> diff --git a/test/sandbox/app/views/integration_examples/turbo_content_type.html.erb b/test/sandbox/app/views/integration_examples/turbo_content_type.html.erb new file mode 100644 index 000000000..2f696640e --- /dev/null +++ b/test/sandbox/app/views/integration_examples/turbo_content_type.html.erb @@ -0,0 +1,20 @@ + + + + Turbo + ViewComponent Content-Type Issue Demo + + + + +

Turbo + ViewComponent Content-Type Issue Demo

+

Check Content-Type header when form is submitted. Open developer tools (Network tab) before submitting.

+ + <%= render TurboContentTypeComponent.new %> + + diff --git a/test/sandbox/app/views/integration_examples/virtual_path_reset.html.erb b/test/sandbox/app/views/integration_examples/virtual_path_reset.html.erb new file mode 100644 index 000000000..af0df65f8 --- /dev/null +++ b/test/sandbox/app/views/integration_examples/virtual_path_reset.html.erb @@ -0,0 +1,7 @@ +

Virtual path: <%= @virtual_path %>

+

<%= t(".message") %>

+ +<%= render MyComponent.new %> + +

Virtual path: <%= @virtual_path %>

+

<%= t(".message") %>

diff --git a/test/sandbox/app/views/layouts/admin.html.erb b/test/sandbox/app/views/layouts/admin.html.erb index e0d2c524f..1892c1937 100644 --- a/test/sandbox/app/views/layouts/admin.html.erb +++ b/test/sandbox/app/views/layouts/admin.html.erb @@ -2,7 +2,7 @@ ViewComponent - Admin - Test - <%= stylesheet_link_tag 'admin' %> + <%= stylesheet_link_tag "admin" %> <%= yield %> diff --git a/test/sandbox/app/views/layouts/admin/application_Foobar.html.erb b/test/sandbox/app/views/layouts/admin/application_Foobar.html.erb index e0d2c524f..1892c1937 100644 --- a/test/sandbox/app/views/layouts/admin/application_Foobar.html.erb +++ b/test/sandbox/app/views/layouts/admin/application_Foobar.html.erb @@ -2,7 +2,7 @@ ViewComponent - Admin - Test - <%= stylesheet_link_tag 'admin' %> + <%= stylesheet_link_tag "admin" %> <%= yield %> diff --git a/test/sandbox/app/views/layouts/application.html.erb b/test/sandbox/app/views/layouts/application.html.erb index 0a9d7c1c6..7695bb9d7 100644 --- a/test/sandbox/app/views/layouts/application.html.erb +++ b/test/sandbox/app/views/layouts/application.html.erb @@ -2,16 +2,16 @@ ViewComponent - Test - <%= stylesheet_link_tag 'application' %> + <%= stylesheet_link_tag "application" %> <%= yield %>