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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 905eaee14b280c6b59fed9887ab8ecf8e1796ca341296debf5c532bbb83ccd5f
4
- data.tar.gz: a260b0e8f3eda211a58f48ac5389ebebc84935888fcf6355a00145338005f33f
3
+ metadata.gz: 3b774ed1d3d704f77cbeebc55d117488eb70f52c1a00527dda727d6cb9100d72
4
+ data.tar.gz: b8c6ce6b033715c768315671e6d3547925943825a100f5ea67e11d7df7b81ba6
5
5
  SHA512:
6
- metadata.gz: 5a4b87a2792a032cde484a793dea94025f091a1bfa2464938437dda769ada354b788193aa896831da5d10e1506d8ada25e6a41287cfd9ff539eda3447c88a7f0
7
- data.tar.gz: 714fcfd6cfa44f24c7c562f72edaf5833e8b25048d5de639b753f9359c97c7fa0586abb478200ea1a4c5700dbf8774b7e7623a25697084cf8674dc487b9a23e6
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="140">
2
-
3
-
1
+ <img src=".github/assets/slotify_wordmark.svg" width="200">
4
2
 
5
- #### _A superpowered slot system for Rails partials._
3
+ #### Superpowered slots for ActionView partials
6
4
 
7
5
  ----------
8
6
 
9
- ## Overview
7
+ ## Overview
10
8
 
11
- Slotify adds a lightweight (but powerful!) slot system for providing content to partials when rendering them.
9
+ Slotify adds an unobtrusive but powerful **content slot API** to ActionView partials.
12
10
 
13
- Slots are defined using a `strict locals`-style magic comment at the top of the partial.
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
- ```erb
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
- <%# slots: (title: "Example title", items: nil, link:) -%>
15
+ ###
20
16
 
21
- <div>
22
- <h1><%= title %></h1>
17
+ ## Slotify basics
23
18
 
24
- <% if items.any? %>
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
- <p>
35
- Example link: <%= partial.link_to link, class: "example-link" %>
36
- </p>
37
- </div>
21
+ ```erb
22
+ <%# slots: (slot_name: "default value", optional_slot_name: nil, required_slot_name:) -%>
38
23
  ```
39
24
 
40
- Content can then be provided to slots when rendering the partial:
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
- <%= render "my_partial", do |partial| %>
44
- <%= partial.with_title do %>
45
- This is a title
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
- <%= partial.with_item "Item 1" %>
49
- <%= partial.with_item "Item 2", class: "text-green-700" %>
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
- <% partial.with_link "example.com", "https://example.com", target: "_blank" %>
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
- Slots defined with singular names can only be called with content once whereas slots defined with plural names can be called multiple times.
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
- ### Requirements
64
+ The example above just scratches the surface of what Slotify slots can do.
58
65
 
59
- * `Rails 8.0+`
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
- ## Usage
68
+ ## Single vs multiple value slots
63
69
 
64
- 🚧 Work in progress! 🚧
70
+ > _Docs coming soon..._
65
71
 
66
- ### Defining slots
72
+ ## Slot arguments and options
67
73
 
68
- Slots are defined using a `strict locals`-style magic comment at the top of the partial template.
74
+ > _Docs coming soon..._
69
75
 
70
- ```erb
71
- <%# slots: (title: "Example title", lists: nil, quotes: nil, website_link:) -%>
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
- * Singular slots can only accept one entry, plural slots can accept many.
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
- ### A more complete example
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
@@ -1,3 +1,3 @@
1
1
  module Slotify
2
- VERSION = "0.0.0"
2
+ VERSION = "0.0.1.alpha.0"
3
3
  end
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-03-31 00:00:00.000000000 Z
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: '0'
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: A superpowered slot system for Rails partials.
84
+ summary: Superpowered slots for your Rails partials.
45
85
  test_files: []