evc_rails 0.2.0 → 0.2.2

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: 1595c9c520d1639384e46e39fce8f611edbe3b27707a9f99ab412bb25a1741f0
4
- data.tar.gz: 23ec6c266ec28a617b0d39d255f33fd5cff58c3db6461dd4382237b8d1989ef2
3
+ metadata.gz: fb8d4d59a3526f01f08e3c152ff5abe083e1a2bf1e66ab929db733356b56c822
4
+ data.tar.gz: 07c8c5688a9ad4ddb78f34ca174f478e9103b656c09223c86aed522746b0b299
5
5
  SHA512:
6
- metadata.gz: 78e5db590a2dd0b84df663fc836b51c24156bf6f5af40d278f8a5a9ec2cd89aa9ddf1fc83a957613ecc980ebfd54e2fa4d84c33e48f910d2c48514d0869789b7
7
- data.tar.gz: c2765e0f368bce11454d9bf95461739a62ef0b89a1d1940c6e069f305499a20e4aeba6dabb4bd017ebc4f1bbefbd60294e0f8ecf53c24e377662a04d16e21cce
6
+ metadata.gz: 6e429ae91cf3b0e3ac7b47001e164e65f6c4c6bd45f6c11f4258bb0a3c7cf77fa04594a4f493b887f434809ff11174996b09099273ac1bfac50897b002489158
7
+ data.tar.gz: 57c5fb478a987d55dd24e22afda9198f9af0b11056ac3ab2bbf66a52ec420251cae33cfa596caa5074015e241cd5d6754b2f95a04c46ba1951c0190694f92e64
data/README.md CHANGED
@@ -154,6 +154,48 @@ 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
+ #### Boolean Attribute Shorthand
180
+
181
+ You can use HTML-style boolean attributes in EVC. If you specify an attribute with no value, it will be passed as `true` to your component initializer. This makes templates more concise and readable:
182
+
183
+ ```erb
184
+ <Button disabled required />
185
+ ```
186
+
187
+ is equivalent to:
188
+
189
+ ```erb
190
+ <%= render ButtonComponent.new(disabled: true, required: true) %>
191
+ ```
192
+
193
+ This works for any boolean parameter your component defines.
194
+
195
+ #### When a Block Variable is Yielded
196
+
197
+ 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.
198
+
157
199
  #### Single Slots (`renders_one`)
158
200
 
159
201
  ```ruby
@@ -178,11 +220,11 @@ end
178
220
  Becomes:
179
221
 
180
222
  ```erb
181
- <%= render CardComponent.new do |c| %>
182
- <% c.header do %>
223
+ <%= render CardComponent.new do |card| %>
224
+ <% card.with_header do %>
183
225
  <h1>Welcome</h1>
184
226
  <% end %>
185
- <% c.body do %>
227
+ <% card.with_body do %>
186
228
  <p>This is the body content.</p>
187
229
  <% end %>
188
230
  <% end %>
@@ -199,68 +241,62 @@ end
199
241
 
200
242
  ```erb
201
243
  <List>
202
- <WithItem>Item 1</WithItem>
203
- <WithItem>Item 2</WithItem>
204
- <WithItem>Item 3</WithItem>
244
+ <% @todo_items.each do |item| %>
245
+ <WithItem>
246
+ <span class="<%= item.completed? ? 'line-through' : '' %>">
247
+ <%= item.title %>
248
+ </span>
249
+ </WithItem>
250
+ <% end %>
205
251
  </List>
206
252
  ```
207
253
 
208
254
  Becomes:
209
255
 
210
256
  ```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 %>
257
+ <%= render ListComponent.new do |list| %>
258
+ <% @todo_items.each do |item| %>
259
+ <% list.with_item do %>
260
+ <span class="<%= item.completed? ? 'line-through' : '' %>">
261
+ <%= item.title %>
262
+ </span>
263
+ <% end %>
264
+ <% end %>
215
265
  <% end %>
216
266
  ```
217
267
 
218
- #### Complex Slot Examples
268
+ #### Custom Variable Naming with `as`
219
269
 
220
- ```erb
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
- ```
270
+ For clarity or to resolve ambiguity when nesting components of the same type, you can provide a custom variable name with the `as` attribute.
271
+
272
+ ```ruby
273
+ # app/components/card_component.rb
274
+ class CardComponent < ViewComponent::Base
275
+ renders_one :header
233
276
 
234
- #### Backward Compatibility
277
+ attr_reader :title
235
278
 
236
- The old `Component::slotname` syntax is still supported for backward compatibility:
279
+ def initialize(title: "Default Title")
280
+ @title = title
281
+ end
282
+ end
283
+ ```
237
284
 
238
285
  ```erb
239
- <Card>
240
- <Card::header>Title</Card::header>
241
- <Card::body>Content</Card::body>
286
+ <Card as="outer_card" title="Outer Card">
287
+ <WithHeader>
288
+ <h2><%= outer_card.title %></h2>
289
+ <Card as="inner_card" title="Inner Card">
290
+ <WithHeader>
291
+ <h3><%= inner_card.title %></h3>
292
+ <p>Outer card title from inner scope: <%= outer_card.title %></p>
293
+ </WithHeader>
294
+ </Card>
295
+ </WithHeader>
242
296
  </Card>
243
297
  ```
244
298
 
245
- ### Complex Nesting
246
-
247
- ```erb
248
- <UI::Card>
249
- <h2 class="text-2xl font-semibold">Dashboard</h2>
250
-
251
- <UI::Grid cols="3" gap="md">
252
- <UI::Card shadow="sm">
253
- <p class="text-center">Widget 1</p>
254
- </UI::Card>
255
- <UI::Card shadow="sm">
256
- <p class="text-center">Widget 2</p>
257
- </UI::Card>
258
- <UI::Card shadow="sm">
259
- <p class="text-center">Widget 3</p>
260
- </UI::Card>
261
- </UI::Grid>
262
- </UI::Card>
263
- ```
299
+ This generates distinct variables, `outer_card` and `inner_card`, allowing you to access the context of each component without collision.
264
300
 
265
301
  ### Mixed Content
266
302
 
@@ -13,7 +13,7 @@ module EvcRails
13
13
  CLOSE_TAG_REGEX = %r{</([A-Z][a-zA-Z0-9_]*(?:::[A-Z][a-zA-Z0-9_]*)*)>}
14
14
 
15
15
  # Regex for attributes
16
- ATTRIBUTE_REGEX = /(\w+)=(?:"([^"]*)"|'([^']*)'|\{([^}]*)\})/
16
+ ATTRIBUTE_REGEX = /(\w+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|\{([^}]*)\}))?/
17
17
 
18
18
  # Cache for compiled templates
19
19
  @template_cache = {}
@@ -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, or 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
-
120
- # Check for WithSlotName syntax (e.g., WithHeader, WithPost)
121
- if tag_name.start_with?("With")
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
- params = parse_attributes(attributes_str, attribute_regex)
140
- param_str = params.join(", ")
128
+ parent_variable = parent_component[8]
141
129
  result << if param_str.empty?
142
- "<% c.#{slot_name} do %><% end %>"
130
+ "<% #{parent_variable}.with_#{slot_name} do %><% end %>"
143
131
  else
144
- "<% c.#{slot_name}(#{param_str}) do %><% end %>"
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
- params = parse_attributes(attributes_str, attribute_regex)
158
- param_str = params.join(", ")
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
- "<% c.#{slot_name} do %>"
146
+ "<% #{parent_variable}.with_#{slot_name} do %>"
162
147
  else
163
- "<% c.#{slot_name}(#{param_str}) do %>"
148
+ "<% #{parent_variable}.with_#{slot_name}(#{param_str}) do %>"
164
149
  end
165
150
  else
166
151
  component_class = "#{tag_name}Component"
167
- params = parse_attributes(attributes_str, attribute_regex)
168
- param_str = params.join(", ")
169
- # If this is the outermost component, add |c| for slot support only if a slot is used
170
- if stack.empty?
171
- 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]
172
- # We'll patch in |c| at close if needed
173
- result << if param_str.empty?
174
- "<%= render #{component_class}.new do %>"
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, _, _, _, _, _, _, _)| tag_name == closing_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
- 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
214
205
 
215
- # Patch in |c| for top-level component if a slot was used
216
- if type == :component && stack.empty? && slot_used
217
- # Find the opening block and insert |c|
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,18 +234,38 @@ 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
- if ruby_expression
255
- params << "#{key}: #{ruby_expression}"
256
- elsif quoted_value
257
- params << "#{key}: \"#{quoted_value.gsub('"', '\\"')}\""
258
- elsif single_quoted_value
259
- params << "#{key}: \"#{single_quoted_value.gsub("'", "\\'")}\""
260
- end
257
+ params << if ruby_expression
258
+ "#{key}: #{ruby_expression}"
259
+ elsif quoted_value
260
+ "#{key}: \"#{quoted_value.gsub('"', '\\"')}\""
261
+ elsif single_quoted_value
262
+ "#{key}: \"#{single_quoted_value.gsub("'", "\\'")}\""
263
+ else
264
+ # Standalone attribute (no value) - treat as boolean true
265
+ "#{key}: true"
266
+ end
261
267
  end
262
- params
268
+ [params, as_variable]
263
269
  end
264
270
  end
265
271
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EvcRails
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
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.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - scttymn