view_component_subtemplates 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 604c21a85c4afb58ae6b08b1bc1fcbc99ed132de60bb7d9e846b3fe0e50e0c25
4
+ data.tar.gz: b5a0456d4a5d8f83d3368f03839a2d04ffbcb6edbc97e2d6224eaae30737a4d5
5
+ SHA512:
6
+ metadata.gz: 276d58f9205465d4c98cb4fe864764dd58d130a850b4b05bb3a5c21942f68edc8a5f123aa5d8dbe68ac4746ea282c97d299a44df5bae3bb4cddfc091182757a1
7
+ data.tar.gz: f36101a4c89486232386935f7b1676a9b359aa1b0ede4b6df95fbf5ef371d4dc5b6633fb3cb59c86f73dc6fea20c60696e42059e447ce3a40934eb0dd8407290
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1
3
+
4
+ Style/StringLiterals:
5
+ EnforcedStyle: double_quotes
6
+
7
+ Style/StringLiteralsInInterpolation:
8
+ EnforcedStyle: double_quotes
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-07-18
4
+
5
+ ### Agregado
6
+ - Implementación inicial del soporte para sub-templates en ViewComponent usando archivos sidecar
7
+ - Añadido el DSL `template_arguments` para definir argumentos en sub-templates
8
+ - Generación dinámica de métodos `call_*` para sub-templates con validación estricta de argumentos
9
+ - Integración con el flujo de compilación de ViewComponent
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # ViewComponent Subtemplates
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/view_component_subtemplates.svg)](https://badge.fury.io/rb/view_component_subtemplates)
4
+ [![Build Status](https://github.com/bukhr/view_component_subtemplates/workflows/CI/badge.svg)](https://github.com/bukhr/view_component_subtemplates/actions)
5
+
6
+ Adds support for **sub-templates with typed arguments** to [ViewComponent](https://viewcomponent.org/), enabling modular, reusable component architectures.
7
+
8
+ ## Features
9
+
10
+ ✅ **Template Arguments** - Define typed arguments for sub-templates
11
+ ✅ **Automatic Detection** - Sub-templates discovered in component sidecar directories
12
+ ✅ **Dynamic Methods** - `call_[name]` helper methods generated automatically
13
+ ✅ **ViewComponent Integration** - Seamless integration with existing ViewComponent workflow
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'view_component_subtemplates'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ ```bash
26
+ bundle install
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ### 1. Define a component
32
+
33
+ ```ruby
34
+ # app/components/table_component.rb
35
+ class TableComponent < ViewComponent::Base
36
+ def initialize(users:, title:)
37
+ @users = users
38
+ @title = title
39
+ end
40
+ end
41
+ ```
42
+
43
+ ### 2. Create your templates
44
+
45
+ ```erb
46
+ <!-- app/components/table_component.html.erb -->
47
+ <div class="table-container">
48
+ <%= call_header(title: @title, sortable: true) %>
49
+
50
+ <tbody>
51
+ <% @users.each_with_index do |user, index| %>
52
+ <%= call_row(model: user, highlight: index.even?) %>
53
+ <% end %>
54
+ </tbody>
55
+
56
+ <%= call_footer(total_count: @users.count) %>
57
+ </div>
58
+ ```
59
+
60
+ ### 3. File structure
61
+
62
+ ```
63
+ app/components/
64
+ ├── table_component.rb
65
+ ├── table_component.html.erb
66
+ └── table_component/
67
+ ├── header.html.erb
68
+ ├── row.html.erb
69
+ └── footer.html.erb
70
+ ```
71
+
72
+ ### 4. Sub-template files
73
+
74
+ ```erb
75
+ <!-- app/components/table_component/header.html.erb -->
76
+ <%# locals: (title:, sortable:) -%>
77
+ <thead class="<%= 'sortable' if sortable %>">
78
+ <tr>
79
+ <th><%= title %></th>
80
+ <th>Actions</th>
81
+ </tr>
82
+ </thead>
83
+ ```
84
+
85
+ ```erb
86
+ <!-- app/components/table_component/row.html.erb -->
87
+ <%# locals: (model:, highlight:) -%>
88
+ <tr class="<%= 'highlighted' if highlight %>">
89
+ <td><%= model.name %></td>
90
+ <td><%= model.email %></td>
91
+ </tr>
92
+ ```
93
+
94
+ ```erb
95
+ <!-- app/components/table_component/footer.html.erb -->
96
+ <%# locals: (total_count:) -%>
97
+ <tfoot>
98
+ <tr>
99
+ <td colspan="2">Total: <%= total_count %> users</td>
100
+ </tr>
101
+ </tfoot>
102
+ ```
103
+
104
+ ### 5. Use in your views
105
+
106
+ ```erb
107
+ <%= render TableComponent.new(users: @users, title: "User List") %>
108
+ ```
109
+
110
+ ## Requirements
111
+
112
+ - Ruby >= 3.1.0
113
+ - Rails >= 7.0.0
114
+ - ViewComponent >= 4.2.0
115
+
116
+ ## Development
117
+
118
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
119
+
120
+ ```bash
121
+ bundle install
122
+ bundle exec rake test
123
+ ```
124
+
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,38 @@
1
+ # compiler_extension.rb
2
+ module ViewComponentSubtemplates
3
+ module CompilerExtension
4
+ # Processes a component class to compile its sub-templates.
5
+ # Called from the after_compile hook.
6
+ def self.process_component(component_class)
7
+ gather_sub_templates_for(component_class).each do |sub_template|
8
+ sub_template.compile_to_component
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def self.gather_sub_templates_for(component_class)
15
+ component_subdir = ViewComponentSubtemplates.component_subdir_for(component_class)
16
+ return [] unless Dir.exist?(component_subdir)
17
+
18
+ template_extensions = ActionView::Template.template_handler_extensions
19
+
20
+ Dir.glob(File.join(component_subdir, "*")).filter_map do |file_path|
21
+ next unless File.file?(file_path)
22
+
23
+ file_extension = File.extname(file_path)[1..] # Remove the leading dot
24
+ next unless template_extensions.include?(file_extension)
25
+
26
+ # Correctly extract template name by removing the first extension found.
27
+ # e.g. "header.html.erb" -> "header"
28
+ template_name = File.basename(file_path).split('.').first
29
+
30
+ SubTemplate.new(
31
+ component: component_class,
32
+ path: file_path,
33
+ template_name: template_name
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,106 @@
1
+ # sub_template.rb
2
+ module ViewComponentSubtemplates
3
+ # Represents a single sub-template file associated with a ViewComponent.
4
+ # This class is responsible for parsing the template, extracting its arguments,
5
+ # and defining the corresponding `call_*` method on the component class.
6
+ #
7
+ # PUBLIC API - The following methods are guaranteed to be stable across minor versions:
8
+ # - #component: Returns the associated component class.
9
+ # - #path: Returns the absolute path to the template file.
10
+ # - #template_name: Returns the short name of the template (e.g., `header`).
11
+ # - #explicit_locals: Returns an array of symbols for the template's arguments.
12
+ # - #source: Returns the raw source code of the template file.
13
+ #
14
+ # PRIVATE API - All other methods are subject to change without notice.
15
+ class SubTemplate
16
+ # PUBLIC API
17
+ attr_reader :component, :path, :template_name
18
+
19
+ def initialize(component:, path:, template_name:)
20
+ @component = component
21
+ @path = path
22
+ @template_name = template_name
23
+ @source = nil # Lazily loaded
24
+ @explicit_locals = nil # Lazily loaded
25
+ end
26
+
27
+ def source
28
+ @source ||= File.read(path)
29
+ end
30
+
31
+ def explicit_locals
32
+ @explicit_locals ||= extract_explicit_locals_from_source
33
+ end
34
+
35
+ # This is the main entry point for the class from the compiler extension
36
+ def compile_to_component
37
+ validate_file_exists!
38
+ compiled_source = compile_erb_source
39
+ define_render_method(compiled_source, explicit_locals)
40
+ end
41
+
42
+ private
43
+
44
+ def validate_file_exists!
45
+ unless File.exist?(path)
46
+ raise ViewComponentSubtemplates::Error, "Template file not found: #{path}"
47
+ end
48
+ end
49
+
50
+ def extract_explicit_locals_from_source
51
+ match = source.match(/<%#\s*locals:\s*\((.*?)\)\s*-%>/)
52
+ return [] unless match
53
+
54
+ locals_string = match[1]
55
+ locals_string.scan(/(\w+):/).flatten.map(&:to_sym)
56
+ end
57
+
58
+ def compile_erb_source
59
+ erb = ERB.new(source)
60
+ erb.filename = path
61
+ erb.src
62
+ end
63
+
64
+ def define_render_method(compiled_source, expected_args)
65
+ render_method_name = "__render_sub_template_#{template_name}"
66
+ call_method_name = "call_#{template_name}"
67
+
68
+ define_private_render_method(render_method_name, compiled_source, expected_args)
69
+ define_public_call_method(call_method_name, render_method_name, expected_args)
70
+ end
71
+
72
+ def define_private_render_method(name, compiled_source, args)
73
+ @component.class_eval <<~RUBY, path, 1
74
+ private
75
+ def #{name}(#{args.join(', ')})
76
+ (#{compiled_source}).html_safe
77
+ end
78
+ RUBY
79
+ end
80
+
81
+ def define_public_call_method(call_name, render_name, args)
82
+ @component.silence_redefinition_of_method(call_name.to_sym)
83
+
84
+ if args.any?
85
+ signature = args.map { |arg| "#{arg}:" }.join(", ")
86
+ forwarded_args = args.join(", ")
87
+
88
+ @component.class_eval <<~RUBY, path, 1
89
+ def #{call_name}(#{signature})
90
+ #{render_name}(#{forwarded_args})
91
+ end
92
+ RUBY
93
+ else
94
+ @component.class_eval <<~RUBY, path, 1
95
+ def #{call_name}
96
+ #{render_name}
97
+ end
98
+ RUBY
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+
105
+
106
+
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponentSubtemplates
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,43 @@
1
+ # view_component_subtemplates.rb
2
+ # frozen_string_literal: true
3
+
4
+ require "view_component"
5
+
6
+ # Ensure ViewComponent internals are loaded
7
+ require "view_component/base"
8
+ require "view_component/compiler"
9
+
10
+ require_relative "view_component_subtemplates/version"
11
+ require_relative "view_component_subtemplates/compiler_extension"
12
+
13
+ module ViewComponentSubtemplates
14
+ class Error < StandardError; end
15
+
16
+ def self.sub_template_path_for(component_class, template_name)
17
+ component_dir = File.dirname(component_class.identifier)
18
+ component_name = component_class.name.demodulize.underscore
19
+ File.join(component_dir, component_name, "#{template_name}.html.erb")
20
+ end
21
+
22
+ def self.component_subdir_for(component_class)
23
+ component_dir = File.dirname(component_class.identifier)
24
+ component_name = component_class.name.demodulize.underscore
25
+ File.join(component_dir, component_name)
26
+ end
27
+
28
+ # Module to extend ViewComponent::Base class methods
29
+ # This hooks into the after_compile class method from ViewComponent PR #2411
30
+ module AfterCompileHook
31
+ def after_compile
32
+ super
33
+ ViewComponentSubtemplates::CompilerExtension.process_component(self)
34
+ end
35
+ end
36
+ end
37
+
38
+ # Load SubTemplate after the module is fully configured
39
+ require_relative "view_component_subtemplates/sub_template"
40
+
41
+ # Extend ViewComponent::Base class methods to hook into after_compile
42
+ # This uses prepend on the singleton class to extend the class method
43
+ ViewComponent::Base.singleton_class.prepend(ViewComponentSubtemplates::AfterCompileHook)
@@ -0,0 +1,4 @@
1
+ module ViewComponentSubtemplates
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: view_component_subtemplates
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - jsolas
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-01-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: view_component
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '7.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '7.0'
41
+ description: Adds support for sub-templates with arguments to ViewComponent, allowing
42
+ components to have multiple templates in their sidecar directory.
43
+ email:
44
+ - jsolas@buk.cl
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".rubocop.yml"
50
+ - CHANGELOG.md
51
+ - CODE_OF_CONDUCT.md
52
+ - README.md
53
+ - Rakefile
54
+ - lib/view_component_subtemplates.rb
55
+ - lib/view_component_subtemplates/compiler_extension.rb
56
+ - lib/view_component_subtemplates/sub_template.rb
57
+ - lib/view_component_subtemplates/version.rb
58
+ - sig/view_component_subtemplates.rbs
59
+ homepage: https://github.com/bukhr/view_component_subtemplates
60
+ licenses:
61
+ - MIT
62
+ metadata:
63
+ allowed_push_host: https://rubygems.org
64
+ homepage_uri: https://github.com/bukhr/view_component_subtemplates
65
+ source_code_uri: https://github.com/bukhr/view_component_subtemplates
66
+ changelog_uri: https://github.com/bukhr/view_component_subtemplates/blob/main/CHANGELOG.md
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 3.1.0
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 3.5.18
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: Sub-templates functionality for ViewComponent
86
+ test_files: []