evc_rails 0.2.3 → 0.3.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: '0083734e9b8f16100d769aa08c7393c7e60105a5c9bcbc3774c5955cc3c71def'
4
- data.tar.gz: 9099d7be89c3b5c6805fbd0b7b9316409c53a89ff62b1460391265c4a18fa97b
3
+ metadata.gz: cbedccd24baa2e896e5f5dc2f232b1a481ac3e5c9e82653f18d6d40d3b888f8d
4
+ data.tar.gz: 022e8e06c631aa3d6d52363377600a9b38870ffaa0506e1d5e732c0bf6859b73
5
5
  SHA512:
6
- metadata.gz: 742227dddbef882cf3a085bfd2c8642e4decc61a76bb83f858b55bb900d1064d09b9507beb3cbfbb62d5472a644793f4457057d23962cce134a6d75aa018bfca
7
- data.tar.gz: 13948d925ffc1419337d7d6a104be86bdae102577a133393978cb8c00eab2f7ee63bc627c6e15667c34671534f020588cf5e97297cbc82f6e0d79e800c9b416e
6
+ metadata.gz: b0eca6627b253da66c7cc9d44730f0c7dff6306ae6040bae1ac08b294d59723f72d0a3ec2ffc67736de566b7e6567803e2f17c14590c17e0ec1952d96005309a
7
+ data.tar.gz: 0a1268f2b06d7027134d9277b2a48b1372e6483e6c8c08fdd0e2aed1b36f4bf81286edbce9b760a18435ff64f11f2e6fa06a5ba9008af22efe9f24e950daa104
data/CHANGELOG.md CHANGED
@@ -5,7 +5,33 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.3.0] - 2024-12-19
9
+
10
+ ### Added
11
+
12
+ - **Enhanced slot nesting support**: Improved handling of deeply nested slots within components
13
+ - **Self-closing slot optimization**: Self-closing slots now render as method calls without blocks for better performance
14
+ - **Stack-based nesting logic**: More robust handling of complex nested component and slot structures
15
+
16
+ ### Changed
17
+
18
+ - **Slot variable naming**: Improved slot variable naming by stripping `with_` prefix for cleaner block variables
19
+ - **Nested slot context**: Enhanced logic to ensure slots use the nearest component variable unless directly nested inside another slot
20
+
21
+ ### Fixed
22
+
23
+ - **Block variable syntax**: Fixed Ruby block variable syntax to use `do |var|` instead of `|var| do`
24
+ - **Block variable yielding**: Corrected block variable yielding for complex nested structures
25
+ - **Slot method compatibility**: Ensured proper compatibility with ViewComponent's generated slot methods
26
+ - **Test suite improvements**: Updated all tests to reflect the new behavior and added comprehensive test coverage
27
+
28
+ ## [0.2.3] - 2024-12-19
29
+
30
+ ### Changed
31
+
32
+ - **Block variable yielding with `as`**: The block variable is now always yielded if the `as` attribute is present on a component tag, even if no `<With...>` slot tags are used inside the block. This makes advanced usage (such as calling plural slot methods directly) more ergonomic and predictable.
33
+ - **Documentation improvements**: Clarified the need for the `as` attribute when using plural slot methods with block variables, and improved wording for advanced usage in the README.
34
+ - **Test suite**: Added and updated tests to verify block variable yielding with `as` and to match the new behavior.
9
35
 
10
36
  ## [0.2.2] - 2024-12-19
11
37
 
@@ -77,11 +103,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
103
  - **Drop-in replacement**: Works as a replacement for `.erb` files
78
104
  - **Basic caching**: Template compilation caching for performance
79
105
 
80
- ---
81
-
82
106
  ## Version History Notes
83
107
 
84
108
  - **0.1.0**: Initial release with core functionality
85
109
  - **0.2.0**: Major feature addition with slots and namespaced components
86
110
  - **0.2.1**: Improved variable naming and slot method compatibility
87
111
  - **0.2.2**: Added boolean attribute shorthand for cleaner templates
112
+ - **0.2.3**: Added block variable yielding with `as` attribute for more ergonomic advanced usage
113
+ - **0.3.0**: Enhanced slot nesting support and improved block variable handling for complex component structures
data/README.md CHANGED
@@ -42,10 +42,11 @@ No component modifications required - just install and enjoy easier syntax!
42
42
  - **Block components**: `<Container>content</Container>`
43
43
  - **Attributes**: String, Ruby expressions, and multiple attributes
44
44
  - **Namespaced components**: `<UI::Button />`, `<Forms::Fields::TextField />`
45
- - **Slot support**: `<Card::Header>...</Card::Header>` with `renders_one` and `renders_many`
46
- - **Deep nesting**: Complex component hierarchies
45
+ - **Advanced slot support**: `<WithHeader>...</WithHeader>` with `renders_one` and `renders_many`, including complex nesting
46
+ - **Deep nesting**: Complex component hierarchies with proper block variable handling
47
47
  - **Production-ready caching** with Rails.cache integration
48
48
  - **Better error messages** with line numbers and column positions
49
+ - **Boolean attribute shorthand** for cleaner templates
49
50
 
50
51
  ## Installation
51
52
 
@@ -230,6 +231,52 @@ Becomes:
230
231
  <% end %>
231
232
  ```
232
233
 
234
+ #### Self-Closing Slots
235
+
236
+ You can also use self-closing slot tags when you don't need to pass content:
237
+
238
+ ```erb
239
+ <Card>
240
+ <WithHeader />
241
+ <WithBody>
242
+ <p>This is the body content.</p>
243
+ </WithBody>
244
+ </Card>
245
+ ```
246
+
247
+ Becomes:
248
+
249
+ ```erb
250
+ <%= render CardComponent.new do |card| %>
251
+ <% card.with_header %>
252
+ <% card.with_body do %>
253
+ <p>This is the body content.</p>
254
+ <% end %>
255
+ <% end %>
256
+ ```
257
+
258
+ #### Slot Attributes with Ruby Expressions
259
+
260
+ Slots can accept attributes and Ruby expressions:
261
+
262
+ ```erb
263
+ <Card>
264
+ <WithHeader user={@current_user} class="welcome-header">
265
+ Welcome, <%= @current_user.name %>
266
+ </WithHeader>
267
+ </Card>
268
+ ```
269
+
270
+ Becomes:
271
+
272
+ ```erb
273
+ <%= render CardComponent.new do |card| %>
274
+ <% card.with_header(user: @current_user, class: "welcome-header") do %>
275
+ Welcome, <%= @current_user.name %>
276
+ <% end %>
277
+ <% end %>
278
+ ```
279
+
233
280
  #### Multiple Slots (`renders_many`)
234
281
 
235
282
  ```ruby
@@ -326,6 +373,42 @@ This is equivalent to the ERB version:
326
373
 
327
374
  You can use this approach for any plural slot method generated by `renders_many`. The block variable (e.g., `navigation`) is always available inside the component block when you use the `as` attribute, even if there are no `<With...>` slot tags present.
328
375
 
376
+ #### Complex Nested Slot Structures
377
+
378
+ EVC handles complex nested slot structures with proper block variable scoping:
379
+
380
+ ```erb
381
+ <Navigation>
382
+ <WithLink href={learning_path} text="Learning Path" />
383
+ <WithLink href={courses_path} text="All Courses" />
384
+ <WithLink text="Reports">
385
+ <WithSublink href={reports_users_path} text="Users" />
386
+ <WithSublink href={reports_activity_path} text="Activity" />
387
+ </WithLink>
388
+ <WithFooter>
389
+ <div>Footer content</div>
390
+ </WithFooter>
391
+ </Navigation>
392
+ ```
393
+
394
+ Becomes:
395
+
396
+ ```erb
397
+ <%= render NavigationComponent.new do |navigation| %>
398
+ <% navigation.with_link(href: learning_path, text: "Learning Path") %>
399
+ <% navigation.with_link(href: courses_path, text: "All Courses") %>
400
+ <% navigation.with_link(text: "Reports") do |link| %>
401
+ <% link.with_sublink(href: reports_users_path, text: "Users") %>
402
+ <% link.with_sublink(href: reports_activity_path, text: "Activity") %>
403
+ <% end %>
404
+ <% navigation.with_footer do %>
405
+ <div>Footer content</div>
406
+ <% end %>
407
+ <% end %>
408
+ ```
409
+
410
+ This demonstrates how EVC properly handles nested slots with correct block variable scoping - the inner `WithSublink` slots use the `link` variable from their parent `WithLink` slot.
411
+
329
412
  ### Mixed Content
330
413
 
331
414
  You can mix regular HTML, ERB, and component tags:
@@ -113,6 +113,7 @@ module EvcRails
113
113
 
114
114
  # Determine if this is a slot (e.g., WithHeader, WithPost)
115
115
  parent_component = stack.reverse.find { |item| item[4] == :component }
116
+ parent_slot_index = stack.rindex { |item| item[4] == :slot }
116
117
  is_slot = false
117
118
  slot_name = nil
118
119
 
@@ -121,15 +122,22 @@ module EvcRails
121
122
  # Convert CamelCase slot name to snake_case
122
123
  slot_name = tag_name[4..].gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
123
124
  parent_component[6] = true # Mark parent as having a slot
125
+ # If this slot is nested inside another slot, mark the parent slot as having nested slots
126
+ stack[parent_slot_index][6] = true if parent_slot_index
124
127
  end
125
128
 
126
129
  if is_self_closing
127
130
  if is_slot
128
- parent_variable = parent_component[8]
131
+ # For self-closing slots, use the most recent slot variable if present, otherwise the component variable
132
+ parent_variable = if stack.last && stack.last[4] == :slot
133
+ stack.last[8]
134
+ else
135
+ parent_component[8]
136
+ end
129
137
  result << if param_str.empty?
130
- "<% #{parent_variable}.with_#{slot_name} do %><% end %>"
138
+ "<% #{parent_variable}.with_#{slot_name} %>"
131
139
  else
132
- "<% #{parent_variable}.with_#{slot_name}(#{param_str}) do %><% end %>"
140
+ "<% #{parent_variable}.with_#{slot_name}(#{param_str}) %>"
133
141
  end
134
142
  else
135
143
  component_class = "#{tag_name}Component"
@@ -140,8 +148,20 @@ module EvcRails
140
148
  end
141
149
  end
142
150
  elsif is_slot
143
- parent_variable = parent_component[8]
144
- stack << [tag_name, nil, param_str, result.length, :slot, slot_name, false, match.begin(0), nil]
151
+ # For block slots, determine the parent variable
152
+ if stack.last && stack.last[4] == :slot
153
+ # The most recent stack entry is a slot; use its variable
154
+ parent_variable = stack.last[8]
155
+ # Mark the parent slot as having nested slots
156
+ stack.last[6] = true
157
+ else
158
+ # Otherwise, use the nearest component variable
159
+ parent_variable = parent_component[8]
160
+ end
161
+ # Generate a variable name for this slot (strip 'with_' prefix)
162
+ slot_variable_name = component_variable_name(tag_name.sub(/^With/, ""))
163
+ stack << [tag_name, nil, param_str, result.length, :slot, slot_name, false, match.begin(0),
164
+ slot_variable_name]
145
165
  result << if param_str.empty?
146
166
  "<% #{parent_variable}.with_#{slot_name} do %>"
147
167
  else
@@ -150,7 +170,6 @@ module EvcRails
150
170
  else
151
171
  component_class = "#{tag_name}Component"
152
172
  variable_name = as_variable || component_variable_name(tag_name)
153
- # If as_variable is present, force block variable to be yielded
154
173
  force_block_variable = !as_variable.nil?
155
174
  stack << [tag_name, component_class, param_str, result.length, :component, nil, false, match.begin(0),
156
175
  variable_name, force_block_variable]
@@ -199,14 +218,23 @@ module EvcRails
199
218
  relevant_part = result[start_pos..-1]
200
219
  match_for_insertion = /( do)( %>)/.match(relevant_part)
201
220
  if match_for_insertion
202
- # Insert the variable name just before the ` do`
203
- insertion_point = start_pos + match_for_insertion.begin(1)
221
+ # Insert the variable name just after the ` do`
222
+ insertion_point = start_pos + match_for_insertion.begin(1) + match_for_insertion[1].length
204
223
  result.insert(insertion_point, " |#{variable_name}|")
205
224
  end
206
225
  end
207
226
 
208
227
  result << "<% end %>"
209
228
  else # It's a slot
229
+ _tag_name, _component_class, param_str, start_pos, _type, _slot_name, has_nested_slots, _open_pos, slot_variable_name = open_tag_data
230
+ if has_nested_slots
231
+ relevant_part = result[start_pos..-1]
232
+ match_for_insertion = /( do)( %>)/.match(relevant_part)
233
+ if match_for_insertion
234
+ insertion_point = start_pos + match_for_insertion.begin(1) + match_for_insertion[1].length
235
+ result.insert(insertion_point, " |#{slot_variable_name}|")
236
+ end
237
+ end
210
238
  result << "<% end %>"
211
239
  end
212
240
 
@@ -255,17 +283,85 @@ module EvcRails
255
283
  end.strip
256
284
 
257
285
  params = []
258
- attributes_str.scan(attribute_regex) do |key, quoted_value, single_quoted_value, ruby_expression|
259
- params << if ruby_expression
260
- "#{key}: #{ruby_expression}"
261
- elsif quoted_value
262
- "#{key}: \"#{quoted_value.gsub('"', '\\"')}\""
263
- elsif single_quoted_value
264
- "#{key}: \"#{single_quoted_value.gsub("'", "\\'")}\""
265
- else
266
- # Standalone attribute (no value) - treat as boolean true
267
- "#{key}: true"
268
- end
286
+ str = attributes_str.dup
287
+ until str.nil? || str.empty?
288
+ str = "" if str.nil?
289
+ str.lstrip!
290
+ str = "" if str.nil?
291
+ break if str.empty?
292
+ # Match key
293
+ break unless str =~ /\A(\w+)/
294
+
295
+ key = ::Regexp.last_match(1)
296
+ str = str[::Regexp.last_match(0).length..-1]
297
+ str = "" if str.nil?
298
+ str.lstrip!
299
+ str = "" if str.nil?
300
+ if str.start_with?("=")
301
+ str = str[1..-1]
302
+ str = "" if str.nil?
303
+ str.lstrip!
304
+ str = "" if str.nil?
305
+ if str.start_with?("{")
306
+ # Parse balanced curly braces
307
+ depth = 0
308
+ i = 0
309
+ found = false
310
+ while true
311
+ str = "" if str.nil?
312
+ break if str.empty? || i >= str.length
313
+
314
+ c = str[i]
315
+ if c == "{"
316
+ depth += 1
317
+ elsif c == "}"
318
+ depth -= 1
319
+ if depth == 0
320
+ found = true
321
+ break
322
+ end
323
+ end
324
+ i += 1
325
+ end
326
+ if found
327
+ ruby_expr = str[1...i] # skip opening '{', up to before closing '}'
328
+ str = str[(i + 1)..-1]
329
+ str = "" if str.nil?
330
+ params << "#{key}: #{ruby_expr}"
331
+ else
332
+ # Unbalanced braces, treat as error or fallback
333
+ params << "#{key}: true"
334
+ str = ""
335
+ end
336
+ elsif str.start_with?("\"")
337
+ # Double-quoted string
338
+ if str =~ /\A"([^"]*)"/
339
+ str = str[::Regexp.last_match(0).length..-1]
340
+ str = "" if str.nil?
341
+ params << "#{key}: \"#{::Regexp.last_match(1).gsub('"', '\\"')}\""
342
+ else
343
+ params << "#{key}: true"
344
+ str = ""
345
+ end
346
+ elsif str.start_with?("'")
347
+ # Single-quoted string
348
+ if str =~ /\A'([^']*)'/
349
+ str = str[::Regexp.last_match(0).length..-1]
350
+ str = "" if str.nil?
351
+ params << "#{key}: \"#{::Regexp.last_match(1).gsub("'", "\\'")}\""
352
+ else
353
+ params << "#{key}: true"
354
+ str = ""
355
+ end
356
+ else
357
+ # Unquoted value or malformed, treat as boolean true
358
+ params << "#{key}: true"
359
+ str = ""
360
+ end
361
+ else
362
+ # Standalone attribute (no value) - treat as boolean true
363
+ params << "#{key}: true"
364
+ end
269
365
  end
270
366
  [params, as_variable]
271
367
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EvcRails
4
- VERSION = "0.2.3"
4
+ VERSION = "0.3.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.2.3
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - scttymn