Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ nav_order: 6

## main

* Fix fragment cache collisions after `render_parent`, missing relative-partial and inherited-template dependencies, format fallback cache collisions, and stale cached output after development template edits.

*Joel Hawksley*

* Fix template method collisions between formats and variants, preserve default slot state, and copy collection spacer slots independently.

*Joel Hawksley*

* Apply `--skip-suffix` to all generated component files and restore full request URLs after `with_request_url` blocks.

*Joel Hawksley*

* Reset the assertion page when rendering a preview so assertions inspect the latest output.

*Joel Hawksley*
Expand Down
4 changes: 3 additions & 1 deletion docs/guide/caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class PostComponent < ViewComponent::Base
end
```

The cache key also includes the component's digest, format, ordered variants, and locale. As with Rails' `cache` helper, caching only happens when `perform_caching` is enabled.
The cache key also includes the component's digest, ordered format and variant fallbacks, and locale. As with Rails' `cache` helper, caching only happens when `perform_caching` is enabled.

To cache only some renders, pass `if:` or `unless:` with a method name or a proc:

Expand All @@ -61,6 +61,8 @@ end

Dependencies are found by scanning the component's templates, its Ruby class, and the modules it includes. Every component class named there counts, whether it's passed to `render`, built by a method, or assigned to a variable, and so does every partial rendered by its path. Slots count the class they're declared with, including one named by a string.

Relative partial names, such as `render "heading"`, track matching partials in every directory of the view paths, since the calling controller determines which one renders. Use a full path to limit invalidation to one partial.

A class or partial name built at runtime can't be found that way. Declare it with Rails' `# Template Dependency:` comment in the Ruby file or template, naming partials by path and components by class:

```ruby
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,4 @@ Set `:spacer_component` as an instantiated component to render between items:
<%= render(ProductComponent.with_collection(@products, spacer_component: SpacerComponent.new)) %>
```

Which will render the SpacerComponent component between `ProductComponent`s.
The spacer renders between `ProductComponent` instances. Pass an unrendered spacer instance. Each collection render uses an independent copy of the spacer and its slots.
10 changes: 10 additions & 0 deletions docs/guide/generators.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,13 @@ To always use a specific parent class, set `config.view_component.generate.paren
### Skip collision check

The generator prevents naming collisions with existing components. To skip this check and force the generator to run, use the `--skip-collision-check` or `--force` option.

### Skip the component suffix

Use `--skip-suffix` to generate `Example` instead of `ExampleComponent`:

```console
bin/rails generate view_component:component Example title --skip-suffix
```

The template, sidecar directory, locale files, Stimulus controller, tests, and previews use the same unsuffixed name.
2 changes: 2 additions & 0 deletions docs/guide/slots.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,3 +378,5 @@ class SlotableDefaultInstanceComponent < ViewComponent::Base
end
end
```

Default slots retain their content and configuration between accesses within a component render. Like explicitly supplied slots, their rendered output is memoized.
10 changes: 9 additions & 1 deletion lib/generators/view_component/abstract_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

module ViewComponent
module AbstractGenerator
def self.included(generator)
generator.class_option :skip_suffix, type: :boolean, default: false
end

def copy_view_file
template("component.html.#{engine_name}", destination) unless options["inline"] || options["call"]
end
Expand All @@ -21,7 +25,11 @@ def destination_directory
end

def destination_file_name
"#{file_name}_component"
"#{file_name}#{"_component" unless options[:skip_suffix]}"
end

def component_class_name
"#{class_name}#{"Component" unless options[:skip_suffix]}"
end

def file_name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ class ComponentGenerator < Rails::Generators::NamedBase
source_root File.expand_path("templates", __dir__)

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
Expand All @@ -20,10 +19,13 @@ class ComponentGenerator < Rails::Generators::NamedBase
class_option :sidecar, type: :boolean, default: false
class_option :stimulus, type: :boolean,
default: ViewComponent::Base.config.generate.stimulus_controller
class_option :skip_suffix, type: :boolean, default: false

def check_class_collision
class_collisions component_class_name
end

def create_component_file
template "component.rb", File.join(component_path, class_path, "#{file_name}#{"_component" unless options[:skip_suffix]}.rb")
template "component.rb", File.join(component_path, class_path, "#{destination_file_name}.rb")
end

hook_for :test_framework
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

<% module_namespacing do -%>
class <%= class_name %><%= options[:skip_suffix] ? "" : "Component" %> < <%= parent_class %>
class <%= component_class_name %> < <%= parent_class %>
<%- if inline_template? -%>
<%= template_engine %>_template <<~<%= template_engine.upcase %>
<h1>Hello, World!</h1>
Expand Down
6 changes: 1 addition & 5 deletions lib/generators/view_component/locale/locale_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,7 @@ def translation_keys

def destination(locale = nil)
extension = ".#{locale}" if locale
if sidecar?
File.join(component_path, class_path, "#{file_name}_component", "#{file_name}_component#{extension}.yml")
else
File.join(component_path, class_path, "#{file_name}_component#{extension}.yml")
end
File.join(destination_directory, "#{destination_file_name}#{extension}.yml")
end
end
end
Expand Down
15 changes: 9 additions & 6 deletions lib/generators/view_component/preview/preview_generator.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
# frozen_string_literal: true

require "generators/view_component/abstract_generator"

module ViewComponent
module Generators
class PreviewGenerator < ::Rails::Generators::NamedBase
include ViewComponent::AbstractGenerator

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

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

def check_class_collision
class_collisions "#{component_class_name}Preview"
end

def create_preview_file
preview_paths = ViewComponent::Base.config.previews.paths
Expand All @@ -20,15 +27,11 @@ def create_preview_file
preview_paths.one? ? preview_paths.first : "test/components/previews"
end

template "component_preview.rb", File.join(path_prefix, class_path, "#{file_name}_component_preview.rb")
template "component_preview.rb", File.join(path_prefix, class_path, "#{destination_file_name}_preview.rb")
end

private

def file_name
@_file_name ||= super.sub(/_component\z/i, "")
end

def render_signature
return if attributes.blank?

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# frozen_string_literal: true

<% module_namespacing do -%>
class <%= class_name %>ComponentPreview < ViewComponent::Preview
class <%= component_class_name %>Preview < ViewComponent::Preview
def default
render(<%= class_name %>Component.new<%= "(#{render_signature})" if render_signature %>)
render(<%= component_class_name %>.new<%= "(#{render_signature})" if render_signature %>)
end
end
<% end -%>
2 changes: 1 addition & 1 deletion lib/generators/view_component/rspec/rspec_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class RspecGenerator < ::Rails::Generators::NamedBase
source_root File.expand_path("templates", __dir__)

def create_test_file
template "component_spec.rb", File.join(spec_component_path, class_path, "#{file_name}_component_spec.rb")
template "component_spec.rb", File.join(spec_component_path, class_path, "#{destination_file_name}_spec.rb")
end

private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

require "rails_helper"

RSpec.describe <%= namespaced? ? "#{namespace.name}::" : '' %><%= class_name %>Component, type: :component do
RSpec.describe <%= namespaced? ? "#{namespace.name}::" : '' %><%= component_class_name %>, type: :component do
pending "add some examples to (or delete) #{__FILE__}"

# it "renders something useful" do
Expand Down
6 changes: 1 addition & 5 deletions lib/generators/view_component/stimulus/stimulus_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ def filetype
end

def destination
if sidecar?
File.join(component_path, class_path, "#{file_name}_component", "#{file_name}_component_controller.#{filetype}")
else
File.join(component_path, class_path, "#{file_name}_component_controller.#{filetype}")
end
File.join(destination_directory, "#{destination_file_name}_controller.#{filetype}")
end

def legacy_stimulus?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

require "test_helper"

class <%= namespaced? ? "#{namespace.name}::" : '' %><%= class_name %>ComponentTest < ViewComponent::TestCase
class <%= namespaced? ? "#{namespace.name}::" : '' %><%= component_class_name %>Test < ViewComponent::TestCase
def test_component_renders_something_useful
# assert_equal(
# %(<span>Hello, components!</span>),
# render_inline(<%= class_name %>Component.new(message: "Hello, components!")).css("span").to_html
# render_inline(<%= component_class_name %>.new(message: "Hello, components!")).css("span").to_html
# )
end
end
15 changes: 8 additions & 7 deletions lib/generators/view_component/test_unit/test_unit_generator.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
# frozen_string_literal: true

require "generators/view_component/abstract_generator"

module ViewComponent
module Generators
class TestUnitGenerator < ::Rails::Generators::NamedBase
include ViewComponent::AbstractGenerator

source_root File.expand_path("templates", __dir__)
check_class_collision suffix: "ComponentTest"

def create_test_file
template "component_test.rb", File.join("test/components", class_path, "#{file_name}_component_test.rb")
def check_class_collision
class_collisions "#{component_class_name}Test"
end

private

def file_name
@_file_name ||= super.sub(/_component\z/i, "")
def create_test_file
template "component_test.rb", File.join("test/components", class_path, "#{destination_file_name}_test.rb")
end
end
end
Expand Down
6 changes: 4 additions & 2 deletions lib/view_component/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ def render_parent
# When rendering the parent inside an .erb template, use `#render_parent` instead.
def render_parent_to_string
@__vc_parent_render_level ||= 0 # ensure a good starting value
old_current_template = @current_template

begin
target_render = self.class.instance_variable_get(:@__vc_ancestor_calls)[@__vc_parent_render_level]
Expand All @@ -250,6 +251,7 @@ def render_parent_to_string
target_render.bind_call(self, @__vc_requested_details)
ensure
@__vc_parent_render_level -= 1
@current_template = old_current_template
end
end

Expand Down Expand Up @@ -458,14 +460,14 @@ def with_captured_virtual_path(captured_path)
# @private
def digest_path_from_template(template)
component_path = self.class.virtual_path
return super unless component_path && template.virtual_path == component_path
return super unless component_path && template.is_a?(ViewComponent::Template)

digest = self.class.cache_digest(
finder: lookup_context,
format: template.format || lookup_context.formats.first || :html
)

digest.present? ? "#{component_path}:#{digest}" : component_path
digest.present? ? "#{template.virtual_path}:#{digest}" : template.virtual_path
end

private
Expand Down
4 changes: 3 additions & 1 deletion lib/view_component/cache_digest/digestor_lookup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ def self.install!
private

def find_template(finder, name, prefixes, partial, keys)
Resolver.instance.find_templates(name, prefixes.first, partial, {formats: finder.formats}).first || super
Resolver.instance.find_templates(
name, prefixes.first, partial, {formats: finder.formats}, view_paths: finder.view_paths
).first || super
end
end
end
Expand Down
Loading
Loading