view_component_subtemplates 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7f722cb0d9a6fcd6224817d0e22c98eac0e958fbc4e97fd40b756522aebe462
4
- data.tar.gz: 23e07e652f582ed9827adb0ae6100c76587c9fd115346a5fe843f8cf0e8c881c
3
+ metadata.gz: acf9c5ce1fdb01c2939e9e5737cfaf2b66a38a9540f8f24eafadf3cb473aaba5
4
+ data.tar.gz: 78bfcfbc62662ab2d5ac93a7f56a1bf4fcdd410132bf23e356c58b9a503336f9
5
5
  SHA512:
6
- metadata.gz: ac11be7bea5d9ecca0f28b03606a9044a75b3492f3e877bf0e73e97809e66e74d180bb595b314005e43176171889c4314fe60514e8a6ba63691261e04064922a
7
- data.tar.gz: 88786270c38e3637cfbb5dc45ab0c4f44da14a06ed0c7a92f1d814f7bd09c2551739227172329fd02d035ddae1a834ffb3767c7afa14a16bd81a546d61ab7a48
6
+ metadata.gz: 85de07486ae8017548b8bcef1168967cecb340bfe3060564f33fc9779b8fef323703b364b38d2bde01814d7f4eecb9295f73fc620531ebf14a529108d6a8c7dd
7
+ data.tar.gz: 34d0b0f65cac64023cb71eecb06e6001dccb9f1b2144510ea71c21acbaf19cdf13cfaff17a19840ae7be55c09ce52ac3a157810cd11726ce836ab83e0f7800c3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-06-12
4
+
5
+ ### Added
6
+ - `render_subtemplate_in(view_context, name)` to render a single subtemplate on its own, from outside the component (e.g. a controller responding with just a fragment), without rendering the main template. It is the subtemplate-level counterpart of ViewComponent's `render_in`. The subtemplate must not declare locals (per-render data is passed through the component's constructor); rendering a subtemplate with locals, or one that is not defined, raises a clear error.
7
+
8
+ ## [0.2.0] - 2026-01-21
9
+
10
+ ### Added
11
+ - Automatic subtemplate processing for ancestor components
12
+ - Support for multi-level inheritance (grandparent → parent → child)
13
+
14
+ ### Fixed
15
+ - Inheritance issue where child components miss parent's `call_*` methods
16
+
3
17
  ## [0.1.1] - 2026-01-13
4
18
 
5
19
  ### Changed
data/README.md CHANGED
@@ -107,6 +107,47 @@ app/components/
107
107
  <%= render TableComponent.new(users: @users, title: "User List") %>
108
108
  ```
109
109
 
110
+ ## Rendering a subtemplate on its own
111
+
112
+ Sometimes you need to render just one subtemplate, outside the component's main
113
+ template. A common case is a controller action that responds to an AJAX request
114
+ with only an updated fragment.
115
+
116
+ Call `render_subtemplate_in` on a component instance, passing the current view
117
+ context. The subtemplate **must not declare locals**: any per-render data is
118
+ passed through the component's constructor, so the call site stays free of an
119
+ untyped `**locals` boundary.
120
+
121
+ ```ruby
122
+ class UsersController < ApplicationController
123
+ def row
124
+ component = RowComponent.new(user: User.find(params[:id]), highlight: false)
125
+
126
+ render html: component.render_subtemplate_in(view_context, :row), layout: false
127
+ end
128
+ end
129
+ ```
130
+
131
+ ```erb
132
+ <%# app/components/row_component/row.html.erb -- no `locals:` line %>
133
+ <tr class="<%= 'highlighted' if @highlight %>"><td><%= @user.name %></td></tr>
134
+ ```
135
+
136
+ `render_subtemplate_in(view_context, name)` renders the named subtemplate and
137
+ returns its HTML as an html_safe string, without rendering the component's main
138
+ template. It is the subtemplate-level counterpart of ViewComponent's `render_in`:
139
+ `render_in(view_context)` is the external entry point for the main template
140
+ (internally `call`), and `render_subtemplate_in` is the external entry point for
141
+ a single subtemplate (internally `call_<name>`).
142
+
143
+ `render_subtemplate_in` raises a clear error if the named subtemplate does not
144
+ exist, or if it declares locals (pass that data through the component's
145
+ constructor instead).
146
+
147
+ **The no-locals rule applies only to standalone rendering.** Inside a component's
148
+ templates, `call_<name>` keeps taking locals exactly as shown in the Quick Start;
149
+ `render_subtemplate_in` is the only path that requires a no-locals subtemplate.
150
+
110
151
  ## Requirements
111
152
 
112
153
  - Ruby >= 3.1.0
@@ -1,14 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # compiler_extension.rb
4
3
  module ViewComponentSubtemplates
4
+ # Extends ViewComponent's compilation process to handle subtemplates.
5
5
  module CompilerExtension
6
- # Processes a component class to compile its sub-templates.
7
- # Called from the after_compile hook.
6
+ # Compiles all subtemplates for a component into call_* methods.
7
+ # Skips processing if already done for this component.
8
+ #
9
+ # @param component_class [Class] the component class to process
10
+ # @return [void]
8
11
  def self.process_component(component_class)
12
+ return if component_class.instance_variable_get(:@__subtemplates_processed)
13
+
9
14
  gather_sub_templates_for(component_class).each(&:compile_to_component)
15
+ component_class.instance_variable_set(:@__subtemplates_processed, true)
10
16
  end
11
17
 
18
+ # Discovers subtemplate files in the component's subdirectory.
19
+ #
20
+ # @param component_class [Class] the component class
21
+ # @return [Array<SubTemplate>] array of subtemplate objects
12
22
  def self.gather_sub_templates_for(component_class)
13
23
  component_subdir = ViewComponentSubtemplates.component_subdir_for(component_class)
14
24
  return [] unless Dir.exist?(component_subdir)
@@ -17,12 +27,8 @@ module ViewComponentSubtemplates
17
27
 
18
28
  Dir.glob(File.join(component_subdir, "*")).filter_map do |file_path|
19
29
  next unless File.file?(file_path)
30
+ next unless template_extensions.include?(File.extname(file_path)[1..])
20
31
 
21
- file_extension = File.extname(file_path)[1..] # Remove the leading dot
22
- next unless template_extensions.include?(file_extension)
23
-
24
- # Correctly extract template name by removing the first extension found.
25
- # e.g. "header.html.erb" -> "header"
26
32
  template_name = File.basename(file_path).split(".").first
27
33
 
28
34
  SubTemplate.new(
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponentSubtemplates
4
+ # Renders one subtemplate on its own (no main template), e.g. to serve a fragment
5
+ # from a controller. The subtemplate-level counterpart of `render_in`.
6
+ # Requires a no-locals subtemplate: per-render data goes through the constructor.
7
+ module StandaloneRenderer
8
+ # @param view_context [ActionView::Base] inside a controller, this is `view_context`.
9
+ # @param name [Symbol, String] subtemplate name, without the `call_` prefix.
10
+ # @return [String] the subtemplate's HTML (html_safe).
11
+ def render_subtemplate_in(view_context, name)
12
+ self.class.__vc_compile(raise_errors: true)
13
+ ensure_renderable_subtemplate!(name)
14
+
15
+ # Set up the view context the helper guards require. `__vc_original_view_context`
16
+ # makes `helpers` reuse it instead of building a new one (keep both).
17
+ @view_context = view_context
18
+ self.__vc_original_view_context = view_context
19
+
20
+ public_send("call_#{name}")
21
+ end
22
+
23
+ private
24
+
25
+ def ensure_renderable_subtemplate!(name)
26
+ call_method = "call_#{name}"
27
+ unless respond_to?(call_method)
28
+ raise ViewComponentSubtemplates::Error,
29
+ "Subtemplate `#{name}` is not defined on #{self.class}. " \
30
+ "Available subtemplates: #{available_subtemplates.join(", ").presence || "(none)"}."
31
+ end
32
+
33
+ return if method(call_method).parameters.empty?
34
+
35
+ raise ViewComponentSubtemplates::Error,
36
+ "Subtemplate `#{name}` declares locals; standalone rendering requires a subtemplate " \
37
+ "without locals. Pass per-render data through the component's constructor instead."
38
+ end
39
+
40
+ def available_subtemplates
41
+ methods.grep(/\Acall_(.+)\z/) { Regexp.last_match(1) }
42
+ end
43
+ end
44
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ViewComponentSubtemplates
4
- VERSION = "0.1.1"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -26,15 +26,21 @@ module ViewComponentSubtemplates
26
26
  module AfterCompileHook
27
27
  def after_compile
28
28
  super
29
- ViewComponentSubtemplates::CompilerExtension.process_component(self)
29
+
30
+ # Process subtemplates for all ancestors (top to bottom) and self
31
+ ancestors.select { |a| a.is_a?(Class) && a < ViewComponent::Base }
32
+ .reverse
33
+ .each { |ancestor| ViewComponentSubtemplates::CompilerExtension.process_component(ancestor) }
30
34
  end
31
35
  end
32
36
  end
33
37
 
34
38
  # Load SubTemplate after the module is fully configured
35
39
  require_relative "view_component_subtemplates/sub_template"
40
+ require_relative "view_component_subtemplates/standalone_renderer"
36
41
 
37
42
  # Hook into ViewComponent when it loads (lazy loading for faster boot in development)
38
43
  ActiveSupport.on_load(:view_component) do
39
44
  ViewComponent::Base.singleton_class.prepend(ViewComponentSubtemplates::AfterCompileHook)
45
+ ViewComponent::Base.include(ViewComponentSubtemplates::StandaloneRenderer)
40
46
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: view_component_subtemplates
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - jsolas
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-01-14 00:00:00.000000000 Z
11
+ date: 2026-06-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -53,6 +53,7 @@ files:
53
53
  - Rakefile
54
54
  - lib/view_component_subtemplates.rb
55
55
  - lib/view_component_subtemplates/compiler_extension.rb
56
+ - lib/view_component_subtemplates/standalone_renderer.rb
56
57
  - lib/view_component_subtemplates/sub_template.rb
57
58
  - lib/view_component_subtemplates/version.rb
58
59
  homepage: https://github.com/bukhr/view_component_subtemplates