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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e15aa81852a8394bcc2a904cff923f3d5a5f588c618a2c0c6112f33203b0bd2c
4
- data.tar.gz: cdd1c962213f4c2a6593a522ac2aee9cc793858f4759c218242169a26fd40ace
3
+ metadata.gz: 5fa808d0d2dcb0efe2ff7a1807cbfabfb62995594731b066eb718e3aac9e1ee3
4
+ data.tar.gz: fc944ac269042805ec1bb1c9175a4646ac56ed4a3ac2461c6061c211c831b608
5
5
  SHA512:
6
- metadata.gz: 91a5a791151b6e2290dc3a20a8e95980a0821bf2526314a3b07724e85dda06f11a6149bfd9d808eb4af9e67da90d80616ddb9fa799d2c61ebc0e908e4cb627ce
7
- data.tar.gz: 0bc11c9d4026d5357b5d86089884eff7d30bf82a66823ec164aa3600cbf2ca6774c835c18175a132225acd9204cd95879f76a5698fbc80e355a46ba1ac771e85
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
- <Card::Header>
195
+ <WithHeader>
170
196
  <h1>Welcome</h1>
171
- </Card::Header>
172
- <Card::Body>
197
+ </WithHeader>
198
+ <WithBody>
173
199
  <p>This is the body content.</p>
174
- </Card::Body>
200
+ </WithBody>
175
201
  </Card>
176
202
  ```
177
203
 
178
204
  Becomes:
179
205
 
180
206
  ```erb
181
- <%= render CardComponent.new do |c| %>
182
- <% c.header do %>
207
+ <%= render CardComponent.new do |card| %>
208
+ <% card.with_header do %>
183
209
  <h1>Welcome</h1>
184
210
  <% end %>
185
- <% c.body do %>
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
- <List::Item>Item 1</List::Item>
203
- <List::Item>Item 2</List::Item>
204
- <List::Item>Item 3</List::Item>
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 |c| %>
212
- <% c.item do %>Item 1<% end %>
213
- <% c.item do %>Item 2<% end %>
214
- <% c.item do %>Item 3<% end %>
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
- ### Complex Nesting
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
- <UI::Card>
222
- <h2 class="text-2xl font-semibold">Dashboard</h2>
223
-
224
- <UI::Grid cols="3" gap="md">
225
- <UI::Card shadow="sm">
226
- <p class="text-center">Widget 1</p>
227
- </UI::Card>
228
- <UI::Card shadow="sm">
229
- <p class="text-center">Widget 2</p>
230
- </UI::Card>
231
- <UI::Card shadow="sm">
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., Card::Header inside Card)
113
- parent = stack.last
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
- slot_parent = nil
117
- if parent
118
- parent_tag = parent[0]
119
- if tag_name.start_with?("#{parent_tag}::")
120
- is_slot = true
121
- slot_name = tag_name.split("::").last.downcase
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
- params = parse_attributes(attributes_str, attribute_regex)
131
- param_str = params.join(", ")
128
+ parent_variable = parent_component[8]
132
129
  result << if param_str.empty?
133
- "<% c.#{slot_name} do %><% end %>"
130
+ "<% #{parent_variable}.with_#{slot_name} do %><% end %>"
134
131
  else
135
- "<% c.#{slot_name}(#{param_str}) do %><% end %>"
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
- params = parse_attributes(attributes_str, attribute_regex)
149
- param_str = params.join(", ")
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
- "<% c.#{slot_name} do %>"
146
+ "<% #{parent_variable}.with_#{slot_name} do %>"
153
147
  else
154
- "<% c.#{slot_name}(#{param_str}) do %>"
148
+ "<% #{parent_variable}.with_#{slot_name}(#{param_str}) do %>"
155
149
  end
156
150
  else
157
151
  component_class = "#{tag_name}Component"
158
- params = parse_attributes(attributes_str, attribute_regex)
159
- param_str = params.join(", ")
160
- # If this is the outermost component, add |c| for slot support only if a slot is used
161
- if stack.empty?
162
- stack << [tag_name, component_class, param_str, result.length, :component, nil, false, match.begin(0)] # [tag_name, class, params, pos, type, slot_name, slot_used, open_pos]
163
- # We'll patch in |c| at close if needed
164
- result << if param_str.empty?
165
- "<%= render #{component_class}.new do %>"
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, _, _, _, _, _, _, _)| tag_name == closing_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
- tag_name, component_class, param_str, start_pos, type, slot_name, slot_used, open_pos = stack.delete_at(matching_index)
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
- # Patch in |c| for top-level component if a slot was used
207
- if type == :component && stack.empty? && slot_used
208
- # Find the opening block and insert |c|
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EvcRails
4
- VERSION = "0.1.4"
4
+ VERSION = "0.2.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: evc_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - scttymn