evc_rails 0.1.4 → 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 +77 -30
- data/lib/evc_rails/template_handler.rb +65 -53
- 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
|
@@ -166,23 +192,23 @@ end
|
|
166
192
|
|
167
193
|
```erb
|
168
194
|
<Card>
|
169
|
-
<
|
195
|
+
<WithHeader>
|
170
196
|
<h1>Welcome</h1>
|
171
|
-
</
|
172
|
-
<
|
197
|
+
</WithHeader>
|
198
|
+
<WithBody>
|
173
199
|
<p>This is the body content.</p>
|
174
|
-
</
|
200
|
+
</WithBody>
|
175
201
|
</Card>
|
176
202
|
```
|
177
203
|
|
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,42 +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`
|
253
|
+
|
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.
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
# app/components/card_component.rb
|
258
|
+
class CardComponent < ViewComponent::Base
|
259
|
+
renders_one :header
|
260
|
+
|
261
|
+
attr_reader :title
|
262
|
+
|
263
|
+
def initialize(title: "Default Title")
|
264
|
+
@title = title
|
265
|
+
end
|
266
|
+
end
|
267
|
+
```
|
219
268
|
|
220
269
|
```erb
|
221
|
-
<
|
222
|
-
<
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
<p class="text-center">Widget 3</p>
|
233
|
-
</UI::Card>
|
234
|
-
</UI::Grid>
|
235
|
-
</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>
|
236
281
|
```
|
237
282
|
|
283
|
+
This generates distinct variables, `outer_card` and `inner_card`, allowing you to access the context of each component without collision.
|
284
|
+
|
238
285
|
### Mixed Content
|
239
286
|
|
240
287
|
You can mix regular HTML, ERB, and component tags:
|
@@ -104,40 +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.,
|
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
|
-
slot_parent = parent_tag
|
123
|
-
# Mark parent as having a slot
|
124
|
-
parent[6] = true
|
125
|
-
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
|
126
124
|
end
|
127
125
|
|
128
126
|
if is_self_closing
|
129
127
|
if is_slot
|
130
|
-
|
131
|
-
param_str = params.join(", ")
|
128
|
+
parent_variable = parent_component[8]
|
132
129
|
result << if param_str.empty?
|
133
|
-
"<%
|
130
|
+
"<% #{parent_variable}.with_#{slot_name} do %><% end %>"
|
134
131
|
else
|
135
|
-
"<%
|
132
|
+
"<% #{parent_variable}.with_#{slot_name}(#{param_str}) do %><% end %>"
|
136
133
|
end
|
137
134
|
else
|
138
135
|
component_class = "#{tag_name}Component"
|
139
|
-
params = parse_attributes(attributes_str, attribute_regex)
|
140
|
-
param_str = params.join(", ")
|
141
136
|
result << if param_str.empty?
|
142
137
|
"<%= render #{component_class}.new %>"
|
143
138
|
else
|
@@ -145,35 +140,23 @@ module EvcRails
|
|
145
140
|
end
|
146
141
|
end
|
147
142
|
elsif is_slot
|
148
|
-
|
149
|
-
param_str
|
150
|
-
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]
|
151
145
|
result << if param_str.empty?
|
152
|
-
"<%
|
146
|
+
"<% #{parent_variable}.with_#{slot_name} do %>"
|
153
147
|
else
|
154
|
-
"<%
|
148
|
+
"<% #{parent_variable}.with_#{slot_name}(#{param_str}) do %>"
|
155
149
|
end
|
156
150
|
else
|
157
151
|
component_class = "#{tag_name}Component"
|
158
|
-
|
159
|
-
param_str
|
160
|
-
|
161
|
-
if
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
else
|
167
|
-
"<%= render #{component_class}.new(#{param_str}) do %>"
|
168
|
-
end
|
169
|
-
else
|
170
|
-
stack << [tag_name, component_class, param_str, result.length, :component, nil, false, match.begin(0)]
|
171
|
-
result << if param_str.empty?
|
172
|
-
"<%= render #{component_class}.new do %>"
|
173
|
-
else
|
174
|
-
"<%= render #{component_class}.new(#{param_str}) do %>"
|
175
|
-
end
|
176
|
-
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
|
177
160
|
end
|
178
161
|
|
179
162
|
pos = match.end(0)
|
@@ -193,7 +176,7 @@ module EvcRails
|
|
193
176
|
end
|
194
177
|
|
195
178
|
# Find the matching opening tag (from the end)
|
196
|
-
matching_index = stack.rindex { |(tag_name,
|
179
|
+
matching_index = stack.rindex { |(tag_name, *)| tag_name == closing_tag_name }
|
197
180
|
if matching_index.nil?
|
198
181
|
line = line_number_at_position(source, match.begin(0))
|
199
182
|
col = column_number_at_position(source, match.begin(0))
|
@@ -201,18 +184,30 @@ module EvcRails
|
|
201
184
|
end
|
202
185
|
|
203
186
|
# Pop the matching opening tag
|
204
|
-
|
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
|
205
205
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
open_block_regex = /(<%= render #{component_class}\.new(?:\(.*?\))? do)( %>)/
|
210
|
-
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 %>"
|
211
209
|
end
|
212
210
|
|
213
|
-
# Add closing block
|
214
|
-
result << (type == :slot ? "<% end %>" : "<% end %>")
|
215
|
-
|
216
211
|
pos = match.end(0)
|
217
212
|
else
|
218
213
|
# No more tags, add remaining content
|
@@ -239,7 +234,24 @@ module EvcRails
|
|
239
234
|
erb_string
|
240
235
|
end
|
241
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
|
+
|
242
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
|
+
|
243
255
|
params = []
|
244
256
|
attributes_str.scan(attribute_regex) do |key, quoted_value, single_quoted_value, ruby_expression|
|
245
257
|
if ruby_expression
|
@@ -250,7 +262,7 @@ module EvcRails
|
|
250
262
|
params << "#{key}: \"#{single_quoted_value.gsub("'", "\\'")}\""
|
251
263
|
end
|
252
264
|
end
|
253
|
-
params
|
265
|
+
[params, as_variable]
|
254
266
|
end
|
255
267
|
end
|
256
268
|
end
|
data/lib/evc_rails/version.rb
CHANGED