slotify 0.0.0 → 0.0.1.alpha.0
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 +108 -49
- data/lib/slotify/entry.rb +83 -0
- data/lib/slotify/entry_collection.rb +31 -0
- data/lib/slotify/entry_options.rb +20 -0
- data/lib/slotify/error.rb +22 -0
- data/lib/slotify/extensions/base.rb +36 -0
- data/lib/slotify/extensions/partial_renderer.rb +40 -0
- data/lib/slotify/extensions/template.rb +51 -0
- data/lib/slotify/helpers.rb +30 -0
- data/lib/slotify/partial.rb +122 -0
- data/lib/slotify/slotify_helpers.rb +18 -0
- data/lib/slotify/tag_options_merger.rb +54 -0
- data/lib/slotify/utils.rb +50 -0
- data/lib/slotify/version.rb +1 -1
- data/lib/slotify.rb +16 -0
- metadata +46 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b774ed1d3d704f77cbeebc55d117488eb70f52c1a00527dda727d6cb9100d72
|
4
|
+
data.tar.gz: b8c6ce6b033715c768315671e6d3547925943825a100f5ea67e11d7df7b81ba6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ad4a579d74c471684a40b0d60cd8362325cb529d13a362c73d23f5aa67f7383d8d4c9853ebe3e9e4120a31126c4a0069688c924a38ceb8ae0d857e0ba3935b59
|
7
|
+
data.tar.gz: c64f9c44d9880d0488acac9bcc0a4882780b1517024ac86c3b73177f287817bf70ea52d036b74516c0e3f7a4681453acdfccda46b1d9b98ca80f011a09857ebf
|
data/README.md
CHANGED
@@ -1,81 +1,142 @@
|
|
1
|
-
<img src=".github/assets/slotify_wordmark.svg" width="
|
2
|
-
|
3
|
-
|
1
|
+
<img src=".github/assets/slotify_wordmark.svg" width="200">
|
4
2
|
|
5
|
-
####
|
3
|
+
#### Superpowered slots for ActionView partials
|
6
4
|
|
7
5
|
----------
|
8
6
|
|
9
|
-
## Overview
|
7
|
+
## Overview
|
10
8
|
|
11
|
-
Slotify adds
|
9
|
+
Slotify adds an unobtrusive but powerful **content slot API** to ActionView partials.
|
12
10
|
|
13
|
-
Slots are
|
14
|
-
Slot content is accessed via 'regular' local variables within the template.
|
11
|
+
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.
|
15
12
|
|
16
|
-
|
17
|
-
<!-- views/_my_partial_.html.erb -->
|
13
|
+
Slotified partials are **a great tool for building components** in a Rails app if you want to stay close to _The Rails Way™️_ or just want to avoid the additional overhead and learning curve of libraries like [ViewComponent](https://viewcomponent.org/) or [Phlex](https://www.phlex.fun/).
|
18
14
|
|
19
|
-
|
15
|
+
###
|
20
16
|
|
21
|
-
|
22
|
-
<h1><%= title %></h1>
|
17
|
+
## Slotify basics
|
23
18
|
|
24
|
-
|
25
|
-
<ul>
|
26
|
-
<%= items.each do |item| %>
|
27
|
-
<li <%= item.options %>>
|
28
|
-
<%= item %>
|
29
|
-
</li>
|
30
|
-
<% end%>
|
31
|
-
</ul>
|
32
|
-
<% end %>
|
19
|
+
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)).
|
33
20
|
|
34
|
-
|
35
|
-
|
36
|
-
</p>
|
37
|
-
</div>
|
21
|
+
```erb
|
22
|
+
<%# slots: (slot_name: "default value", optional_slot_name: nil, required_slot_name:) -%>
|
38
23
|
```
|
39
24
|
|
40
|
-
|
25
|
+
Slot content is accessed via standard local variables within the partial. So a simple slot-enabled `article` partial template might look something like this:
|
41
26
|
|
42
27
|
```erb
|
43
|
-
|
44
|
-
|
45
|
-
|
28
|
+
<!-- _article.html.erb -->
|
29
|
+
|
30
|
+
<%# slots: (heading: "Default title", body: nil) -%>
|
31
|
+
|
32
|
+
<article>
|
33
|
+
<h1><%= heading %></h1>
|
34
|
+
<% if body.present? %>
|
35
|
+
<div>
|
36
|
+
<%= body %>
|
37
|
+
</div>
|
46
38
|
<% end %>
|
39
|
+
</article>
|
40
|
+
```
|
41
|
+
|
42
|
+
> [!NOTE]
|
43
|
+
> _The above code should feel familiar if you have used partials 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!)._
|
47
44
|
|
48
|
-
|
49
|
-
|
45
|
+
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.
|
46
|
+
|
47
|
+
For example, here our `article` partial is being rendered with content for the `heading` and `body` slots that were defined above:
|
48
|
+
|
49
|
+
```erb
|
50
|
+
<!-- index.html.erb -->
|
50
51
|
|
51
|
-
|
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 %>
|
52
58
|
<% end %>
|
53
59
|
```
|
54
60
|
|
55
|
-
|
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)._
|
56
63
|
|
57
|
-
|
64
|
+
The example above just scratches the surface of what Slotify slots can do.
|
58
65
|
|
59
|
-
|
60
|
-
* `Ruby 3.1+`
|
66
|
+
You can [jump to a more full-featured example here](#full-example) or read on to learn more...
|
61
67
|
|
62
|
-
##
|
68
|
+
## Single vs multiple value slots
|
63
69
|
|
64
|
-
|
70
|
+
> _Docs coming soon..._
|
65
71
|
|
66
|
-
|
72
|
+
## Slot arguments and options
|
67
73
|
|
68
|
-
|
74
|
+
> _Docs coming soon..._
|
69
75
|
|
70
|
-
|
71
|
-
|
76
|
+
## Using helpers with slots
|
77
|
+
|
78
|
+
> _Docs coming soon..._
|
79
|
+
|
80
|
+
## Rendering slot contents
|
81
|
+
|
82
|
+
> _Docs coming soon..._
|
83
|
+
|
84
|
+
<a name="defining-slots" id="defining-slots"></a>
|
85
|
+
## Defining slots
|
86
|
+
|
87
|
+
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.
|
88
|
+
|
89
|
+
> _Docs coming soon..._
|
90
|
+
|
91
|
+
### Required slots
|
92
|
+
|
93
|
+
> _Docs coming soon..._
|
94
|
+
|
95
|
+
### Optional slots
|
96
|
+
|
97
|
+
> _Docs coming soon..._
|
98
|
+
|
99
|
+
### Setting default values
|
100
|
+
|
101
|
+
> _Docs coming soon..._
|
102
|
+
|
103
|
+
### Using alongside strict locals
|
104
|
+
|
105
|
+
> _Docs coming soon..._
|
106
|
+
|
107
|
+
## Slotify API
|
108
|
+
|
109
|
+
> _Docs coming soon..._
|
110
|
+
|
111
|
+
## Installation
|
112
|
+
|
113
|
+
Add the following to your Rails app Gemfile:
|
114
|
+
|
115
|
+
```rb
|
116
|
+
gem "slotify"
|
72
117
|
```
|
73
118
|
|
74
|
-
|
75
|
-
* Slots can be required (no default value) or optional.
|
76
|
-
* Optional slots can additionaly specify default content as needed.
|
119
|
+
And then run `bundle install`. You are good to go!
|
77
120
|
|
78
|
-
|
121
|
+
## Requirements
|
122
|
+
|
123
|
+
* `Rails 7.1+`
|
124
|
+
* `Ruby 3.1+`
|
125
|
+
|
126
|
+
## Credits
|
127
|
+
|
128
|
+
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).
|
129
|
+
|
130
|
+
`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)!
|
131
|
+
|
132
|
+
<br>
|
133
|
+
|
134
|
+
---
|
135
|
+
|
136
|
+
<br>
|
137
|
+
<a name="full-example" id="full-example"></a>
|
138
|
+
|
139
|
+
## A more full-featured example
|
79
140
|
|
80
141
|
```erb
|
81
142
|
<!-- views/_example.html.erb -->
|
@@ -145,6 +206,4 @@ Slots are defined using a `strict locals`-style magic comment at the top of the
|
|
145
206
|
<% end %>
|
146
207
|
```
|
147
208
|
|
148
|
-
## Credits
|
149
209
|
|
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,83 @@
|
|
1
|
+
module Slotify
|
2
|
+
class Entry
|
3
|
+
include Utils
|
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, **kwargs)
|
10
|
+
@view_context = view_context
|
11
|
+
@slot_name = slot_name.to_sym
|
12
|
+
@args = args
|
13
|
+
@options = options
|
14
|
+
@block = block
|
15
|
+
@partial_path = partial_path
|
16
|
+
end
|
17
|
+
|
18
|
+
def options
|
19
|
+
EntryOptions.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
|
+
Entry.new(@view_context, @slot_name, @args, options.to_h, @block, partial_path:)
|
56
|
+
end
|
57
|
+
|
58
|
+
def with_default_options(default_options)
|
59
|
+
options = merge_tag_options(default_options, @options)
|
60
|
+
Entry.new(@view_context, @slot_name, @args, options.to_h, @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 EntryCollection
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
delegate_missing_to :@entries
|
6
|
+
|
7
|
+
def initialize(entries = [])
|
8
|
+
@entries = entries.is_a?(Entry) ? [entries] : entries
|
9
|
+
end
|
10
|
+
|
11
|
+
def with_default_options(...)
|
12
|
+
EntryCollection.new(map { _1.with_default_options(...) })
|
13
|
+
end
|
14
|
+
|
15
|
+
def with_partial_path(...)
|
16
|
+
EntryCollection.new(map { _1.with_partial_path(...) })
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
@entries.reduce(ActiveSupport::SafeBuffer.new) do |buffer, entry|
|
21
|
+
buffer << entry.to_s
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def render_in(view_context, &block)
|
26
|
+
@entries.reduce(ActiveSupport::SafeBuffer.new) do |buffer, entry|
|
27
|
+
buffer << entry.render_in(view_context, &block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Slotify
|
2
|
+
class EntryOptions < ActiveSupport::OrderedOptions
|
3
|
+
def initialize(view_context, options = {})
|
4
|
+
@view_context = view_context
|
5
|
+
merge!(options)
|
6
|
+
end
|
7
|
+
|
8
|
+
def except(...)
|
9
|
+
EntryOptions.new(@view_context, to_h.except(...))
|
10
|
+
end
|
11
|
+
|
12
|
+
def slice(...)
|
13
|
+
EntryOptions.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
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Slotify
|
2
|
+
class UnknownSlotError < NameError
|
3
|
+
end
|
4
|
+
|
5
|
+
class MissingRequiredSlotError < ArgumentError
|
6
|
+
end
|
7
|
+
|
8
|
+
class MultipleSlotEntriesError < ArgumentError
|
9
|
+
end
|
10
|
+
|
11
|
+
class SlotsAccessError < RuntimeError
|
12
|
+
end
|
13
|
+
|
14
|
+
class UndefinedSlotError < StandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
class SlotArgumentError < ArgumentError
|
18
|
+
end
|
19
|
+
|
20
|
+
class StrictSlotsError < ArgumentError
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Slotify
|
2
|
+
module Extensions
|
3
|
+
module Base
|
4
|
+
extend SlotifyHelpers
|
5
|
+
|
6
|
+
slotify_helpers(
|
7
|
+
*ActionView::Helpers::UrlHelper.instance_methods(false),
|
8
|
+
*ActionView::Helpers::TagHelper.instance_methods(false)
|
9
|
+
)
|
10
|
+
|
11
|
+
attr_reader :partial
|
12
|
+
|
13
|
+
def render(target = {}, locals = {}, &block)
|
14
|
+
@partial = Slotify::Partial.new(self)
|
15
|
+
super
|
16
|
+
ensure
|
17
|
+
@partial = partial.outer_partial
|
18
|
+
end
|
19
|
+
|
20
|
+
def _layout_for(*args, &block)
|
21
|
+
if block && args.first.is_a?(Symbol)
|
22
|
+
capture_with_outer_partial_access(*args, &block)
|
23
|
+
else
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def capture_with_outer_partial_access(*args, &block)
|
29
|
+
inner_partial, @partial = partial, partial.outer_partial
|
30
|
+
inner_partial.capture(*args, &block)
|
31
|
+
ensure
|
32
|
+
@partial = inner_partial
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,40 @@
|
|
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
|
+
|
9
|
+
view.capture_with_outer_partial_access(&block) if block
|
10
|
+
partial.with_strict_slots(template.strict_slots_keys)
|
11
|
+
locals = locals.merge(partial.slot_locals)
|
12
|
+
|
13
|
+
decorate_strict_slots_errors do
|
14
|
+
super(view, locals, template, layout, block)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def decorate_strict_slots_errors(&block)
|
19
|
+
yield
|
20
|
+
rescue ActionView::Template::Error => error
|
21
|
+
if missing_strict_locals_error?(error)
|
22
|
+
local_names = error.cause.message.scan(/\s:(\w+)/).flatten
|
23
|
+
if local_names.any?
|
24
|
+
slot_names = local_names.intersection(error.template.strict_slots_keys).map { ":#{_1}" }
|
25
|
+
if slot_names.any?
|
26
|
+
message = "missing #{"slot".pluralize(slot_names.size)}: #{slot_names.join(", ")} for #{error.template.short_identifier}"
|
27
|
+
raise Slotify::StrictSlotsError, message
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
raise error
|
32
|
+
end
|
33
|
+
|
34
|
+
def missing_strict_locals_error?(error)
|
35
|
+
(defined?(ActionView::StrictLocalsError) && error.cause.is_a?(ActionView::StrictLocalsError)) ||
|
36
|
+
(error.cause.is_a?(ArgumentError) && error.cause.message.match(/missing local/))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,51 @@
|
|
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
|
+
|
45
|
+
strict_slots_keys.each_with_object(+super) do |key, code|
|
46
|
+
code << "#{key} = partial.content_for(:#{key}, #{key});"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Slotify
|
2
|
+
class Helpers
|
3
|
+
include Utils
|
4
|
+
|
5
|
+
def initialize(view_context)
|
6
|
+
@view_context = view_context
|
7
|
+
end
|
8
|
+
|
9
|
+
def respond_to_missing?(name, include_private = false)
|
10
|
+
@view_context.respond_to?(name) || @view_context.tag.respond_to?(name)
|
11
|
+
end
|
12
|
+
|
13
|
+
def method_missing(name, *args, **options, &block)
|
14
|
+
results = with_resolved_args(args, options, block) do |rargs, roptions, rblock|
|
15
|
+
call_helper(name, *rargs, **roptions.to_h, &rblock)
|
16
|
+
end
|
17
|
+
results.reduce(ActiveSupport::SafeBuffer.new) { _1 << _2 }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def call_helper(name, ...)
|
23
|
+
if @view_context.respond_to?(name)
|
24
|
+
@view_context.public_send(name, ...)
|
25
|
+
else
|
26
|
+
@view_context.tag.public_send(name, ...)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Slotify
|
2
|
+
class Partial
|
3
|
+
include Utils
|
4
|
+
|
5
|
+
attr_reader :outer_partial
|
6
|
+
|
7
|
+
def initialize(view_context)
|
8
|
+
@view_context = view_context
|
9
|
+
@outer_partial = view_context.partial
|
10
|
+
@entries = []
|
11
|
+
@strict_slots = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def content_for(slot_name, fallback_value = nil)
|
15
|
+
raise SlotsAccessError, "slot content cannot be accessed from outside the partial" unless slots_defined?
|
16
|
+
raise UnknownSlotError, "unknown slot :#{slot_name}" unless slot_defined?(slot_name)
|
17
|
+
|
18
|
+
entries = slot_entries(slot_name)
|
19
|
+
if entries.none? && !fallback_value.nil?
|
20
|
+
entries = add_entries(slot_name, to_array(fallback_value))
|
21
|
+
end
|
22
|
+
|
23
|
+
singular?(slot_name) ? entries.first : EntryCollection.new(entries)
|
24
|
+
end
|
25
|
+
|
26
|
+
def content_for?(slot_name)
|
27
|
+
raise SlotsAccessError, "slot content 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_entries(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
|
+
@strict_slots.map { [_1, content_for(_1).presence] }.to_h.compact
|
47
|
+
end
|
48
|
+
|
49
|
+
def with_strict_slots(strict_slot_names)
|
50
|
+
@strict_slots = strict_slot_names.map(&:to_sym)
|
51
|
+
validate_slots!
|
52
|
+
end
|
53
|
+
|
54
|
+
def helpers
|
55
|
+
@helpers || Helpers.new(@view_context)
|
56
|
+
end
|
57
|
+
|
58
|
+
def respond_to_missing?(name, include_private = false)
|
59
|
+
name.start_with?("with_") || helpers.respond_to?(name)
|
60
|
+
end
|
61
|
+
|
62
|
+
def method_missing(name, *args, **options, &block)
|
63
|
+
if name.start_with?("with_")
|
64
|
+
slot_name = name.to_s.delete_prefix("with_")
|
65
|
+
if singular?(slot_name)
|
66
|
+
add_entry(slot_name, args, options, block)
|
67
|
+
else
|
68
|
+
add_entries(slot_name, args.first, options, block)
|
69
|
+
end
|
70
|
+
else
|
71
|
+
helpers.public_send(name, *args, **options, &block)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def slots_defined?
|
78
|
+
!@strict_slots.nil?
|
79
|
+
end
|
80
|
+
|
81
|
+
def slot_defined?(slot_name)
|
82
|
+
slot_name && slots_defined? && @strict_slots.include?(slot_name.to_sym)
|
83
|
+
end
|
84
|
+
|
85
|
+
def slot_entries(slot_name)
|
86
|
+
@entries.filter { _1.slot_name == singularize(slot_name) }
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_entries(slot_name, collection, options = {}, block = nil)
|
90
|
+
unless collection.respond_to?(:each)
|
91
|
+
raise ArgumentError, "expected array to be passed to slot :#{slot_name} (received #{collection.class.name})"
|
92
|
+
end
|
93
|
+
|
94
|
+
collection.map { add_entry(slot_name, _1, options, block) }
|
95
|
+
end
|
96
|
+
|
97
|
+
def add_entry(slot_name, args = [], options = {}, block = nil)
|
98
|
+
with_resolved_args(args, options, block) do |rargs, roptions, rblock|
|
99
|
+
@entries << Entry.new(@view_context, singularize(slot_name), rargs, roptions, rblock)
|
100
|
+
end
|
101
|
+
|
102
|
+
@entries.last
|
103
|
+
end
|
104
|
+
|
105
|
+
def validate_slots!
|
106
|
+
return if @strict_slots.nil?
|
107
|
+
|
108
|
+
singular_slots = @strict_slots.map { singularize(_1) }
|
109
|
+
slots_called = @entries.map(&:slot_name).uniq
|
110
|
+
undefined_slots = slots_called - singular_slots
|
111
|
+
|
112
|
+
if undefined_slots.any?
|
113
|
+
raise UndefinedSlotError, "missing slot #{"definition".pluralize(undefined_slots.size)} for `#{undefined_slots.map { ":#{_1}(s)" }.join(", ")}`"
|
114
|
+
end
|
115
|
+
|
116
|
+
@strict_slots.filter { singular?(_1) }.each do |slot_name|
|
117
|
+
entries = slot_entries(slot_name)
|
118
|
+
raise MultipleSlotEntriesError, "slot :#{slot_name} called #{entries.size} times (expected 1)" if entries.many?
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Slotify
|
2
|
+
module SlotifyHelpers
|
3
|
+
def slotify_helpers(*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
|
+
results = Utils.with_resolved_args(args, kwargs, block) do
|
9
|
+
super(*_1, **_2.to_h, &_3 || block)
|
10
|
+
end
|
11
|
+
|
12
|
+
results.reduce(ActiveSupport::SafeBuffer.new) { _1 << _2 }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
prepend proxy
|
16
|
+
end
|
17
|
+
end
|
18
|
+
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,50 @@
|
|
1
|
+
module Slotify
|
2
|
+
module Utils
|
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
|
+
|
18
|
+
def to_array(input)
|
19
|
+
input.is_a?(Array) ? input : [input]
|
20
|
+
end
|
21
|
+
|
22
|
+
def merge_tag_options(...)
|
23
|
+
TagOptionsMerger.call(...)
|
24
|
+
end
|
25
|
+
|
26
|
+
def with_resolved_args(args = [], options = {}, block = nil)
|
27
|
+
args = args.is_a?(Array) ? args.clone : [args]
|
28
|
+
entry_index = args.index { _1.is_a?(EntryCollection) || _1.is_a?(Entry) }
|
29
|
+
if entry_index.nil?
|
30
|
+
[yield(args, options, block)]
|
31
|
+
else
|
32
|
+
target = args[entry_index]
|
33
|
+
entries = target.is_a?(EntryCollection) ? target : [target]
|
34
|
+
entries.map do |entry|
|
35
|
+
cloned_args = args.clone
|
36
|
+
cloned_args[entry_index, 1] = entry.args.clone
|
37
|
+
|
38
|
+
yield(
|
39
|
+
cloned_args,
|
40
|
+
merge_tag_options(options, entry.options),
|
41
|
+
entry.block || block
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module_function :merge_tag_options
|
48
|
+
module_function :with_resolved_args
|
49
|
+
end
|
50
|
+
end
|
data/lib/slotify/version.rb
CHANGED
data/lib/slotify.rb
CHANGED
@@ -1,3 +1,19 @@
|
|
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.enable_reloading if ENV["RAILS_ENV"] == "development"
|
10
|
+
loader.setup
|
11
|
+
|
2
12
|
module Slotify
|
3
13
|
end
|
14
|
+
|
15
|
+
ActiveSupport.on_load :action_view do
|
16
|
+
prepend Slotify::Extensions::Base
|
17
|
+
ActionView::Template.prepend Slotify::Extensions::Template
|
18
|
+
ActionView::PartialRenderer.prepend Slotify::Extensions::PartialRenderer
|
19
|
+
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.0
|
4
|
+
version: 0.0.1.alpha.0
|
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-04 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/entry.rb
|
50
|
+
- lib/slotify/entry_collection.rb
|
51
|
+
- lib/slotify/entry_options.rb
|
52
|
+
- lib/slotify/error.rb
|
53
|
+
- lib/slotify/extensions/base.rb
|
54
|
+
- lib/slotify/extensions/partial_renderer.rb
|
55
|
+
- lib/slotify/extensions/template.rb
|
56
|
+
- lib/slotify/helpers.rb
|
57
|
+
- lib/slotify/partial.rb
|
58
|
+
- lib/slotify/slotify_helpers.rb
|
59
|
+
- lib/slotify/tag_options_merger.rb
|
60
|
+
- lib/slotify/utils.rb
|
21
61
|
- lib/slotify/version.rb
|
22
62
|
homepage:
|
23
63
|
licenses:
|
@@ -34,12 +74,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
34
74
|
version: 3.1.0
|
35
75
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
76
|
requirements:
|
37
|
-
- - "
|
77
|
+
- - ">"
|
38
78
|
- !ruby/object:Gem::Version
|
39
|
-
version:
|
79
|
+
version: 1.3.1
|
40
80
|
requirements: []
|
41
81
|
rubygems_version: 3.3.3
|
42
82
|
signing_key:
|
43
83
|
specification_version: 4
|
44
|
-
summary:
|
84
|
+
summary: Superpowered slots for your Rails partials.
|
45
85
|
test_files: []
|