slotify 0.0.0 → 0.0.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 +226 -48
- data/lib/slotify/concerns/helpers_concern.rb +16 -0
- data/lib/slotify/concerns/inflection_helper.rb +18 -0
- data/lib/slotify/error.rb +19 -0
- data/lib/slotify/extensions/base.rb +34 -0
- data/lib/slotify/extensions/partial_renderer.rb +39 -0
- data/lib/slotify/extensions/template.rb +50 -0
- data/lib/slotify/partial.rb +124 -0
- data/lib/slotify/services/method_args_resolver.rb +26 -0
- data/lib/slotify/services/tag_options_merger.rb +54 -0
- data/lib/slotify/value.rb +83 -0
- data/lib/slotify/value_collection.rb +31 -0
- data/lib/slotify/value_options.rb +20 -0
- data/lib/slotify/version.rb +1 -1
- data/lib/slotify.rb +18 -0
- metadata +45 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9401d13b9a845572abccc1cd38362bb6facfb5394568a74345d856abc6db80ea
|
4
|
+
data.tar.gz: bd8c9df4efe6ee8d40d7967f06326b9950d160c824828c11537f5c44f63f200c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1af7661c421919e5bc90069a75781075e27b9f97d1d767148f5ece3abe773dc1b206ea59435e511c7e1b1805a34124b19b4e1d8354fc7cf70e27dfbcd4e7445a
|
7
|
+
data.tar.gz: 2c3f889e7f55d65af6be315c9b013a21ab63b8cee3c9304cf3ebd947b3324c899c0830e5ea420d1aac7bda0d54603fe69d97eb87fdc765c1a8c0f8f70ec9af69
|
data/README.md
CHANGED
@@ -1,81 +1,261 @@
|
|
1
|
-
<img src=".github/assets/slotify_wordmark.svg" width="
|
2
|
-
|
1
|
+
<img src=".github/assets/slotify_wordmark.svg" width="200">
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
### Superpowered slots for ActionView partials
|
3
6
|
|
7
|
+
---
|
4
8
|
|
5
|
-
|
9
|
+
## Overview
|
6
10
|
|
7
|
-
|
11
|
+
Slotify adds an unobtrusive (but powerful!) **content slot API** to ActionView partials.
|
8
12
|
|
9
|
-
|
13
|
+
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.
|
10
14
|
|
11
|
-
|
15
|
+
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/).
|
12
16
|
|
13
|
-
|
14
|
-
|
17
|
+
###
|
18
|
+
|
19
|
+
## Slotify basics
|
20
|
+
|
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)).
|
15
22
|
|
16
23
|
```erb
|
17
|
-
|
24
|
+
<%# slots: (slot_name: "default value", optional_slot_name: nil, required_slot_name:) -%>
|
25
|
+
```
|
18
26
|
|
19
|
-
|
27
|
+
Slot content is accessed via **standard local variables** within the partial. So a simple, slot-enabled `article` partial template might look something like this:
|
20
28
|
|
21
|
-
|
22
|
-
|
29
|
+
```erb
|
30
|
+
<!-- _article.html.erb -->
|
31
|
+
|
32
|
+
<%# slots: (heading: "Default title", body: nil) -%>
|
23
33
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
<% end%>
|
31
|
-
</ul>
|
34
|
+
<article>
|
35
|
+
<h1><%= heading %></h1>
|
36
|
+
<% if body.present? %>
|
37
|
+
<div>
|
38
|
+
<%= body %>
|
39
|
+
</div>
|
32
40
|
<% end %>
|
41
|
+
</article>
|
42
|
+
```
|
33
43
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
44
|
+
> [!NOTE]
|
45
|
+
> _The above should feel familiar to anyone who has partials (and strict locals) in the past. This is just regular partial syntax but with `slots` defined instead of `locals` (don't worry - you can still define locals too!)._
|
46
|
+
|
47
|
+
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
|
+
For example, here our `article` partial is being rendered with content for the `heading` and `body` slots that were defined above:
|
50
|
+
|
51
|
+
```erb
|
52
|
+
<%= render "article" do |partial| %>
|
53
|
+
<% partial.with_heading "This is a title" %>
|
54
|
+
<% partial.with_body do %>
|
55
|
+
<p>You can use <%= tag.strong "markup" %> within slot content blocks without
|
56
|
+
having to worry about marking the output as <code>html_safe</code> later.</p>
|
57
|
+
<% end %>
|
58
|
+
<% end %>
|
59
|
+
```
|
60
|
+
|
61
|
+
> [!NOTE]
|
62
|
+
> _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
|
+
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
|
+
|
66
|
+
## Usage
|
67
|
+
|
68
|
+
<a name="defining-slots" id="defining-slots"></a>
|
69
|
+
### Defining slots
|
70
|
+
|
71
|
+
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
|
+
|
73
|
+
```erb
|
74
|
+
<%# slots: (title:, summary: "No summary available", author: nil) -%>
|
75
|
+
```
|
76
|
+
|
77
|
+
#### Required slots
|
78
|
+
|
79
|
+
Required slots are defined without a default value.
|
80
|
+
If no content is provided for a required slot then a `StrictSlotsError` exception will be raised.
|
81
|
+
|
82
|
+
```erb
|
83
|
+
<!-- _required.html.erb -->
|
84
|
+
|
85
|
+
<%# slots: (title:) -%>
|
86
|
+
<h1><%= title %></h1>
|
87
|
+
```
|
88
|
+
|
89
|
+
```erb
|
90
|
+
<%= render "required" do |partial| %>
|
91
|
+
<!-- ❌ raises an error, no content set for the `title` slot -->
|
92
|
+
<% end %>
|
93
|
+
```
|
94
|
+
|
95
|
+
#### Optional slots
|
96
|
+
|
97
|
+
If a default value is set then the slot becomes _optional_. If no content is provided when rendering the partial then
|
98
|
+
the default value will be used instead.
|
99
|
+
|
100
|
+
```erb
|
101
|
+
<%# slots: (title: "Default title", author: nil) -%>
|
102
|
+
```
|
103
|
+
|
104
|
+
### Using alongside strict locals
|
105
|
+
|
106
|
+
Strict locals can be defined in 'slotified' partial templates in the same way as usual,
|
107
|
+
either above or below the `slots` definition.
|
108
|
+
|
109
|
+
```erb
|
110
|
+
<!-- _article.html.erb -->
|
111
|
+
|
112
|
+
<%# locals: (title:) -%>
|
113
|
+
<%# slots: (summary: "No summary available") -%>
|
114
|
+
|
115
|
+
<article>
|
116
|
+
<h1><%= title %></h1>
|
117
|
+
<div><%= summary %></div>
|
118
|
+
</article>
|
119
|
+
```
|
120
|
+
|
121
|
+
Locals are provided when rendering the partial in the usual way.
|
122
|
+
|
123
|
+
```erb
|
124
|
+
<%= render "article", title: "Article title here" do |partial| %>
|
125
|
+
<% partial.with_summary do %>
|
126
|
+
<p>Summary content here...</p>
|
127
|
+
<% end %>
|
128
|
+
<% end %>
|
38
129
|
```
|
39
130
|
|
40
|
-
|
131
|
+
### Slot content and options
|
132
|
+
|
133
|
+
Content is passed into slots using dynamically generated `partial#with_<slot_name>` methods.
|
134
|
+
|
135
|
+
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:
|
41
137
|
|
42
138
|
```erb
|
43
|
-
<%= render "
|
44
|
-
|
45
|
-
|
139
|
+
<%= render "example" do |partial| %>
|
140
|
+
<% partial.with_title "Title passed as argument" %>
|
141
|
+
<% partial.with_title do %>
|
142
|
+
Title passed as block content
|
46
143
|
<% end %>
|
144
|
+
<% end %>
|
145
|
+
```
|
146
|
+
|
147
|
+
### Slot types
|
148
|
+
|
149
|
+
There are two types of slots.
|
150
|
+
|
151
|
+
* **Single-value** slots can only be called **once** and return **a single value**.
|
152
|
+
* **Multiple-value** slots can be called **many times** and return **an array of values**.
|
153
|
+
|
154
|
+
#### Single-value slots
|
155
|
+
|
156
|
+
Single-value slots are defined using a **singlular** slot name:
|
157
|
+
|
158
|
+
```erb
|
159
|
+
<%# slots: (item: nil) -%>
|
160
|
+
```
|
47
161
|
|
48
|
-
|
49
|
-
|
162
|
+
Single-value slots can be called once (at most)
|
163
|
+
and their corresponding template variable represents a single value:
|
50
164
|
|
51
|
-
|
165
|
+
```erb
|
166
|
+
<%= render "example" do |partial| %>
|
167
|
+
<% partial.with_item "Item one" %>
|
52
168
|
<% end %>
|
53
169
|
```
|
54
170
|
|
55
|
-
|
171
|
+
```erb
|
172
|
+
<%# slots: (item: nil) -%>
|
173
|
+
<div>
|
174
|
+
<%= item %> <!-- "Item one" -->
|
175
|
+
</div>
|
176
|
+
```
|
56
177
|
|
57
|
-
|
178
|
+
> [!WARNING]
|
179
|
+
> Calling a single-value slot more than once when rendering a partial will raise an error:
|
180
|
+
>
|
181
|
+
> ```erb
|
182
|
+
> <%= render "example" do |partial| %>
|
183
|
+
> <% partial.with_item "Item one" %>
|
184
|
+
> <% partial.with_item "Item two" %> # ❌ raises an error!
|
185
|
+
> <% end %>
|
186
|
+
> ```
|
58
187
|
|
59
|
-
|
60
|
-
* `Ruby 3.1+`
|
188
|
+
#### Multiple-value slots
|
61
189
|
|
62
|
-
|
190
|
+
Multiple-value slots are defined using a **plural** slot name:
|
63
191
|
|
64
|
-
|
192
|
+
```erb
|
193
|
+
<%# slots: (items: nil) -%>
|
194
|
+
```
|
65
195
|
|
66
|
-
|
196
|
+
Multiple-value slots can be called as many times as needed
|
197
|
+
and their corresponding template variable represents an array of values:
|
67
198
|
|
68
|
-
|
199
|
+
```erb
|
200
|
+
<%= render "example" do |partial| %>
|
201
|
+
<% partial.with_item "Item one" %>
|
202
|
+
<% partial.with_item "Item two" %>
|
203
|
+
<% partial.with_item "Item three" %>
|
204
|
+
<% end %>
|
205
|
+
```
|
69
206
|
|
70
207
|
```erb
|
71
|
-
<%# slots: (
|
208
|
+
<%# slots: (items: nil) -%>
|
209
|
+
|
210
|
+
<%= items %> <!-- ["Item one", "Item two", "Item three"] -->
|
211
|
+
|
212
|
+
<ul>
|
213
|
+
<% items.each do |item| %>
|
214
|
+
<li>
|
215
|
+
<% item %>
|
216
|
+
</li>
|
217
|
+
<% end %>
|
218
|
+
</ul>
|
72
219
|
```
|
73
220
|
|
74
|
-
|
75
|
-
* Slots can be required (no default value) or optional.
|
76
|
-
* Optional slots can additionaly specify default content as needed.
|
221
|
+
### Passing slot content to helpers
|
77
222
|
|
78
|
-
|
223
|
+
> _Docs coming soon..._
|
224
|
+
|
225
|
+
### Rendering slots
|
226
|
+
|
227
|
+
> _Docs coming soon..._
|
228
|
+
|
229
|
+
### Slot value objects
|
230
|
+
|
231
|
+
> _Docs coming soon..._
|
232
|
+
|
233
|
+
## Installation
|
234
|
+
|
235
|
+
Add the following to your Rails app Gemfile:
|
236
|
+
|
237
|
+
```rb
|
238
|
+
gem "slotify"
|
239
|
+
```
|
240
|
+
|
241
|
+
And then run `bundle install`. You are good to go!
|
242
|
+
|
243
|
+
## Requirements
|
244
|
+
|
245
|
+
* `Rails 7.1+`
|
246
|
+
* `Ruby 3.1+`
|
247
|
+
|
248
|
+
## Credits
|
249
|
+
|
250
|
+
Slotify was inspired by the excellent [nice_partials gem](https://github.com/bullet-train-co/nice_partials) as well as ViewComponent's [slots implementation](https://viewcomponent.org/guide/slots.html).
|
251
|
+
|
252
|
+
`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
|
+
|
254
|
+
---
|
255
|
+
|
256
|
+
<a name="full-example" id="full-example"></a>
|
257
|
+
|
258
|
+
## A more full-featured example
|
79
259
|
|
80
260
|
```erb
|
81
261
|
<!-- views/_example.html.erb -->
|
@@ -88,7 +268,7 @@ Slots are defined using a `strict locals`-style magic comment at the top of the
|
|
88
268
|
<%= title %>
|
89
269
|
</h1>
|
90
270
|
|
91
|
-
<p>Example link: <%=
|
271
|
+
<p>Example link: <%= link_to website_link, data: {controller: "external-link"} %></p>
|
92
272
|
|
93
273
|
<%= render lists, title: "Default title" %>
|
94
274
|
|
@@ -114,7 +294,7 @@ Slots are defined using a `strict locals`-style magic comment at the top of the
|
|
114
294
|
|
115
295
|
<% if items.any? %>
|
116
296
|
<%= tag.ul class: "list" do %>
|
117
|
-
<%=
|
297
|
+
<%= content_tag :li, items, class: "list-item" %>
|
118
298
|
<% end %>
|
119
299
|
<% end %>
|
120
300
|
```
|
@@ -145,6 +325,4 @@ Slots are defined using a `strict locals`-style magic comment at the top of the
|
|
145
325
|
<% end %>
|
146
326
|
```
|
147
327
|
|
148
|
-
## Credits
|
149
328
|
|
150
|
-
Slotify was heavily influenced by (and borrows some code from) the excellent [nice_partials gem](https://github.com/bullet-train-co/nice_partials). It provides similar functionality to Slotify so if you are not convinced by what you see here then go check it out!
|
@@ -0,0 +1,16 @@
|
|
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
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Slotify
|
2
|
+
module InflectionHelper
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
def singular?(str)
|
6
|
+
str = str.to_s
|
7
|
+
str.singularize == str && str.pluralize != str
|
8
|
+
end
|
9
|
+
|
10
|
+
def singularize(sym)
|
11
|
+
sym.to_s.singularize.to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
def plural?(str)
|
15
|
+
!singular?(str)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Slotify
|
2
|
+
class UnknownSlotError < NameError
|
3
|
+
end
|
4
|
+
|
5
|
+
class SlotsAccessError < RuntimeError
|
6
|
+
end
|
7
|
+
|
8
|
+
class UndefinedSlotError < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
class MultipleSlotEntriesError < ArgumentError
|
12
|
+
end
|
13
|
+
|
14
|
+
class SlotArgumentError < ArgumentError
|
15
|
+
end
|
16
|
+
|
17
|
+
class StrictSlotsError < ArgumentError
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Slotify
|
2
|
+
module Extensions
|
3
|
+
module Base
|
4
|
+
extend HelpersConcern
|
5
|
+
|
6
|
+
attr_reader :partial
|
7
|
+
|
8
|
+
def render(target = {}, locals = {}, &block)
|
9
|
+
@partial = Slotify::Partial.new(self)
|
10
|
+
super
|
11
|
+
ensure
|
12
|
+
@partial = partial.outer_partial
|
13
|
+
end
|
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
|
+
def capture_with_outer_partial_access(*args, &block)
|
24
|
+
inner_partial, @partial = partial, partial.outer_partial
|
25
|
+
inner_partial.capture(*args, &block)
|
26
|
+
ensure
|
27
|
+
@partial = inner_partial
|
28
|
+
end
|
29
|
+
|
30
|
+
make_compatible_with_slots :url_for, :link_to, :button_to, :link_to_unless_current,
|
31
|
+
:link_to_unless, :link_to_if, :mail_to, :sms_to, :phone_to, :tag, :content_tag
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Slotify
|
2
|
+
module Extensions
|
3
|
+
module PartialRenderer
|
4
|
+
def render_partial_template(view, locals, template, layout, block)
|
5
|
+
return super unless template.strict_slots?
|
6
|
+
|
7
|
+
partial = view.partial
|
8
|
+
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)
|
11
|
+
|
12
|
+
decorate_strict_slots_errors do
|
13
|
+
super(view, locals, template, layout, block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def decorate_strict_slots_errors(&block)
|
18
|
+
yield
|
19
|
+
rescue ActionView::Template::Error => error
|
20
|
+
if missing_strict_locals_error?(error)
|
21
|
+
local_names = error.cause.message.scan(/\s:(\w+)/).flatten
|
22
|
+
if local_names.any?
|
23
|
+
slot_names = local_names.intersection(error.template.strict_slots_keys).map { ":#{_1}" }
|
24
|
+
if slot_names.any?
|
25
|
+
message = "missing #{"slot".pluralize(slot_names.size)}: #{slot_names.join(", ")} for #{error.template.short_identifier}"
|
26
|
+
raise Slotify::StrictSlotsError, message
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
raise error
|
31
|
+
end
|
32
|
+
|
33
|
+
def missing_strict_locals_error?(error)
|
34
|
+
error.template && (defined?(ActionView::StrictLocalsError) && error.cause.is_a?(ActionView::StrictLocalsError)) ||
|
35
|
+
(error.cause.is_a?(ArgumentError) && error.cause.message.match(/missing local/))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "action_view/template/error"
|
2
|
+
|
3
|
+
module Slotify
|
4
|
+
module Extensions
|
5
|
+
module Template
|
6
|
+
STRICT_SLOTS_REGEX = /\#\s+slots:\s+\((.*)\)/
|
7
|
+
STRICT_SLOTS_KEYS_REGEX = /(\w+):(?=(?:[^"\\]*(?:\\.|"(?:[^"\\]*\\.)*[^"\\]*"))*[^"]*$)/
|
8
|
+
|
9
|
+
def initialize(...)
|
10
|
+
super
|
11
|
+
@strict_slots = ActionView::Template::NONE
|
12
|
+
end
|
13
|
+
|
14
|
+
def strict_slots!
|
15
|
+
if @strict_slots == ActionView::Template::NONE
|
16
|
+
source.sub!(STRICT_SLOTS_REGEX, "")
|
17
|
+
strict_slots = $1
|
18
|
+
@strict_slots = if strict_slots.nil?
|
19
|
+
""
|
20
|
+
else
|
21
|
+
strict_slots.sub("**", "").strip.delete_suffix(",")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
@strict_slots
|
26
|
+
end
|
27
|
+
|
28
|
+
def strict_slots?
|
29
|
+
strict_slots!.present?
|
30
|
+
end
|
31
|
+
|
32
|
+
def strict_slots_keys
|
33
|
+
strict_slots!.scan(STRICT_SLOTS_KEYS_REGEX).map(&:first)
|
34
|
+
end
|
35
|
+
|
36
|
+
def strict_locals!
|
37
|
+
return super unless strict_slots?
|
38
|
+
|
39
|
+
[strict_slots!, super].compact.join(", ")
|
40
|
+
end
|
41
|
+
|
42
|
+
def locals_code
|
43
|
+
return super unless strict_slots?
|
44
|
+
strict_slots_keys.each_with_object(+super) do |key, code|
|
45
|
+
code << "#{key} = partial.content_for(:#{key}, binding.local_variable_get(:#{key}));"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Slotify
|
2
|
+
class Partial
|
3
|
+
include InflectionHelper
|
4
|
+
|
5
|
+
attr_reader :outer_partial
|
6
|
+
|
7
|
+
def initialize(view_context)
|
8
|
+
@view_context = view_context
|
9
|
+
@outer_partial = view_context.partial
|
10
|
+
@values = []
|
11
|
+
@strict_slots = nil
|
12
|
+
end
|
13
|
+
|
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
|
22
|
+
|
23
|
+
singular?(slot_name) ? values.first : ValueCollection.new(values)
|
24
|
+
end
|
25
|
+
|
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)
|
29
|
+
|
30
|
+
slot_values(slot_name).any?
|
31
|
+
end
|
32
|
+
|
33
|
+
def capture(*args, &block)
|
34
|
+
@captured_buffer = @view_context.capture(*args, self, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def yield(*args)
|
38
|
+
if args.empty?
|
39
|
+
@captured_buffer
|
40
|
+
else
|
41
|
+
content_for(args.first)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
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]
|
50
|
+
end
|
51
|
+
pairs.filter do |key, value|
|
52
|
+
# keep empty strings as local value but filter out empty arrays
|
53
|
+
# and objects so they don't override any default values set via strict slots.
|
54
|
+
value.is_a?(String) || value&.present?
|
55
|
+
end.to_h
|
56
|
+
end
|
57
|
+
|
58
|
+
def with_strict_slots(strict_slot_names)
|
59
|
+
@strict_slots = strict_slot_names.map(&:to_sym)
|
60
|
+
validate_slots!
|
61
|
+
end
|
62
|
+
|
63
|
+
def respond_to_missing?(name, include_private = false)
|
64
|
+
name.start_with?("with_")
|
65
|
+
end
|
66
|
+
|
67
|
+
def method_missing(name, *args, **options, &block)
|
68
|
+
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
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
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
|
92
|
+
|
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
|
105
|
+
end
|
106
|
+
|
107
|
+
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
|
113
|
+
|
114
|
+
if undefined_slots.any?
|
115
|
+
raise UndefinedSlotError, "missing slot #{"definition".pluralize(undefined_slots.size)} for `#{undefined_slots.map { ":#{_1}(s)" }.join(", ")}`"
|
116
|
+
end
|
117
|
+
|
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?
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Slotify
|
2
|
+
module MethodArgsResolver
|
3
|
+
class << self
|
4
|
+
def call(args = [], options = {}, block = nil)
|
5
|
+
args = args.is_a?(Array) ? args.clone : [args]
|
6
|
+
value_index = args.index { _1.is_a?(ValueCollection) || _1.is_a?(Value) }
|
7
|
+
if value_index.nil?
|
8
|
+
[yield(args, options, block)]
|
9
|
+
else
|
10
|
+
target = args[value_index]
|
11
|
+
values = target.is_a?(ValueCollection) ? target : [target]
|
12
|
+
values.map do |value|
|
13
|
+
cloned_args = args.clone
|
14
|
+
cloned_args[value_index, 1] = value.args.clone
|
15
|
+
|
16
|
+
yield(
|
17
|
+
cloned_args,
|
18
|
+
TagOptionsMerger.call(options, value.options),
|
19
|
+
value.block || block
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Slotify
|
2
|
+
module TagOptionsMerger
|
3
|
+
class << self
|
4
|
+
include ::ActionView::Helpers::TagHelper
|
5
|
+
|
6
|
+
def call(original, target)
|
7
|
+
original = original.to_h.deep_symbolize_keys
|
8
|
+
target = target.to_h.deep_symbolize_keys
|
9
|
+
|
10
|
+
target.each do |key, value|
|
11
|
+
original[key] = case key
|
12
|
+
when :data
|
13
|
+
merge_data_options(original[key], value)
|
14
|
+
when :class
|
15
|
+
merge_class_options(original[key], value)
|
16
|
+
else
|
17
|
+
value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
original
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def merge_data_options(original_data, target_data)
|
27
|
+
original_data = original_data.dup
|
28
|
+
|
29
|
+
target_data.each do |key, value|
|
30
|
+
values = [original_data[key], value]
|
31
|
+
original_data[key] = if key.in?([:controller, :action]) && all_kind_of?(String, values)
|
32
|
+
merge_strings(values)
|
33
|
+
else
|
34
|
+
value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
original_data
|
39
|
+
end
|
40
|
+
|
41
|
+
def merge_class_options(original_classes, target_classes)
|
42
|
+
class_names(original_classes, target_classes)
|
43
|
+
end
|
44
|
+
|
45
|
+
def merge_strings(*args)
|
46
|
+
args.map(&:presence).compact.join(" ")
|
47
|
+
end
|
48
|
+
|
49
|
+
def all_kind_of?(kind, values)
|
50
|
+
values.none? { !_1.is_a?(kind) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Slotify
|
2
|
+
class Value
|
3
|
+
include InflectionHelper
|
4
|
+
|
5
|
+
attr_reader :slot_name, :args, :block
|
6
|
+
|
7
|
+
delegate :presence, to: :@content
|
8
|
+
|
9
|
+
def initialize(view_context, slot_name, args = [], options = {}, block = nil, partial_path: nil)
|
10
|
+
@view_context = view_context
|
11
|
+
@slot_name = slot_name.to_sym
|
12
|
+
@args = args
|
13
|
+
@options = options.to_h
|
14
|
+
@block = block
|
15
|
+
@partial_path = partial_path
|
16
|
+
end
|
17
|
+
|
18
|
+
def options
|
19
|
+
ValueOptions.new(@view_context, @options)
|
20
|
+
end
|
21
|
+
|
22
|
+
def content
|
23
|
+
body = if @block && @block.arity == 0
|
24
|
+
@view_context.capture(&@block)
|
25
|
+
else
|
26
|
+
begin
|
27
|
+
args.first.to_str
|
28
|
+
rescue NoMethodError
|
29
|
+
""
|
30
|
+
end
|
31
|
+
end
|
32
|
+
ActiveSupport::SafeBuffer.new(body.presence || "")
|
33
|
+
end
|
34
|
+
|
35
|
+
alias_method :to_s, :content
|
36
|
+
alias_method :to_str, :content
|
37
|
+
|
38
|
+
def present?
|
39
|
+
@args.present? || @options.present? || @block
|
40
|
+
end
|
41
|
+
|
42
|
+
def empty?
|
43
|
+
@args.empty? || @options.empty? || !@block
|
44
|
+
end
|
45
|
+
|
46
|
+
alias_method :blank?, :empty?
|
47
|
+
|
48
|
+
def to_h
|
49
|
+
@options
|
50
|
+
end
|
51
|
+
|
52
|
+
alias_method :to_hash, :to_h
|
53
|
+
|
54
|
+
def with_partial_path(partial_path)
|
55
|
+
Value.new(@view_context, @slot_name, @args, options, @block, partial_path:)
|
56
|
+
end
|
57
|
+
|
58
|
+
def with_default_options(default_options)
|
59
|
+
options = TagOptionsMerger.call(default_options, @options)
|
60
|
+
Value.new(@view_context, @slot_name, @args, options, @block)
|
61
|
+
end
|
62
|
+
|
63
|
+
def respond_to_missing?(name, include_private = false)
|
64
|
+
name.start_with?("to_") || super
|
65
|
+
end
|
66
|
+
|
67
|
+
def method_missing(name, *args, **options)
|
68
|
+
if name.start_with?("to_") && args.none?
|
69
|
+
@args.first.public_send(name)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def render_in(view_context, &block)
|
74
|
+
view_context.render partial_path, **@options.to_h, &@block || block
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def partial_path
|
80
|
+
@partial_path || @slot_name.to_s
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Slotify
|
2
|
+
class ValueCollection
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
delegate_missing_to :@values
|
6
|
+
|
7
|
+
def initialize(values = [])
|
8
|
+
@values = values.is_a?(Value) ? [values] : values
|
9
|
+
end
|
10
|
+
|
11
|
+
def with_default_options(...)
|
12
|
+
ValueCollection.new(map { _1.with_default_options(...) })
|
13
|
+
end
|
14
|
+
|
15
|
+
def with_partial_path(...)
|
16
|
+
ValueCollection.new(map { _1.with_partial_path(...) })
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
@values.reduce(ActiveSupport::SafeBuffer.new) do |buffer, value|
|
21
|
+
buffer << value.to_s
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def render_in(view_context, &block)
|
26
|
+
@values.reduce(ActiveSupport::SafeBuffer.new) do |buffer, value|
|
27
|
+
buffer << value.render_in(view_context, &block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Slotify
|
2
|
+
class ValueOptions < ActiveSupport::OrderedOptions
|
3
|
+
def initialize(view_context, options = {})
|
4
|
+
@view_context = view_context
|
5
|
+
merge!(options)
|
6
|
+
end
|
7
|
+
|
8
|
+
def except(...)
|
9
|
+
ValueOptions.new(@view_context, to_h.except(...))
|
10
|
+
end
|
11
|
+
|
12
|
+
def slice(...)
|
13
|
+
ValueOptions.new(@view_context, to_h.slice(...))
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
@view_context.tag.attributes(self)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/slotify/version.rb
CHANGED
data/lib/slotify.rb
CHANGED
@@ -1,3 +1,21 @@
|
|
1
|
+
require "zeitwerk"
|
2
|
+
require "action_view"
|
1
3
|
require_relative "slotify/version"
|
4
|
+
require_relative "slotify/error"
|
5
|
+
|
6
|
+
loader = Zeitwerk::Loader.for_gem
|
7
|
+
loader.tag = "slotify"
|
8
|
+
loader.push_dir("#{__dir__}/slotify", namespace: Slotify)
|
9
|
+
loader.collapse("#{__dir__}/slotify/concerns")
|
10
|
+
loader.collapse("#{__dir__}/slotify/services")
|
11
|
+
loader.enable_reloading if ENV["RAILS_ENV"] == "development"
|
12
|
+
loader.setup
|
13
|
+
|
14
|
+
ActiveSupport.on_load :action_view do
|
15
|
+
prepend Slotify::Extensions::Base
|
16
|
+
ActionView::Template.prepend Slotify::Extensions::Template
|
17
|
+
ActionView::PartialRenderer.prepend Slotify::Extensions::PartialRenderer
|
18
|
+
end
|
19
|
+
|
2
20
|
module Slotify
|
3
21
|
end
|
metadata
CHANGED
@@ -1,15 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: slotify
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.1
|
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-
|
12
|
-
dependencies:
|
11
|
+
date: 2025-04-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: zeitwerk
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: actionview
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
13
41
|
description:
|
14
42
|
email:
|
15
43
|
executables: []
|
@@ -18,6 +46,18 @@ extra_rdoc_files: []
|
|
18
46
|
files:
|
19
47
|
- README.md
|
20
48
|
- lib/slotify.rb
|
49
|
+
- lib/slotify/concerns/helpers_concern.rb
|
50
|
+
- lib/slotify/concerns/inflection_helper.rb
|
51
|
+
- lib/slotify/error.rb
|
52
|
+
- lib/slotify/extensions/base.rb
|
53
|
+
- lib/slotify/extensions/partial_renderer.rb
|
54
|
+
- lib/slotify/extensions/template.rb
|
55
|
+
- lib/slotify/partial.rb
|
56
|
+
- lib/slotify/services/method_args_resolver.rb
|
57
|
+
- lib/slotify/services/tag_options_merger.rb
|
58
|
+
- lib/slotify/value.rb
|
59
|
+
- lib/slotify/value_collection.rb
|
60
|
+
- lib/slotify/value_options.rb
|
21
61
|
- lib/slotify/version.rb
|
22
62
|
homepage:
|
23
63
|
licenses:
|
@@ -38,8 +78,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
38
78
|
- !ruby/object:Gem::Version
|
39
79
|
version: '0'
|
40
80
|
requirements: []
|
41
|
-
rubygems_version: 3.
|
81
|
+
rubygems_version: 3.5.10
|
42
82
|
signing_key:
|
43
83
|
specification_version: 4
|
44
|
-
summary:
|
84
|
+
summary: Superpowered slots for your Rails partials.
|
45
85
|
test_files: []
|