slotify 0.0.1 → 0.0.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: 9401d13b9a845572abccc1cd38362bb6facfb5394568a74345d856abc6db80ea
4
- data.tar.gz: bd8c9df4efe6ee8d40d7967f06326b9950d160c824828c11537f5c44f63f200c
3
+ metadata.gz: c0623930b397a09cafd4f47da222d46595e013da9987ee1491be5c501689e095
4
+ data.tar.gz: 29f4f8d8427932b1bf85ed6205262e2dbe8196281951bec74042b2f35967928c
5
5
  SHA512:
6
- metadata.gz: 1af7661c421919e5bc90069a75781075e27b9f97d1d767148f5ece3abe773dc1b206ea59435e511c7e1b1805a34124b19b4e1d8354fc7cf70e27dfbcd4e7445a
7
- data.tar.gz: 2c3f889e7f55d65af6be315c9b013a21ab63b8cee3c9304cf3ebd947b3324c899c0830e5ea420d1aac7bda0d54603fe69d97eb87fdc765c1a8c0f8f70ec9af69
6
+ metadata.gz: b14613a0d5522daf91565b60860c24df657a67807751034aab3a91d70888edeb0bcf381120e958fd42d9cf04290cc7b65964b913ea6a66339d6e375922109608
7
+ data.tar.gz: 754d99784a31195603eaa9a4c9e91dc4cf4575e7987f181163ff6062251cfe50c7af5ff75f2133f552d7b57aaedc327e43c5d0fe74253964528503ea7fa54404
data/README.md CHANGED
@@ -1,27 +1,28 @@
1
1
  <img src=".github/assets/slotify_wordmark.svg" width="200">
2
2
 
3
+ <p><a href="https://rubygems.org/gems/slotify"><img src="https://img.shields.io/gem/v/slotify" alt="Gem version"></a>
4
+ <a href="https://github.com/allmarkedup/slotify/actions/workflows/ci.yml"><img src="https://github.com/allmarkedup/slotify/actions/workflows/ci.yml/badge.svg" alt="CI status"></a></p>
3
5
 
6
+ ## Superpowered slots for ActionView partials
4
7
 
5
- ### Superpowered slots for ActionView partials
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- Slotify adds an unobtrusive (but powerful!) **content slot API** to ActionView partials.
8
+ Slotify adds an unobtrusive (but powerful!) **content slot API** to ActionView partials.
12
9
 
13
10
  Slots are a convenient way to pass blocks of content in to a partial without having to resort to ugly `<% capture do ... end %>` workarounds or unscoped (global) `<% content_for :foo %>` declarations.
14
11
 
15
12
  Slotified partials are a great way to build components in a Rails app without the additional overhead and learning curve of libraries like [ViewComponent](https://viewcomponent.org/) or [Phlex](https://www.phlex.fun/).
16
13
 
14
+ > [!CAUTION]
15
+ > Slotify is still in a early stage of development.
16
+ The documentation is still quite sparse and the API could change at any point prior to a `v1.0` release.
17
+
17
18
  ###
18
19
 
19
20
  ## Slotify basics
20
21
 
21
- Slotify slots are defined using a **[strict locals](https://guides.rubyonrails.org/action_view_overview.html#strict-locals)-style magic comment** at the top of partial templates ([more details here](#defining-slots)).
22
+ Slotify slots are defined using a **[strict locals](https://guides.rubyonrails.org/action_view_overview.html#strict-locals)-style magic comment** at the top of **partial templates** ([more details here](#defining-slots)).
22
23
 
23
24
  ```erb
24
- <%# slots: (slot_name: "default value", optional_slot_name: nil, required_slot_name:) -%>
25
+ <%# slots: (title:, body: nil, theme: "default") -%>
25
26
  ```
26
27
 
27
28
  Slot content is accessed via **standard local variables** within the partial. So a simple, slot-enabled `article` partial template might look something like this:
@@ -29,10 +30,10 @@ Slot content is accessed via **standard local variables** within the partial. So
29
30
  ```erb
30
31
  <!-- _article.html.erb -->
31
32
 
32
- <%# slots: (heading: "Default title", body: nil) -%>
33
+ <%# slots: (title: "Default title", body: nil) -%>
33
34
 
34
35
  <article>
35
- <h1><%= heading %></h1>
36
+ <h1><%= title %></h1>
36
37
  <% if body.present? %>
37
38
  <div>
38
39
  <%= body %>
@@ -46,11 +47,11 @@ Slot content is accessed via **standard local variables** within the partial. So
46
47
 
47
48
  When the partial is rendered, a special `partial` object is yielded as an argument to the block. Slot content is set by calling the appropriate `#with_<slot_name>` methods on this partial object.
48
49
 
49
- For example, here our `article` partial is being rendered with content for the `heading` and `body` slots that were defined above:
50
+ For example, here our `article` partial is being rendered with content for the `title` and `body` slots that were defined above:
50
51
 
51
52
  ```erb
52
53
  <%= render "article" do |partial| %>
53
- <% partial.with_heading "This is a title" %>
54
+ <% partial.with_title "This is a title" %>
54
55
  <% partial.with_body do %>
55
56
  <p>You can use <%= tag.strong "markup" %> within slot content blocks without
56
57
  having to worry about marking the output as <code>html_safe</code> later.</p>
@@ -61,7 +62,82 @@ For example, here our `article` partial is being rendered with content for the `
61
62
  > [!NOTE]
62
63
  > _If you've ever used [ViewComponent](https://viewcomponent.org) then the above code should also feel quite familiar to you - it's pretty much the same syntax used to provide content to [component slots](https://viewcomponent.org/guide/slots.html)._
63
64
 
64
- But this example just scratches the surface of what Slotify slots can do! Read on to learn more (or [jump to a more full-featured example here](#full-example)).
65
+ But this example just scratches the surface of what Slotify slots can do. Have a look at the more full-featured example below or jump to [the usage information](#usage).
66
+
67
+ <details>
68
+ <summary><h4>More full-featured example</h4></summary>
69
+
70
+ ```erb
71
+ <!-- views/_example.html.erb -->
72
+
73
+ <%# locals: (id:) -%>
74
+ <%# slots: (title: "Example title", lists: nil, quotes: nil, website_link:) -%>
75
+
76
+ <%= tag.section id: do %>
77
+ <h1 class="example-title">
78
+ <%= title %>
79
+ </h1>
80
+
81
+ <p>Example link: <%= link_to website_link, data: {controller: "external-link"} %></p>
82
+
83
+ <%= render lists, title: "Default title" %>
84
+
85
+ <% if quotes.any? %>
86
+ <h3>Quotes</h3>
87
+ <% quotes.each do |quote| %>
88
+ <blockquote <%= quote.options.except(:citation) %>>
89
+ <%= quote %>
90
+ <%== "&mdash; #{tag.cite(quote.options.citation)}" if quote.options.citation.present? %>
91
+ </blockquote>
92
+ <% end %>
93
+ <% end %>
94
+ <% end %>
95
+ ```
96
+
97
+ ```erb
98
+ <!-- views/_list.html.erb -->
99
+
100
+ <%# locals: (title:) -%>
101
+ <%# slots: (items: nil) -%>
102
+
103
+ <h3><%= title %></h3>
104
+
105
+ <% if items.any? %>
106
+ <%= tag.ul class: "list" do %>
107
+ <%= content_tag :li, items, class: "list-item" %>
108
+ <% end %>
109
+ <% end %>
110
+ ```
111
+
112
+ ```erb
113
+ <!-- views/slotify.html.erb -->
114
+
115
+ <%= render "example", id: "slotify-example" do |partial| %>
116
+ <% partial.with_subtitle do %>
117
+ This is the <%= tag.em "subtitle" %>
118
+ <% end %>
119
+
120
+ <% partial.with_website_link "example.com", "https://example.com", target: "_blank", data: {controller: "preview-link"} %>
121
+
122
+ <% partial.with_list do |list| %>
123
+ <% list.with_item "first thing" %>
124
+ <% list.with_item "second thing", class: "text-green-700" %>
125
+ <% list.with_item "third thing" %>
126
+ <% end %>
127
+
128
+ <% partial.with_quote citation: "A. Person", class: "text-lg" do %>
129
+ <p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
130
+ <% end %>
131
+
132
+ <% partial.with_quote do %>
133
+ <p>Non quos explicabo eius hic quaerat laboriosam incidunt numquam.</p>
134
+ <% end %>
135
+ <% end %>
136
+ ```
137
+
138
+ </details>
139
+
140
+ ---
65
141
 
66
142
  ## Usage
67
143
 
@@ -71,7 +147,7 @@ But this example just scratches the surface of what Slotify slots can do! Read o
71
147
  Slots are defined using a [strict locals](https://guides.rubyonrails.org/action_view_overview.html#strict-locals)-style magic comment at the top of the partial template. The `slots:` signature uses the same syntax as standard Ruby method signatures:
72
148
 
73
149
  ```erb
74
- <%# slots: (title:, summary: "No summary available", author: nil) -%>
150
+ <%# slots: (title:, body: "No content available", author: nil) -%>
75
151
  ```
76
152
 
77
153
  #### Required slots
@@ -110,11 +186,11 @@ either above or below the `slots` definition.
110
186
  <!-- _article.html.erb -->
111
187
 
112
188
  <%# locals: (title:) -%>
113
- <%# slots: (summary: "No summary available") -%>
189
+ <%# slots: (body: "No content available") -%>
114
190
 
115
191
  <article>
116
192
  <h1><%= title %></h1>
117
- <div><%= summary %></div>
193
+ <div><%= body %></div>
118
194
  </article>
119
195
  ```
120
196
 
@@ -122,28 +198,72 @@ Locals are provided when rendering the partial in the usual way.
122
198
 
123
199
  ```erb
124
200
  <%= render "article", title: "Article title here" do |partial| %>
125
- <% partial.with_summary do %>
126
- <p>Summary content here...</p>
201
+ <% partial.with_body do %>
202
+ <p>Body content here...</p>
127
203
  <% end %>
128
204
  <% end %>
129
205
  ```
130
206
 
131
- ### Slot content and options
207
+ ### Setting slot values
132
208
 
133
- Content is passed into slots using dynamically generated `partial#with_<slot_name>` methods.
209
+ Content is passed into slots using dynamically generated `partial#with_<slot_name>` writer methods.
134
210
 
135
211
  Content can be provided as either the **first argument** or **as a block** when calling these methods at render time.
136
- The following `#with_title` calls are both equivalent:
212
+ The following two examples are equivalent:
137
213
 
138
214
  ```erb
139
215
  <%= render "example" do |partial| %>
140
216
  <% partial.with_title "Title passed as argument" %>
217
+ <% end %>
218
+ ```
219
+
220
+ ```erb
221
+ <%= render "example" do |partial| %>
141
222
  <% partial.with_title do %>
142
223
  Title passed as block content
143
224
  <% end %>
144
225
  <% end %>
145
226
  ```
146
227
 
228
+ > [!TIP]
229
+ > Block content is generally better suited for longer-form content containing HTML tags because it will not need to be marked
230
+ as `html_safe` when used in the partial template.
231
+
232
+ The content will be available as a local variable in the partial template whichever way it is provided.
233
+
234
+ ```erb
235
+ <%# slots: (title:) -%>
236
+ <h1><%= title %></h1>
237
+ ```
238
+
239
+ ### Slot options
240
+
241
+ The slot value writer methods also accept optional arbitrary keyword arguments.
242
+ These can then be accessed in the partial template via the `.options` method on the slot variable.
243
+
244
+ ```erb
245
+ <%= render "example" do |partial| %>
246
+ <% partial.with_title "The title", class: "color-hotpink", data: {controller: "fancy-title"} %>
247
+ <% end %>
248
+ ```
249
+
250
+ ```erb
251
+ <%# slots: (title:) -%>
252
+
253
+ <%= title.options.keys %> <!-- [:class, :data] -->
254
+ <%= title %> <!-- The title -->
255
+ ```
256
+
257
+ Slot options can be useful for providing tag attributes when rendering slot content or rendering variants
258
+ of a slot based on an option value.
259
+
260
+ When rendered as a string the options are passed through the Rails `tag.attributes` helper to generate an HTML tag attributes string:
261
+
262
+ ```erb
263
+ <h1 <%= title.options %>><%= title %></h1>
264
+ <!-- <h1 class="color-hotpink" data-controller="fancy-title">The title</h1> -->
265
+ ```
266
+
147
267
  ### Slot types
148
268
 
149
269
  There are two types of slots.
@@ -194,7 +314,9 @@ Multiple-value slots are defined using a **plural** slot name:
194
314
  ```
195
315
 
196
316
  Multiple-value slots can be called as many times as needed
197
- and their corresponding template variable represents an array of values:
317
+ and their corresponding template variable represents an array of values.
318
+
319
+ The slot writer methods for multiple-value slots use the **singluar form** of the slot name (e.g. `#with_item` for the `items` slot).
198
320
 
199
321
  ```erb
200
322
  <%= render "example" do |partial| %>
@@ -218,17 +340,134 @@ and their corresponding template variable represents an array of values:
218
340
  </ul>
219
341
  ```
220
342
 
221
- ### Passing slot content to helpers
343
+ ### Using slots with helpers
222
344
 
223
345
  > _Docs coming soon..._
224
346
 
347
+ ```erb
348
+ <% partial.with_title "The title", class: "color-hotpink" %>
349
+ <% partial.with_website_link "Example website", "https://example.com", data: {controller: "external-link"} %>
350
+
351
+ <% partial.with_item "Item one" %>
352
+ <% partial.with_item "Item two", class: "highlight" %>
353
+ ```
354
+
355
+ ```erb
356
+ <%= content_tag :h1, title %> <!-- <h1 class="color-hotpink">The title</h1> -->
357
+ <%= content_tag :h1, title, class: "example-title" %> <!-- <h1 class="example-title color-hotpink">The title</h1> -->
358
+
359
+ <%= link_to website_link %> <!-- <a href="https://example.com" data-controller="external-link">Example website</a> -->
360
+
361
+ <%= content_tag :li, items %> <!-- <li>Item one</li><li class="highlight">Item two</li> -->
362
+ <%= content_tag :li, items, class: "item" %> <!-- <li class="item">Item one</li><li class="item highlight">Item two</li> -->
363
+ ```
364
+
225
365
  ### Rendering slots
226
366
 
227
367
  > _Docs coming soon..._
228
368
 
229
- ### Slot value objects
369
+ ### Slot values API
230
370
 
231
- > _Docs coming soon..._
371
+ **Singlular slot value variables** in partial templates are actually instances of `Slotity::Value`.
372
+ These value objects are automatically stringified so in most cases you will not even be aware of this and they can just be treated as regular string variables.
373
+
374
+
375
+
376
+ ```erb
377
+ <%= render "example" do |partial| %>
378
+ <% partial.with_title class: "color-hotpink" do %>
379
+ The title
380
+ <% end %>
381
+ <% end %>
382
+ ```
383
+
384
+ ```erb
385
+ <% title.is_a?(Slotify::Value) %> <!-- true -->
386
+ <% items.is_a?(Slotify::ValueCollection) %> <!-- true -->
387
+
388
+ <%= title %> <!-- "The title" -->
389
+ <% title.content %> <!-- "The title" -->
390
+
391
+ <% title.options %> <!-- { class: "color-hotpink" } (hash of any options provided when calling the `.with_title` slot value writer method) -->
392
+ <%= title.options %> <!-- "class='color-hotpink'" (string generated by passing the options hash through the Rails `tag.attributes` helper) -->
393
+ ```
394
+
395
+ **Plural slot value variables** in partial templates are instances of the enumerable `Slotify::ValueCollection` class, with all items instances of `Slotity::Value`.
396
+
397
+ ```erb
398
+ <%= render "example" do |partial| %>
399
+ <% partial.with_item "Item one" %>
400
+ <% partial.with_item "Item two", class: "current" %>
401
+ <% end %>
402
+ ```
403
+
404
+ ```erb
405
+ <% items.is_a?(Slotify::ValueCollection) %> <!-- true -->
406
+
407
+ <% items.each do |item| %>
408
+ <li <%= item.options %>><%= item %></li>
409
+ <% end %>
410
+ <!-- <li>Item one</li> <li class="current">Item two</li> -->
411
+
412
+ <%= items %> <!-- "Item one Item two" -->
413
+ ```
414
+
415
+ #### `Slotity::Value`
416
+
417
+ The following methods are available on `Slotity::Value` instances:
418
+
419
+ **`.content`**
420
+
421
+ Returns the slot content string that was provided as the first argument or as the block when calling the slot writer method.
422
+
423
+ **`.options`**
424
+
425
+ Returns a `Slotify::ValueOptions` instance that can be treated like a `Hash`. Calling `.slice` or `.except` on this will return another `Slotify::ValueOptions` instance.
426
+
427
+ When converted to a string either explicitly (via `.to_s`) or implicitly (by outputting the value template using ERB `<%= %>` expression tags) the stringified value is generated by passing the options hash through the Rails `tag.attributes` helper.
428
+
429
+ **`.with_default_options(default_options)`**
430
+
431
+ Merges the options set when calling the slot value writer method with the `default_options` hash provided and returns a new `Slotity::Value` instance with the merged options set.
432
+
433
+ ```erb
434
+ <% title_with_default_opts = title.with_default_options(class: "size-lg", aria: {level: 1}) %> <!-- apply default options -->
435
+
436
+ <% title_with_default_opts.options %> <!-- { class: "size-lg color-hotpink", aria: {level: 1} } -->
437
+ <%= title_with_default_opts.options %> <!-- "class='size-lg color-hotpink' aria-level='1'" -->
438
+ ```
439
+
440
+ ## Slotify vs alternatives
441
+
442
+ #### `nice_partials`
443
+
444
+ Slotify was very much inspired by the [Nice Partials gem](https://github.com/bullet-train-co/nice_partials) and both provide similar functionality.
445
+ However there are a number of key differences:
446
+
447
+ * Slotify requires the explicit definition of slots using 'strict locals'-style comments;
448
+ Nice partials slots are implicitly defined when rendering the partial.
449
+ * Slotify slot values are available as local variables;
450
+ with Nice partials slot values are accessed via methods on the `partial` variable.
451
+ * Slotify has the concept (and enforces the use) of single-value vs. multiple-value slots.
452
+ * Slotify slot content and options are transparently expanded and merged into defaults when using with helpers like `content_tag` and `link_to`.
453
+ * Slotify slot values are `renderable` objects
454
+
455
+ You might choose slotify if you prefer a stricter, 'Rails-native'-feeling slots implementation, and Nice Partials if you want more render-time flexibility and a clearer
456
+ separation of 'nice partial' functionality from ActionView-provided locals etc.
457
+
458
+ #### `view_component`
459
+
460
+ Both [ViewComponent](https://viewcomponent.org/) and Slotify provide a 'slots' API for content blocks.
461
+ Slotify's slot writer syntax (i.e. `.with_<slot_name>` methods) and the concept of single-value (`renders_one`) vs multiple-value (`renders_many`) slots
462
+ are both modelled on ViewComponent's slots implementation.
463
+
464
+ However apart from that they are quite different. Slotify adds functionality to regular ActionView partials whereas ViewComponent provides a complete standalone component system.
465
+
466
+ Each ViewComponent has an associated class which can be used to extract and encapsulate view logic.
467
+ Slotify doesn't have an analagous concept, any view-specific logic will by default live in the partial template (as per standard partial rendering patterns).
468
+
469
+ You might choose Slotify if you want a more 'component-y' API but you don't want the overhead or learning curve associated with a tool that sits somewhat adjacent to the standard Rails way of doing things.
470
+ But if you have components with a lot of view logic or want a more formalised component format then ViewComponent is likely a better fit for your project.
232
471
 
233
472
  ## Installation
234
473
 
@@ -251,78 +490,72 @@ Slotify was inspired by the excellent [nice_partials gem](https://github.com/bul
251
490
 
252
491
  `nice_partials` provides very similar functionality to Slotify but takes a slightly different approach/style. So if you are not convinced by Slotify then definitely [check it out](https://github.com/bullet-train-co/nice_partials)!
253
492
 
254
- ---
493
+ ## Benchmarks
255
494
 
256
- <a name="full-example" id="full-example"></a>
495
+ Slotify is still in the early stages of development and no attempt has yet been made to optimise rendering performance.
257
496
 
258
- ## A more full-featured example
497
+ Below are some initial (crude) benchmarking comparisons with other similar gems.
259
498
 
260
- ```erb
261
- <!-- views/_example.html.erb -->
499
+ > [!TIP]
500
+ > Benchmarks can be run using the `bin/benchmarks` command from the repository root.
262
501
 
263
- <%# locals: (id:) -%>
264
- <%# slots: (title: "Example title", lists: nil, quotes: nil, website_link:) -%>
502
+ ```
503
+ ✨🦄 ACTION_VIEW 🦄✨
265
504
 
266
- <%= tag.section id: do %>
267
- <h1 class="example-title">
268
- <%= title %>
269
- </h1>
270
-
271
- <p>Example link: <%= link_to website_link, data: {controller: "external-link"} %></p>
505
+ ruby 3.3.1 (2024-04-23 revision c56cd86388) [arm64-darwin23]
506
+ Warming up --------------------------------------
507
+ no slots 12.997k i/100ms
508
+ slots 10.891k i/100ms
509
+ Calculating -------------------------------------
510
+ no slots 125.622k 5.4%) i/s (7.96 μs/i) - 1.261M in 10.072521s
511
+ slots 108.468k (± 3.3%) i/s (9.22 μs/i) - 1.089M in 10.053026s
272
512
 
273
- <%= render lists, title: "Default title" %>
513
+ Comparison:
514
+ no slots: 125621.9 i/s
515
+ slots: 108467.5 i/s - 1.16x slower
274
516
 
275
- <% if quotes.any? %>
276
- <h3>Quotes</h3>
277
- <% quotes.each do |quote| %>
278
- <blockquote <%= quote.options.except(:citation) %>>
279
- <%= quote %>
280
- <%== "&mdash; #{tag.cite(quote.options.citation)}" if quote.options.citation.present? %>
281
- </blockquote>
282
- <% end %>
283
- <% end %>
284
- <% end %>
285
- ```
286
517
 
287
- ```erb
288
- <!-- views/_list.html.erb -->
518
+ ✨🦄 NICE_PARTIALS 🦄✨
289
519
 
290
- <%# locals: (title:) -%>
291
- <%# slots: (items: nil) -%>
292
-
293
- <h3><%= title %></h3>
520
+ ruby 3.3.1 (2024-04-23 revision c56cd86388) [arm64-darwin23]
521
+ Warming up --------------------------------------
522
+ no slots 11.822k i/100ms
523
+ slots 4.204k i/100ms
524
+ Calculating -------------------------------------
525
+ no slots 114.190k (± 4.7%) i/s (8.76 μs/i) - 1.147M in 10.069870s
526
+ slots 41.138k (± 4.3%) i/s (24.31 μs/i) - 411.992k in 10.039730s
294
527
 
295
- <% if items.any? %>
296
- <%= tag.ul class: "list" do %>
297
- <%= content_tag :li, items, class: "list-item" %>
298
- <% end %>
299
- <% end %>
300
- ```
528
+ Comparison:
529
+ no slots: 114190.2 i/s
530
+ slots: 41137.9 i/s - 2.78x slower
301
531
 
302
- ```erb
303
- <!-- views/slotify.html.erb -->
304
532
 
305
- <%= render "example", id: "slotify-example" do |partial| %>
306
- <% partial.with_subtitle do %>
307
- This is the <%= tag.em "subtitle" %>
308
- <% end %>
533
+ ✨🦄 VIEW_COMPONENT 🦄✨
309
534
 
310
- <% partial.with_website_link "example.com", "https://example.com", target: "_blank", data: {controller: "preview-link"} %>
535
+ ruby 3.3.1 (2024-04-23 revision c56cd86388) [arm64-darwin23]
536
+ Warming up --------------------------------------
537
+ no slots 20.329k i/100ms
538
+ slots 7.409k i/100ms
539
+ Calculating -------------------------------------
540
+ no slots 196.288k (± 4.7%) i/s (5.09 μs/i) - 1.972M in 10.073103s
541
+ slots 71.311k (± 5.0%) i/s (14.02 μs/i) - 718.673k in 10.108426s
311
542
 
312
- <% partial.with_list do |list| %>
313
- <% list.with_item "first thing" %>
314
- <% list.with_item "second thing", class: "text-green-700" %>
315
- <% list.with_item "third thing" %>
316
- <% end %>
543
+ Comparison:
544
+ no slots: 196287.6 i/s
545
+ slots: 71310.5 i/s - 2.75x slower
317
546
 
318
- <% partial.with_quote citation: "A. Person", class: "text-lg" do %>
319
- <p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
320
- <% end %>
321
547
 
322
- <% partial.with_quote do %>
323
- <p>Non quos explicabo eius hic quaerat laboriosam incidunt numquam.</p>
324
- <% end %>
325
- <% end %>
326
- ```
548
+ ✨🦄 SLOTIFY 🦄✨
327
549
 
550
+ ruby 3.3.1 (2024-04-23 revision c56cd86388) [arm64-darwin23]
551
+ Warming up --------------------------------------
552
+ no slots 10.883k i/100ms
553
+ slots 77.000 i/100ms
554
+ Calculating -------------------------------------
555
+ no slots 110.153k (± 4.8%) i/s (9.08 μs/i) - 1.099M in 10.009002s
556
+ slots 789.118 (± 4.1%) i/s (1.27 ms/i) - 7.931k in 10.071749s
328
557
 
558
+ Comparison:
559
+ no slots: 110152.8 i/s
560
+ slots: 789.1 i/s - 139.59x slower
561
+ ```
@@ -0,0 +1,20 @@
1
+ module Slotify
2
+ module SlotCompatability
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def make_compatible_with_slots(*method_names)
7
+ proxy = Module.new
8
+ method_names.each do |name|
9
+ proxy.define_method(name) do |*args, **kwargs, &block|
10
+ return super(*args, **kwargs, &block) if args.none?
11
+
12
+ results = MethodArgsResolver.call(args, kwargs, block) { super(*_1, **_2, &_3) }
13
+ results.reduce(ActiveSupport::SafeBuffer.new) { _1 << _2 }
14
+ end
15
+ end
16
+ prepend proxy
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/slotify/error.rb CHANGED
@@ -2,7 +2,7 @@ module Slotify
2
2
  class UnknownSlotError < NameError
3
3
  end
4
4
 
5
- class SlotsAccessError < RuntimeError
5
+ class SlotsDefinedError < RuntimeError
6
6
  end
7
7
 
8
8
  class UndefinedSlotError < StandardError
@@ -16,4 +16,7 @@ module Slotify
16
16
 
17
17
  class StrictSlotsError < ArgumentError
18
18
  end
19
+
20
+ class ReservedSlotNameError < ArgumentError
21
+ end
19
22
  end
@@ -1,25 +1,17 @@
1
1
  module Slotify
2
2
  module Extensions
3
3
  module Base
4
- extend HelpersConcern
4
+ include SlotCompatability
5
5
 
6
- attr_reader :partial
6
+ attr_accessor :partial
7
7
 
8
- def render(target = {}, locals = {}, &block)
8
+ def render(options = {}, locals = {}, &block)
9
9
  @partial = Slotify::Partial.new(self)
10
10
  super
11
11
  ensure
12
12
  @partial = partial.outer_partial
13
13
  end
14
14
 
15
- def _layout_for(*args, &block)
16
- if block && args.first.is_a?(Symbol)
17
- capture_with_outer_partial_access(*args, &block)
18
- else
19
- super
20
- end
21
- end
22
-
23
15
  def capture_with_outer_partial_access(*args, &block)
24
16
  inner_partial, @partial = partial, partial.outer_partial
25
17
  inner_partial.capture(*args, &block)
@@ -4,10 +4,10 @@ module Slotify
4
4
  def render_partial_template(view, locals, template, layout, block)
5
5
  return super unless template.strict_slots?
6
6
 
7
- partial = view.partial
7
+ view.partial.define_slots!(template.strict_slots_keys)
8
+
8
9
  view.capture_with_outer_partial_access(&block) if block
9
- partial.with_strict_slots(template.strict_slots_keys)
10
- locals = locals.merge(partial.slot_locals)
10
+ locals = locals.merge(view.partial.slot_locals)
11
11
 
12
12
  decorate_strict_slots_errors do
13
13
  super(view, locals, template, layout, block)
@@ -3,16 +3,17 @@ require "action_view/template/error"
3
3
  module Slotify
4
4
  module Extensions
5
5
  module Template
6
+ STRICT_SLOTS_NONE = Object.new
6
7
  STRICT_SLOTS_REGEX = /\#\s+slots:\s+\((.*)\)/
7
8
  STRICT_SLOTS_KEYS_REGEX = /(\w+):(?=(?:[^"\\]*(?:\\.|"(?:[^"\\]*\\.)*[^"\\]*"))*[^"]*$)/
8
9
 
9
10
  def initialize(...)
10
11
  super
11
- @strict_slots = ActionView::Template::NONE
12
+ @strict_slots = STRICT_SLOTS_NONE
12
13
  end
13
14
 
14
15
  def strict_slots!
15
- if @strict_slots == ActionView::Template::NONE
16
+ if @strict_slots == STRICT_SLOTS_NONE
16
17
  source.sub!(STRICT_SLOTS_REGEX, "")
17
18
  strict_slots = $1
18
19
  @strict_slots = if strict_slots.nil?
@@ -30,7 +31,7 @@ module Slotify
30
31
  end
31
32
 
32
33
  def strict_slots_keys
33
- strict_slots!.scan(STRICT_SLOTS_KEYS_REGEX).map(&:first)
34
+ @strict_slots_keys ||= strict_slots!.scan(STRICT_SLOTS_KEYS_REGEX).map(&:first)
34
35
  end
35
36
 
36
37
  def strict_locals!
@@ -41,8 +42,9 @@ module Slotify
41
42
 
42
43
  def locals_code
43
44
  return super unless strict_slots?
45
+
44
46
  strict_slots_keys.each_with_object(+super) do |key, code|
45
- code << "#{key} = partial.content_for(:#{key}, binding.local_variable_get(:#{key}));"
47
+ code << "partial.set_slot_default(:#{key}, binding.local_variable_get(:#{key})); #{key} = partial.public_send(:#{key});"
46
48
  end
47
49
  end
48
50
  end
@@ -2,32 +2,31 @@ module Slotify
2
2
  class Partial
3
3
  include InflectionHelper
4
4
 
5
+ RESERVED_SLOT_NAMES = [
6
+ :content, :slot, :value, :content_for,
7
+ :capture, :yield, :partial
8
+ ]
9
+
5
10
  attr_reader :outer_partial
6
11
 
7
12
  def initialize(view_context)
8
13
  @view_context = view_context
9
14
  @outer_partial = view_context.partial
10
- @values = []
11
- @strict_slots = nil
15
+ @values = ValueStore.new(@view_context)
16
+ @defined_slots = nil
12
17
  end
13
18
 
14
- def content_for(slot_name, fallback_value = nil)
15
- raise SlotsAccessError, "slot values cannot be accessed from outside the partial" unless slots_defined?
16
- raise UnknownSlotError, "unknown slot :#{slot_name}" unless slot_defined?(slot_name)
17
-
18
- values = slot_values(slot_name)
19
- if values.none? && !fallback_value.nil?
20
- values = add_values(slot_name, Array(fallback_value))
21
- end
19
+ def content_for(slot_name)
20
+ raise UnknownSlotError, "unknown slot :#{slot_name}" unless slot?(slot_name)
22
21
 
23
- singular?(slot_name) ? values.first : ValueCollection.new(values)
22
+ slot_values = values.for(slot_name)
23
+ singular?(slot_name) ? slot_values.first : ValueCollection.new(slot_values)
24
24
  end
25
25
 
26
26
  def content_for?(slot_name)
27
- raise SlotsAccessError, "slot values cannot be accessed from outside the partial" unless slots_defined?
28
- raise UnknownSlotError, "unknown slot :#{slot_name}" unless slot_defined?(slot_name)
27
+ raise UnknownSlotError, "unknown slot :#{slot_name}" unless slot?(slot_name)
29
28
 
30
- slot_values(slot_name).any?
29
+ values.for(slot_name).any?
31
30
  end
32
31
 
33
32
  def capture(*args, &block)
@@ -35,19 +34,30 @@ module Slotify
35
34
  end
36
35
 
37
36
  def yield(*args)
38
- if args.empty?
39
- @captured_buffer
40
- else
41
- content_for(args.first)
37
+ args.empty? ? @captured_buffer : content_for(args.first)
38
+ end
39
+
40
+ def content
41
+ self.yield
42
+ end
43
+
44
+ def set_slot_default(slot_name, default_value)
45
+ raise UnknownSlotError, "unknown slot :#{slot_name}" unless slot?(slot_name)
46
+
47
+ if values.for(slot_name).none? && !default_value.nil?
48
+ values.add(slot_name, Array.wrap(default_value))
42
49
  end
43
50
  end
44
51
 
45
52
  def slot_locals
46
- pairs = @strict_slots.map do |slot_name|
47
- values = slot_values(slot_name)
48
- values = singular?(slot_name) ? values&.first : values
49
- [slot_name, values]
53
+ validate_slots!
54
+
55
+ pairs = @defined_slots.map do |slot_name|
56
+ slot_values = values.for(slot_name)
57
+ slot_values = singular?(slot_name) ? slot_values&.first : slot_values
58
+ [slot_name, slot_values]
50
59
  end
60
+
51
61
  pairs.filter do |key, value|
52
62
  # keep empty strings as local value but filter out empty arrays
53
63
  # and objects so they don't override any default values set via strict slots.
@@ -55,69 +65,50 @@ module Slotify
55
65
  end.to_h
56
66
  end
57
67
 
58
- def with_strict_slots(strict_slot_names)
59
- @strict_slots = strict_slot_names.map(&:to_sym)
60
- validate_slots!
68
+ def define_slots!(slot_names)
69
+ raise SlotsDefinedError, "Slots cannot be redefined" unless @defined_slots.nil?
70
+
71
+ @defined_slots = slot_names.map(&:to_sym).each do |slot_name|
72
+ if RESERVED_SLOT_NAMES.include?(singularize(slot_name))
73
+ raise ReservedSlotNameError, ":#{slot_name} is a reserved word and cannot be used as a slot name"
74
+ end
75
+ end
61
76
  end
62
77
 
63
78
  def respond_to_missing?(name, include_private = false)
64
- name.start_with?("with_")
79
+ name.start_with?("with_") || slot?(name)
65
80
  end
66
81
 
67
82
  def method_missing(name, *args, **options, &block)
68
83
  if name.start_with?("with_")
69
- slot_name = name.to_s.delete_prefix("with_")
70
- if singular?(slot_name)
71
- add_value(slot_name, args, options, block)
72
- else
73
- collection = args.first
74
- add_values(slot_name, collection, options, block)
75
- end
84
+ values.add(name.to_s.delete_prefix("with_"), args, options, block)
85
+ elsif slot?(name)
86
+ content_for(name)
87
+ else
88
+ super
76
89
  end
77
90
  end
78
91
 
79
92
  private
80
93
 
81
- def slots_defined?
82
- !@strict_slots.nil?
83
- end
84
-
85
- def slot_defined?(slot_name)
86
- slot_name && slots_defined? && @strict_slots.include?(slot_name.to_sym)
87
- end
88
-
89
- def slot_values(slot_name)
90
- @values.filter { _1.slot_name == singularize(slot_name) }
91
- end
94
+ attr_reader :values
92
95
 
93
- def add_values(slot_name, collection, options = {}, block = nil)
94
- collection.map { add_value(slot_name, _1, options, block) }
95
- rescue NoMethodError
96
- raise SlotArgumentError, "expected array to be passed to slot :#{slot_name} (received #{collection.class.name})"
97
- end
98
-
99
- def add_value(slot_name, args = [], options = {}, block = nil)
100
- MethodArgsResolver.call(args, options, block) do
101
- @values << Value.new(@view_context, singularize(slot_name), _1, _2, _3)
102
- end
103
-
104
- @values.last
96
+ def slot?(slot_name)
97
+ slot_name && @defined_slots.include?(slot_name.to_sym)
105
98
  end
106
99
 
107
100
  def validate_slots!
108
- return if @strict_slots.nil?
109
-
110
- singular_slots = @strict_slots.map { singularize(_1) }
111
- slots_called = @values.map(&:slot_name).uniq
112
- undefined_slots = slots_called - singular_slots
101
+ return if @defined_slots.nil?
113
102
 
103
+ undefined_slots = values.slot_names - @defined_slots.map { singularize(_1) }
114
104
  if undefined_slots.any?
115
- raise UndefinedSlotError, "missing slot #{"definition".pluralize(undefined_slots.size)} for `#{undefined_slots.map { ":#{_1}(s)" }.join(", ")}`"
105
+ raise UndefinedSlotError,
106
+ "missing slot #{"definition".pluralize(undefined_slots.size)} for `#{undefined_slots.map { ":#{_1}(s)" }.join(", ")}`"
116
107
  end
117
108
 
118
- @strict_slots.filter { singular?(_1) }.each do |slot_name|
119
- values = slot_values(slot_name)
120
- raise MultipleSlotEntriesError, "slot :#{slot_name} called #{values.size} times (expected 1)" if values.many?
109
+ @defined_slots.filter { singular?(_1) }.each do |slot_name|
110
+ slot_values = values.for(slot_name)
111
+ raise MultipleSlotEntriesError, "slot :#{slot_name} called #{slot_values.size} times (expected 1)" if slot_values.many?
121
112
  end
122
113
  end
123
114
  end
data/lib/slotify/value.rb CHANGED
@@ -4,11 +4,11 @@ module Slotify
4
4
 
5
5
  attr_reader :slot_name, :args, :block
6
6
 
7
- delegate :presence, to: :@content
7
+ delegate :presence, :to_s, :to_str, to: :content
8
8
 
9
- def initialize(view_context, slot_name, args = [], options = {}, block = nil, partial_path: nil)
9
+ def initialize(view_context, args = [], options = {}, block = nil, slot_name: nil, partial_path: nil)
10
10
  @view_context = view_context
11
- @slot_name = slot_name.to_sym
11
+ @slot_name = slot_name&.to_sym
12
12
  @args = args
13
13
  @options = options.to_h
14
14
  @block = block
@@ -20,21 +20,16 @@ module Slotify
20
20
  end
21
21
 
22
22
  def content
23
- body = if @block && @block.arity == 0
24
- @view_context.capture(&@block)
23
+ if @block && @block.arity == 0
24
+ body = @view_context.capture(&@block)
25
+ ActiveSupport::SafeBuffer.new(body.presence || "")
26
+ elsif args.first.is_a?(String)
27
+ ActiveSupport::SafeBuffer.new(args.first)
25
28
  else
26
- begin
27
- args.first.to_str
28
- rescue NoMethodError
29
- ""
30
- end
29
+ args.first
31
30
  end
32
- ActiveSupport::SafeBuffer.new(body.presence || "")
33
31
  end
34
32
 
35
- alias_method :to_s, :content
36
- alias_method :to_str, :content
37
-
38
33
  def present?
39
34
  @args.present? || @options.present? || @block
40
35
  end
@@ -52,24 +47,30 @@ module Slotify
52
47
  alias_method :to_hash, :to_h
53
48
 
54
49
  def with_partial_path(partial_path)
55
- Value.new(@view_context, @slot_name, @args, options, @block, partial_path:)
50
+ Value.new(@view_context, @args, options, @block, slot_name: @slot_name, partial_path:)
56
51
  end
57
52
 
58
53
  def with_default_options(default_options)
59
54
  options = TagOptionsMerger.call(default_options, @options)
60
- Value.new(@view_context, @slot_name, @args, options, @block)
55
+ Value.new(@view_context, @args, options, @block, slot_name: @slot_name)
61
56
  end
62
57
 
63
58
  def respond_to_missing?(name, include_private = false)
64
59
  name.start_with?("to_") || super
65
60
  end
66
61
 
67
- def method_missing(name, *args, **options)
68
- if name.start_with?("to_") && args.none?
69
- @args.first.public_send(name)
62
+ def method_missing(name, ...)
63
+ if name.start_with?("to_")
64
+ @args.first.public_send(name, ...)
65
+ else
66
+ super
70
67
  end
71
68
  end
72
69
 
70
+ def [](key)
71
+ key.is_a?(Integer) ? @args[key] : super
72
+ end
73
+
73
74
  def render_in(view_context, &block)
74
75
  view_context.render partial_path, **@options.to_h, &@block || block
75
76
  end
@@ -0,0 +1,28 @@
1
+ module Slotify
2
+ class ValueStore
3
+ include InflectionHelper
4
+
5
+ def initialize(view_context)
6
+ @view_context = view_context
7
+ @values = []
8
+ end
9
+
10
+ def for(slot_name)
11
+ @values.select { _1.slot_name == singularize(slot_name) }
12
+ end
13
+
14
+ def add(slot_name, args = [], options = {}, block = nil)
15
+ if plural?(slot_name)
16
+ Array.wrap(args.first).map { add(singularize(slot_name), _1, options, block) }
17
+ else
18
+ MethodArgsResolver.call(args, options, block) do
19
+ @values << Value.new(@view_context, _1, _2, _3, slot_name: singularize(slot_name))
20
+ end
21
+ end
22
+ end
23
+
24
+ def slot_names
25
+ @values.map(&:slot_name).uniq
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,3 @@
1
1
  module Slotify
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slotify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Perkins
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-06 00:00:00.000000000 Z
11
+ date: 2025-04-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -46,8 +46,8 @@ extra_rdoc_files: []
46
46
  files:
47
47
  - README.md
48
48
  - lib/slotify.rb
49
- - lib/slotify/concerns/helpers_concern.rb
50
49
  - lib/slotify/concerns/inflection_helper.rb
50
+ - lib/slotify/concerns/slot_compatability.rb
51
51
  - lib/slotify/error.rb
52
52
  - lib/slotify/extensions/base.rb
53
53
  - lib/slotify/extensions/partial_renderer.rb
@@ -58,6 +58,7 @@ files:
58
58
  - lib/slotify/value.rb
59
59
  - lib/slotify/value_collection.rb
60
60
  - lib/slotify/value_options.rb
61
+ - lib/slotify/value_store.rb
61
62
  - lib/slotify/version.rb
62
63
  homepage:
63
64
  licenses:
@@ -78,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
79
  - !ruby/object:Gem::Version
79
80
  version: '0'
80
81
  requirements: []
81
- rubygems_version: 3.5.10
82
+ rubygems_version: 3.3.3
82
83
  signing_key:
83
84
  specification_version: 4
84
85
  summary: Superpowered slots for your Rails partials.
@@ -1,16 +0,0 @@
1
- module Slotify
2
- module HelpersConcern
3
- def make_compatible_with_slots(*method_names)
4
- proxy = Module.new
5
- method_names.each do |name|
6
- proxy.define_method(name) do |*args, **kwargs, &block|
7
- return super(*args, **kwargs, &block) if args.none?
8
-
9
- results = MethodArgsResolver.call(args, kwargs, block) { super(*_1, **_2, &_3) }
10
- results.reduce(ActiveSupport::SafeBuffer.new) { _1 << _2 }
11
- end
12
- end
13
- prepend proxy
14
- end
15
- end
16
- end