evc_rails 0.2.0 → 0.2.1
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/README.md +68 -48
- data/lib/evc_rails/template_handler.rb +65 -62
- data/lib/evc_rails/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5fa808d0d2dcb0efe2ff7a1807cbfabfb62995594731b066eb718e3aac9e1ee3
|
4
|
+
data.tar.gz: fc944ac269042805ec1bb1c9175a4646ac56ed4a3ac2461c6061c211c831b608
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 197a8e203c89e35b9a2365b38d40a9c24c042045a9ea654538f9fa5eb860778d7a1312231c921d6a7b9119cc41869ece6240ce5af201a4c94420406a5f02f228
|
7
|
+
data.tar.gz: 0ae04a20540c3ad665ac2b7309e1053bdb200ca6f0a9c439752904ff9400fb6ce45defbe15cc620256a6959dd7b2c2b46489bea45921c1d44b09ea813a86f000
|
data/README.md
CHANGED
@@ -154,6 +154,32 @@ This maps to:
|
|
154
154
|
|
155
155
|
### Slot Support
|
156
156
|
|
157
|
+
EVC provides a powerful and intuitive way to work with ViewComponent slots. To populate a slot, you use a corresponding `<With...>` tag that matches the **method name** provided by `renders_one` or `renders_many`.
|
158
|
+
|
159
|
+
When you use slots, EVC automatically makes the component's instance available in a block variable. By default, this variable is named after the component itself in snake_case (e.g., `<Accordion>` yields an `accordion` variable). This allows you to easily call component methods like `<%= accordion.arrow %>` from within the block. This variable is even available in deeply nested components, and you can provide a custom name to avoid ambiguity when nesting components of the same type.
|
160
|
+
|
161
|
+
#### Slot Naming Convention
|
162
|
+
|
163
|
+
The key to understanding slot tags in `evc_rails` is that they map directly to the **methods** generated by ViewComponent, not the `renders_...` declaration itself.
|
164
|
+
|
165
|
+
For both `renders_one` and `renders_many`, ViewComponent always generates a singular `with_*` method.
|
166
|
+
|
167
|
+
- `renders_one :header` provides a `with_header` method. You use `<WithHeader>`.
|
168
|
+
- `renders_many :items` provides a singular `with_item` method. You use the singular `<WithItem>` tag for each item you want to render.
|
169
|
+
|
170
|
+
This design provides maximum flexibility, allowing you to pass content as a block or make multiple self-closing calls, just like you would in standard ERB.
|
171
|
+
|
172
|
+
#### Attributes vs. Slots
|
173
|
+
|
174
|
+
There are two ways to pass information to a component:
|
175
|
+
|
176
|
+
- **As attributes:** Data passed as attributes on the main component tag (e.g. `<Card title="...">`) is sent to its `initialize` method.
|
177
|
+
- **As slot content:** Rich content passed via `<With...>` tags is used to populate the component's named slots.
|
178
|
+
|
179
|
+
#### When a Block Variable is Yielded
|
180
|
+
|
181
|
+
The contextual variable (e.g., `|card|`) is only yielded if one or more `<With...>` slot tags are present inside the component block. If you render a component like `<Card></Card>` with no slots inside, `evc_rails` is smart enough to render it without the `do |card|` part.
|
182
|
+
|
157
183
|
#### Single Slots (`renders_one`)
|
158
184
|
|
159
185
|
```ruby
|
@@ -178,11 +204,11 @@ end
|
|
178
204
|
Becomes:
|
179
205
|
|
180
206
|
```erb
|
181
|
-
<%= render CardComponent.new do |
|
182
|
-
<%
|
207
|
+
<%= render CardComponent.new do |card| %>
|
208
|
+
<% card.with_header do %>
|
183
209
|
<h1>Welcome</h1>
|
184
210
|
<% end %>
|
185
|
-
<%
|
211
|
+
<% card.with_body do %>
|
186
212
|
<p>This is the body content.</p>
|
187
213
|
<% end %>
|
188
214
|
<% end %>
|
@@ -199,69 +225,63 @@ end
|
|
199
225
|
|
200
226
|
```erb
|
201
227
|
<List>
|
202
|
-
|
203
|
-
|
204
|
-
|
228
|
+
<% @todo_items.each do |item| %>
|
229
|
+
<WithItem>
|
230
|
+
<span class="<%= item.completed? ? 'line-through' : '' %>">
|
231
|
+
<%= item.title %>
|
232
|
+
</span>
|
233
|
+
</WithItem>
|
234
|
+
<% end %>
|
205
235
|
</List>
|
206
236
|
```
|
207
237
|
|
208
238
|
Becomes:
|
209
239
|
|
210
240
|
```erb
|
211
|
-
<%= render ListComponent.new do |
|
212
|
-
<%
|
213
|
-
|
214
|
-
|
241
|
+
<%= render ListComponent.new do |list| %>
|
242
|
+
<% @todo_items.each do |item| %>
|
243
|
+
<% list.with_item do %>
|
244
|
+
<span class="<%= item.completed? ? 'line-through' : '' %>">
|
245
|
+
<%= item.title %>
|
246
|
+
</span>
|
247
|
+
<% end %>
|
248
|
+
<% end %>
|
215
249
|
<% end %>
|
216
250
|
```
|
217
251
|
|
218
|
-
####
|
252
|
+
#### Custom Variable Naming with `as`
|
219
253
|
|
220
|
-
|
221
|
-
<Navigation>
|
222
|
-
<WithLink href={learning_path} text="Learning Path" />
|
223
|
-
<WithLink href={courses_path} text="All Courses" />
|
224
|
-
<WithLink text="Reports">
|
225
|
-
<WithSublink href={reports_users_path} text="Users" />
|
226
|
-
<WithSublink href={reports_activity_path} text="Activity" />
|
227
|
-
</WithLink>
|
228
|
-
<WithFooter>
|
229
|
-
<div>Footer content</div>
|
230
|
-
</WithFooter>
|
231
|
-
</Navigation>
|
232
|
-
```
|
254
|
+
For clarity or to resolve ambiguity when nesting components of the same type, you can provide a custom variable name with the `as` attribute.
|
233
255
|
|
234
|
-
|
256
|
+
```ruby
|
257
|
+
# app/components/card_component.rb
|
258
|
+
class CardComponent < ViewComponent::Base
|
259
|
+
renders_one :header
|
235
260
|
|
236
|
-
|
261
|
+
attr_reader :title
|
237
262
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
</Card>
|
263
|
+
def initialize(title: "Default Title")
|
264
|
+
@title = title
|
265
|
+
end
|
266
|
+
end
|
243
267
|
```
|
244
268
|
|
245
|
-
### Complex Nesting
|
246
|
-
|
247
269
|
```erb
|
248
|
-
<
|
249
|
-
<
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
<p class="text-center">Widget 3</p>
|
260
|
-
</UI::Card>
|
261
|
-
</UI::Grid>
|
262
|
-
</UI::Card>
|
270
|
+
<Card as="outer_card" title="Outer Card">
|
271
|
+
<WithHeader>
|
272
|
+
<h2><%= outer_card.title %></h2>
|
273
|
+
<Card as="inner_card" title="Inner Card">
|
274
|
+
<WithHeader>
|
275
|
+
<h3><%= inner_card.title %></h3>
|
276
|
+
<p>Outer card title from inner scope: <%= outer_card.title %></p>
|
277
|
+
</WithHeader>
|
278
|
+
</Card>
|
279
|
+
</WithHeader>
|
280
|
+
</Card>
|
263
281
|
```
|
264
282
|
|
283
|
+
This generates distinct variables, `outer_card` and `inner_card`, allowing you to access the context of each component without collision.
|
284
|
+
|
265
285
|
### Mixed Content
|
266
286
|
|
267
287
|
You can mix regular HTML, ERB, and component tags:
|
@@ -104,49 +104,35 @@ module EvcRails
|
|
104
104
|
match = next_open
|
105
105
|
tag_name = match[1]
|
106
106
|
attributes_str = match[2].to_s.strip
|
107
|
+
params, as_variable = parse_attributes(attributes_str, attribute_regex)
|
108
|
+
param_str = params.join(", ")
|
107
109
|
is_self_closing = match[0].end_with?("/>")
|
108
110
|
|
109
111
|
# Add content before the tag
|
110
112
|
result << source[pos...match.begin(0)] if pos < match.begin(0)
|
111
113
|
|
112
|
-
# Determine if this is a slot (e.g., WithHeader, WithPost
|
113
|
-
|
114
|
+
# Determine if this is a slot (e.g., WithHeader, WithPost)
|
115
|
+
parent_component = stack.reverse.find { |item| item[4] == :component }
|
114
116
|
is_slot = false
|
115
117
|
slot_name = nil
|
116
|
-
|
117
|
-
if
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
is_slot = true
|
123
|
-
slot_name = tag_name[4..-1].downcase # Remove "With" prefix and convert to lowercase
|
124
|
-
slot_parent = parent_tag
|
125
|
-
# Mark parent as having a slot
|
126
|
-
parent[6] = true
|
127
|
-
# Check for Component::slotname syntax (backward compatibility)
|
128
|
-
elsif tag_name.start_with?("#{parent_tag}::")
|
129
|
-
is_slot = true
|
130
|
-
slot_name = tag_name.split("::").last.downcase
|
131
|
-
slot_parent = parent_tag
|
132
|
-
# Mark parent as having a slot
|
133
|
-
parent[6] = true
|
134
|
-
end
|
118
|
+
|
119
|
+
if parent_component && tag_name.start_with?("With")
|
120
|
+
is_slot = true
|
121
|
+
# Convert CamelCase slot name to snake_case
|
122
|
+
slot_name = tag_name[4..].gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
123
|
+
parent_component[6] = true # Mark parent as having a slot
|
135
124
|
end
|
136
125
|
|
137
126
|
if is_self_closing
|
138
127
|
if is_slot
|
139
|
-
|
140
|
-
param_str = params.join(", ")
|
128
|
+
parent_variable = parent_component[8]
|
141
129
|
result << if param_str.empty?
|
142
|
-
"<%
|
130
|
+
"<% #{parent_variable}.with_#{slot_name} do %><% end %>"
|
143
131
|
else
|
144
|
-
"<%
|
132
|
+
"<% #{parent_variable}.with_#{slot_name}(#{param_str}) do %><% end %>"
|
145
133
|
end
|
146
134
|
else
|
147
135
|
component_class = "#{tag_name}Component"
|
148
|
-
params = parse_attributes(attributes_str, attribute_regex)
|
149
|
-
param_str = params.join(", ")
|
150
136
|
result << if param_str.empty?
|
151
137
|
"<%= render #{component_class}.new %>"
|
152
138
|
else
|
@@ -154,35 +140,23 @@ module EvcRails
|
|
154
140
|
end
|
155
141
|
end
|
156
142
|
elsif is_slot
|
157
|
-
|
158
|
-
param_str
|
159
|
-
stack << [tag_name, nil, param_str, result.length, :slot, slot_name, false, match.begin(0)]
|
143
|
+
parent_variable = parent_component[8]
|
144
|
+
stack << [tag_name, nil, param_str, result.length, :slot, slot_name, false, match.begin(0), nil]
|
160
145
|
result << if param_str.empty?
|
161
|
-
"<%
|
146
|
+
"<% #{parent_variable}.with_#{slot_name} do %>"
|
162
147
|
else
|
163
|
-
"<%
|
148
|
+
"<% #{parent_variable}.with_#{slot_name}(#{param_str}) do %>"
|
164
149
|
end
|
165
150
|
else
|
166
151
|
component_class = "#{tag_name}Component"
|
167
|
-
|
168
|
-
param_str
|
169
|
-
|
170
|
-
if
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
else
|
176
|
-
"<%= render #{component_class}.new(#{param_str}) do %>"
|
177
|
-
end
|
178
|
-
else
|
179
|
-
stack << [tag_name, component_class, param_str, result.length, :component, nil, false, match.begin(0)]
|
180
|
-
result << if param_str.empty?
|
181
|
-
"<%= render #{component_class}.new do %>"
|
182
|
-
else
|
183
|
-
"<%= render #{component_class}.new(#{param_str}) do %>"
|
184
|
-
end
|
185
|
-
end
|
152
|
+
variable_name = as_variable || component_variable_name(tag_name)
|
153
|
+
stack << [tag_name, component_class, param_str, result.length, :component, nil, false, match.begin(0),
|
154
|
+
variable_name]
|
155
|
+
result << if param_str.empty?
|
156
|
+
"<%= render #{component_class}.new do %>"
|
157
|
+
else
|
158
|
+
"<%= render #{component_class}.new(#{param_str}) do %>"
|
159
|
+
end
|
186
160
|
end
|
187
161
|
|
188
162
|
pos = match.end(0)
|
@@ -202,7 +176,7 @@ module EvcRails
|
|
202
176
|
end
|
203
177
|
|
204
178
|
# Find the matching opening tag (from the end)
|
205
|
-
matching_index = stack.rindex { |(tag_name,
|
179
|
+
matching_index = stack.rindex { |(tag_name, *)| tag_name == closing_tag_name }
|
206
180
|
if matching_index.nil?
|
207
181
|
line = line_number_at_position(source, match.begin(0))
|
208
182
|
col = column_number_at_position(source, match.begin(0))
|
@@ -210,18 +184,30 @@ module EvcRails
|
|
210
184
|
end
|
211
185
|
|
212
186
|
# Pop the matching opening tag
|
213
|
-
|
187
|
+
open_tag_data = stack.delete_at(matching_index)
|
188
|
+
tag_type = open_tag_data[4]
|
189
|
+
|
190
|
+
if tag_type == :component
|
191
|
+
_tag_name, component_class, param_str, start_pos, _type, _slot_name, slot_used, _open_pos, variable_name = open_tag_data
|
192
|
+
|
193
|
+
# Patch in |variable_name| for component if a slot was used
|
194
|
+
if slot_used
|
195
|
+
# More robustly find the end of the `do` block to insert the variable.
|
196
|
+
# This avoids faulty regex matching on complex parameters.
|
197
|
+
relevant_part = result[start_pos..-1]
|
198
|
+
match_for_insertion = /( do)( %>)/.match(relevant_part)
|
199
|
+
if match_for_insertion
|
200
|
+
# Insert the variable name just before the ` do`
|
201
|
+
insertion_point = start_pos + match_for_insertion.begin(1)
|
202
|
+
result.insert(insertion_point, " |#{variable_name}|")
|
203
|
+
end
|
204
|
+
end
|
214
205
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
open_block_regex = /(<%= render #{component_class}\.new(?:\(.*?\))? do)( %>)/
|
219
|
-
result.sub!(open_block_regex) { "#{::Regexp.last_match(1)} |c|#{::Regexp.last_match(2)}" }
|
206
|
+
result << "<% end %>"
|
207
|
+
else # It's a slot
|
208
|
+
result << "<% end %>"
|
220
209
|
end
|
221
210
|
|
222
|
-
# Add closing block
|
223
|
-
result << (type == :slot ? "<% end %>" : "<% end %>")
|
224
|
-
|
225
211
|
pos = match.end(0)
|
226
212
|
else
|
227
213
|
# No more tags, add remaining content
|
@@ -248,7 +234,24 @@ module EvcRails
|
|
248
234
|
erb_string
|
249
235
|
end
|
250
236
|
|
237
|
+
def component_variable_name(tag_name)
|
238
|
+
# Simplified version of ActiveSupport's underscore
|
239
|
+
name = tag_name.gsub(/::/, "_")
|
240
|
+
name.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
241
|
+
name.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
242
|
+
name.tr!("-", "_")
|
243
|
+
name.downcase!
|
244
|
+
name
|
245
|
+
end
|
246
|
+
|
251
247
|
def parse_attributes(attributes_str, attribute_regex = ATTRIBUTE_REGEX)
|
248
|
+
as_variable = nil
|
249
|
+
# Find and remove the `as` attribute, storing its value.
|
250
|
+
attributes_str = attributes_str.gsub(/\bas=(?:"([^"]*)"|'([^']*)')/) do |_match|
|
251
|
+
as_variable = Regexp.last_match(1) || Regexp.last_match(2)
|
252
|
+
""
|
253
|
+
end.strip
|
254
|
+
|
252
255
|
params = []
|
253
256
|
attributes_str.scan(attribute_regex) do |key, quoted_value, single_quoted_value, ruby_expression|
|
254
257
|
if ruby_expression
|
@@ -259,7 +262,7 @@ module EvcRails
|
|
259
262
|
params << "#{key}: \"#{single_quoted_value.gsub("'", "\\'")}\""
|
260
263
|
end
|
261
264
|
end
|
262
|
-
params
|
265
|
+
[params, as_variable]
|
263
266
|
end
|
264
267
|
end
|
265
268
|
end
|
data/lib/evc_rails/version.rb
CHANGED