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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +41 -0
- data/lib/view_component_subtemplates/compiler_extension.rb +14 -8
- data/lib/view_component_subtemplates/standalone_renderer.rb +44 -0
- data/lib/view_component_subtemplates/version.rb +1 -1
- data/lib/view_component_subtemplates.rb +7 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: acf9c5ce1fdb01c2939e9e5737cfaf2b66a38a9540f8f24eafadf3cb473aaba5
|
|
4
|
+
data.tar.gz: 78bfcfbc62662ab2d5ac93a7f56a1bf4fcdd410132bf23e356c58b9a503336f9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
#
|
|
7
|
-
#
|
|
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
|
|
@@ -26,15 +26,21 @@ module ViewComponentSubtemplates
|
|
|
26
26
|
module AfterCompileHook
|
|
27
27
|
def after_compile
|
|
28
28
|
super
|
|
29
|
-
|
|
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.
|
|
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-
|
|
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
|