phlex-slotable 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1aa68f30783948c69ae6e3a9018e3e0fe828b8d1e3392208bbd926dead27761
4
- data.tar.gz: f60047a296679c099258024d72e23d13a252bd4f5fd38576769716218785d962
3
+ metadata.gz: 6daa3c251793367884c7260e217d1df50891db3e5e5d07d3510dce8dc9dab868
4
+ data.tar.gz: 95a6c59d0c7571933821a8df89b76542789d16fe222677b7be48af06730bd7f1
5
5
  SHA512:
6
- metadata.gz: cf5b19581bab8cb2ba50e91e44f9489b40cbdae7c0873310a98637be51cbac6212e8621a95f67bd8d5d1ad1286533b66023847c4ac4eacbe2a7f867635b2bfaa
7
- data.tar.gz: d1e6a2cb591541105802d4338b857baf99ef5817aacbdd644f5a6ad3e77a4ac60669c8b5f0d9f7bdae63565e05f6fe7cbe6852e29aac88aa31366def63a5ac01
6
+ metadata.gz: 1c04f7d85f3068f064d8b45566de267975cd73d86d5fe7b34114865836b8cdf064cc218f01c86ddc5ffce9b942d39b1ca63f4188af23222e86f4cd6570ad516b
7
+ data.tar.gz: 14ac74deabd99d4af0b9370279bb0bf592d9116426c7037f40cee94c198f2d92a3dd034577218b6aaa0fa118cc7e54869966177ada7564ee96b00ca2d0e78687
data/CHANGELOG.md CHANGED
@@ -1,5 +1,65 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2024-02-14
4
+
5
+ - Match Slotable peformance with DeferredRender
6
+ ```
7
+ ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]
8
+ Warming up --------------------------------------
9
+ Deferred 26.779k i/100ms
10
+ Slotable 0.2.0 21.636k i/100ms
11
+ Slotable 0.3.0 27.013k i/100ms
12
+ Calculating -------------------------------------
13
+ Deferred 267.884k (± 0.6%) i/s - 1.366M in 5.098391s
14
+ Slotable 0.2.0 216.193k (± 0.4%) i/s - 1.082M in 5.003961s
15
+ Slotable 0.3.0 270.082k (± 0.5%) i/s - 1.351M in 5.001001s
16
+ ```
17
+ *stephannv*
18
+
19
+ - Allow polymorphic slots
20
+ ```ruby
21
+ class CardComponent < Phlex::HTML
22
+ include Phlex::Slotable
23
+
24
+ slot :avatar, types: { icon: IconComponent, image: ImageComponent }
25
+
26
+ def template
27
+ if avatar_slot?
28
+ render avatar_slot
29
+ end
30
+ end
31
+ end
32
+
33
+ render CardComponent.new do |card|
34
+ if user
35
+ card.with_image_avatar(src: user.image_url)
36
+ else
37
+ card.with_icon_avatar(name: :user)
38
+ end
39
+ end
40
+ ```
41
+
42
+ *stephannv*
43
+
44
+ ## [0.2.0] - 2024-02-13
45
+
46
+ - Allow view slots using string as class name
47
+
48
+ *stephannv*
49
+
50
+ - Allow lambda slots
51
+
52
+ *stephannv*
53
+
3
54
  ## [0.1.0] - 2024-02-12
55
+ - Add single and multi slots
56
+
57
+ *stephannv*
58
+
59
+ - Add generic slots
60
+
61
+ *stephannv*
62
+
63
+ - Add view slots
4
64
 
5
- - Initial release
65
+ *stephannv*
data/README.md CHANGED
@@ -1,25 +1,28 @@
1
+ > [!WARNING]
2
+ > Please note that Phlex::Slotable is currently under development and may undergo changes to its API before reaching the stable release (1.0.0). As a result, there may be breaking changes that affect its usage.
3
+
1
4
  # Phlex::Slotable
2
5
 
3
- Phlex::Slotable enables slots feature to Phlex views. Inspired by ViewComponent.
6
+ Phlex::Slotable enables slots feature to [Phlex](https://www.phlex.fun/) views. Inspired by ViewComponent.
4
7
 
5
8
  ## Installation
6
9
 
7
10
  Install the gem and add to the application's Gemfile by executing:
8
11
 
9
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
12
+ $ bundle add phlex-slotable
10
13
 
11
14
  If bundler is not being used to manage dependencies, install the gem by executing:
12
15
 
13
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
16
+ $ gem install phlex-slotable
14
17
 
15
18
  ## Usage
16
19
 
17
20
  #### Basic
18
21
 
19
- Include `Phlex::Slotable` to your Phlex view and use `slot` class method to define slots.
22
+ To incorportate slots into your Phlex views, include `Phlex::Slotable` and utilize `slot` class method to define them.
20
23
 
21
- - `slot :slot_name` defines a single slot that will be rendered at most once per component
22
- - `slot :slot_name, many: true` defines a slot that can be rendered multiple times per component
24
+ - `slot :slot_name` declaration establishes a single slot intended for rendering once within a view
25
+ - `slot :slot_name, many: true` denotes a slot capable of being rendered multiple times within a view
23
26
 
24
27
  ```ruby
25
28
  class BlogComponent < Phlex::HTML
@@ -32,9 +35,9 @@ class BlogComponent < Phlex::HTML
32
35
  end
33
36
  ```
34
37
 
35
- To render a single slot, render `{slot_name}_slot`, eg. `render header_slot`.
38
+ To render a single slot, utilize the `{slot_name}_slot` method. For example, you can render the `header_slot` using `render header_slot`.
36
39
 
37
- To render a multi slot, iterate over `{slot_name}_slots` and render each element, eg. `post_slots.each { |s| render s }`
40
+ For multi-slot rendering, iterate over the `{slot_name}_slots` collection and and render each slot individually, eg. `post_slots.each { |s| render s }`.
38
41
 
39
42
  ```ruby
40
43
  class BlogComponent < Phlex::HTML
@@ -44,11 +47,11 @@ class BlogComponent < Phlex::HTML
44
47
  slot :post, many: true
45
48
 
46
49
  def template
47
- div id: "header" do
50
+ div id: "header" do
48
51
  render header_slot
49
52
  end
50
53
 
51
- div id: "main" do
54
+ div id: "main" do
52
55
  post_slots.each do |slot|
53
56
  p { render slot }
54
57
  end
@@ -59,7 +62,7 @@ class BlogComponent < Phlex::HTML
59
62
  end
60
63
  ```
61
64
 
62
- To set slot content, you need to use `with_{slot_name}` when rendering the view:
65
+ When setting slot content, ensure to utilize the `with_{slot_name}` method while rendering the view:
63
66
 
64
67
  ```ruby
65
68
  class MyPage < Phlex::HTML
@@ -96,7 +99,7 @@ This will output:
96
99
 
97
100
  #### Predicate methods
98
101
 
99
- You can check if a slot has been passed to the component using `{slot_name}_slot?` when it is a single slot, or `{slot_name}_slots?` when it is a multi slot.
102
+ You can verify whether a slot has been provided to the view using `{slot_name}_slot?` for single slots or `{slot_name}_slots?` when for multi-slots.
100
103
 
101
104
  ```ruby
102
105
  class BlogComponent < Phlex::HTML
@@ -106,13 +109,13 @@ class BlogComponent < Phlex::HTML
106
109
  slot :post, many: true
107
110
 
108
111
  def template
109
- if header_slot?
112
+ if header_slot?
110
113
  div id: "header" do
111
114
  render header_slot
112
115
  end
113
116
  end
114
117
 
115
- div id: "main" do
118
+ div id: "main" do
116
119
  if post_slots?
117
120
  post_slots.each do |slot|
118
121
  p { render slot }
@@ -129,11 +132,11 @@ end
129
132
 
130
133
  #### View slot
131
134
 
132
- Slots can render other views, you just need to pass the view class name to `slot` method.
135
+ Slots have the capability to render other views, Simply pass the view class name to the `slot` method.
133
136
 
134
137
  ```ruby
135
138
  class HeaderComponent < Phlex::HTML
136
- def initialize(size:)
139
+ def initialize(size:)
137
140
  @size = size
138
141
  end
139
142
 
@@ -143,7 +146,7 @@ class HeaderComponent < Phlex::HTML
143
146
  end
144
147
 
145
148
  class PostComponent < Phlex::HTML
146
- def initialize(featured:)
149
+ def initialize(featured:)
147
150
  @featured = featured
148
151
  end
149
152
 
@@ -159,13 +162,13 @@ class BlogComponent < Phlex::HTML
159
162
  slot :post, PostComponent, many: true
160
163
 
161
164
  def template
162
- if header_slot?
165
+ if header_slot?
163
166
  div id: "header" do
164
167
  render header_slot
165
168
  end
166
169
  end
167
170
 
168
- div id: "main" do
171
+ div id: "main" do
169
172
  if post_slots?
170
173
  post_slots.each { render slot }
171
174
 
@@ -207,11 +210,147 @@ The output:
207
210
  </div>
208
211
  ```
209
212
 
210
- ## Roadmap
213
+ You can pass the class name as a string for cases where the class isn't evaluated yet, such as with inner classes. For example:
214
+ ```ruby
215
+ class BlogComponent < Phlex::HTML
216
+ include Phlex::Slotable
217
+
218
+ # This will not work
219
+ slot :header, HeaderComponent # uninitialized constant BlogComponent::HeaderComponent
220
+ # You should do this
221
+ slot :header, "HeaderComponent"
222
+
223
+ private
224
+
225
+ class HeaderComponent < Phlex::HTML
226
+ # ...
227
+ end
228
+ end
229
+ ```
230
+
231
+ #### Lambda slots
232
+ Lambda slots are valuable when you prefer not to create another component for straightforward structures or when you need to render another view with specific parameters
233
+ ```ruby
234
+
235
+ class BlogComponent < Phlex::HTML
236
+ include Phlex::Slotable
237
+
238
+ slot :header, ->(size:, &content) { render HeaderComponent.new(size: size, color: "blue"), &content }
239
+ slot :post, ->(featured:, &content) { span(class: featured ? "featured" : nil, &content) }, many: true
240
+ end
241
+
242
+ class MyPage < Phlex::HTML
243
+ def template
244
+ render BlogComponent.new do |blog|
245
+ blog.with_header(size: :lg) { "Hello World!" }
246
+
247
+ blog.with_post(featured: true) { "Post A" }
248
+ blog.with_post { "Post B" }
249
+ blog.with_post { "Post C" }
250
+ end
251
+ end
252
+ end
253
+ ```
254
+
255
+ You can access the internal view state within lambda slots. For example:
256
+ ```ruby
257
+ class BlogComponent < Phlex::HTML
258
+ include Phlex::Slotable
259
+
260
+ slot :header, ->(size:, &content) { render HeaderComponent.new(size: size, color: @header_color), &content }
261
+
262
+ def initialize(header_color:)
263
+ @header_color = header_color
264
+ end
265
+ end
266
+
267
+ class MyPage < Phlex::HTML
268
+ def template
269
+ render BlogComponent.new(header_color: "red") do |blog|
270
+ blog.with_header(size: :lg) { "Hello World!" }
271
+ end
272
+ end
273
+ end
274
+ ```
275
+
276
+ #### Polymorphic slots
277
+ Polymorphic slots can render one of several possible slots, allowing for flexibility in component content. This feature is particularly useful when you require a fixed structure but need to accommodate different types of content. To implement this, simply pass a types hash containing the types along with corresponding slot definitions.
278
+
279
+ ```ruby
280
+ class CardComponent < Phlex::HTML
281
+ include Phlex::Slotable
211
282
 
212
- [] Accepts Strings as view class name
213
- [] Allow lambda slots
214
- [] Allow polymorphic slots
283
+ slot :avatar, types: { icon: IconComponent, image: ImageComponent }
284
+
285
+ def template
286
+ if avatar_slot?
287
+ figure id: "avatar" do
288
+ render avatar_slot
289
+ end
290
+ end
291
+ end
292
+ end
293
+ ```
294
+
295
+ This allows you to set the icon slot using `with_icon_avatar` or the image slot using `with_image_avatar`:
296
+ ```ruby
297
+ class UserCardComponent < Phlex::HTML
298
+ def initialize(user:)
299
+ @user = user
300
+ end
301
+
302
+ def template
303
+ render CardComponent.new do |card|
304
+ if @user.image?
305
+ card.with_image_avatar(src: @user.image)
306
+ else
307
+ card.with_icon_avatar(name: :user)
308
+ end
309
+ end
310
+ end
311
+ end
312
+ ```
313
+
314
+ Please note that you can still utilize the other slot definition APIs:
315
+ ```ruby
316
+ class CardComponent < Phlex::HTML
317
+ include Phlex::Slotable
318
+
319
+ slot :avatar, types: {
320
+ icon: IconComponent,
321
+ image: "ImageComponent",
322
+ text: ->(size:, &content) { span(class: "text-#{size}", &content) }
323
+ }, many: true
324
+
325
+ def template
326
+ if avatar_slots?
327
+ avatar_slots.each do |slot|
328
+ render slot
329
+ end
330
+ end
331
+
332
+ span { "Count: #{avatar_slots.size}" }
333
+ end
334
+
335
+ ...
336
+ end
337
+
338
+ class UsersCardComponent < Phlex::HTML
339
+ def template
340
+ render CardComponent.new do |card|
341
+ card.with_image_avatar(src: @user.image)
342
+ card.with_icon_avatar(name: :user)
343
+ card.with_text_avatar(size: :lg) { "SV" }
344
+ end
345
+ end
346
+ end
347
+ ```
348
+
349
+
350
+ ## Roadmap
351
+ - ✅ ~~Accept Strings as view class name~~
352
+ - ✅ ~~Allow lambda slots~~
353
+ - ✅ ~~Allow polymorphic slots~~
215
354
 
216
355
  ## Development
217
356
 
data/benchmark/main.rb ADDED
@@ -0,0 +1,105 @@
1
+ require "benchmark"
2
+ require "benchmark/ips"
3
+ require_relative "../lib/phlex/slotable"
4
+
5
+ class DeferredList < Phlex::HTML
6
+ include Phlex::DeferredRender
7
+
8
+ def initialize
9
+ @items = []
10
+ end
11
+
12
+ def template
13
+ if @header
14
+ h1(class: "header", &@header)
15
+ end
16
+
17
+ ul do
18
+ @items.each do |item|
19
+ li { render(item) }
20
+ end
21
+ end
22
+ end
23
+
24
+ def header(&block)
25
+ @header = block
26
+ end
27
+
28
+ def with_item(&content)
29
+ @items << content
30
+ end
31
+ end
32
+
33
+ class SlotableList < Phlex::HTML
34
+ include Phlex::Slotable
35
+
36
+ slot :header
37
+ slot :item, many: true
38
+
39
+ def template
40
+ if header_slot
41
+ h1(class: "header", &header_slot)
42
+ end
43
+
44
+ ul do
45
+ item_slots.each do |slot|
46
+ li { render(slot) }
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ class DeferredListExample < Phlex::HTML
53
+ def template
54
+ render DeferredList.new do |list|
55
+ list.header do
56
+ "Header"
57
+ end
58
+
59
+ list.with_item do
60
+ "One"
61
+ end
62
+
63
+ list.with_item do
64
+ "two"
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ class SlotableListExample < Phlex::HTML
71
+ def template
72
+ render SlotableList.new do |list|
73
+ list.with_header do
74
+ "Header"
75
+ end
76
+
77
+ list.with_item do
78
+ "One"
79
+ end
80
+
81
+ list.with_item do
82
+ "two"
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ puts RUBY_DESCRIPTION
89
+
90
+ deferred_list = DeferredListExample.new.call
91
+ slotable_list = SlotableListExample.new.call
92
+
93
+ raise unless deferred_list == slotable_list
94
+
95
+ Benchmark.bmbm do |x|
96
+ x.report("Deferred") { 1_000_000.times { DeferredListExample.new.call } }
97
+ x.report("Slotable") { 1_000_000.times { SlotableListExample.new.call } }
98
+ end
99
+
100
+ puts
101
+
102
+ Benchmark.ips do |x|
103
+ x.report("Deferred") { DeferredListExample.new.call }
104
+ x.report("Slotable") { SlotableListExample.new.call }
105
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Slotable
5
- VERSION = "0.1.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
@@ -9,50 +9,91 @@ module Phlex
9
9
  end
10
10
 
11
11
  module ClassMethods
12
- def slot(slot_name, callable = nil, many: false)
12
+ def slot(slot_name, callable = nil, types: nil, many: false)
13
13
  include Phlex::DeferredRender
14
14
 
15
- if many
16
- define_method :"with_#{slot_name}" do |*args, **kwargs, &block|
17
- instance_variable_set(:"@#{slot_name}_slots", []) unless instance_variable_defined?(:"@#{slot_name}_slots")
15
+ if types
16
+ types.each do |type, callable|
17
+ define_setter_method(slot_name, callable, many: many, type: type)
18
+ end
19
+ else
20
+ define_setter_method(slot_name, callable, many: many)
21
+ end
22
+ define_predicate_method(slot_name, many: many)
23
+ define_getter_method(slot_name, many: many)
24
+ end
18
25
 
19
- value = case callable
20
- when nil
21
- block
22
- else
23
- callable.new(*args, **kwargs, &block)
26
+ private
27
+
28
+ def define_setter_method(slot_name, callable, many:, type: nil)
29
+ slot_name_with_type = type ? "#{type}_#{slot_name}" : slot_name
30
+
31
+ setter_method = if many
32
+ <<-RUBY
33
+ def with_#{slot_name_with_type}(*args, **kwargs, &block)
34
+ @#{slot_name}_slots ||= []
35
+ @#{slot_name}_slots << #{callable_value(slot_name_with_type, callable)}
24
36
  end
37
+ RUBY
38
+ else
39
+ <<-RUBY
40
+ def with_#{slot_name_with_type}(*args, **kwargs, &block)
41
+ @#{slot_name}_slot = #{callable_value(slot_name_with_type, callable)}
42
+ end
43
+ RUBY
44
+ end
25
45
 
26
- instance_variable_get(:"@#{slot_name}_slots") << value
27
- end
46
+ class_eval(setter_method, __FILE__, __LINE__ + 1)
47
+ define_lambda_method(slot_name_with_type, callable) if callable.is_a?(Proc)
48
+ end
28
49
 
29
- define_method :"#{slot_name}_slots?" do
30
- !send(:"#{slot_name}_slots").empty?
31
- end
32
- private :"#{slot_name}_slots?"
50
+ def define_lambda_method(slot_name, callable)
51
+ define_method :"__call_#{slot_name}__", &callable
52
+ private :"__call_#{slot_name}__"
53
+ end
33
54
 
34
- define_method :"#{slot_name}_slots" do
35
- instance_variable_get(:"@#{slot_name}_slots") || instance_variable_set(:"@#{slot_name}_slots", [])
36
- end
37
- private :"#{slot_name}_slots"
38
- else
39
- define_method :"with_#{slot_name}" do |*args, **kwargs, &block|
40
- value = case callable
41
- when nil
42
- block
43
- else
44
- callable.new(*args, **kwargs, &block)
55
+ def define_getter_method(slot_name, many:)
56
+ getter_method = if many
57
+ <<-RUBY
58
+ def #{slot_name}_slots
59
+ @#{slot_name}_slots ||= []
45
60
  end
61
+ private :#{slot_name}_slots
62
+ RUBY
63
+ else
64
+ <<-RUBY
65
+ def #{slot_name}_slot = @#{slot_name}_slot
66
+ private :#{slot_name}_slot
67
+ RUBY
68
+ end
46
69
 
47
- instance_variable_set(:"@#{slot_name}_slot", value)
48
- end
70
+ class_eval(getter_method, __FILE__, __LINE__ + 1)
71
+ end
49
72
 
50
- define_method :"#{slot_name}_slot?" do
51
- !instance_variable_get(:"@#{slot_name}_slot").nil?
52
- end
53
- private :"#{slot_name}_slot?"
73
+ def define_predicate_method(slot_name, many:)
74
+ predicate_method = if many
75
+ <<-RUBY
76
+ def #{slot_name}_slots? = #{slot_name}_slots.any?
77
+ private :#{slot_name}_slots?
78
+ RUBY
79
+ else
80
+ <<-RUBY
81
+ def #{slot_name}_slot? = !#{slot_name}_slot.nil?
82
+ private :#{slot_name}_slot?
83
+ RUBY
84
+ end
54
85
 
55
- attr_reader :"#{slot_name}_slot"
86
+ class_eval(predicate_method, __FILE__, __LINE__ + 1)
87
+ end
88
+
89
+ def callable_value(slot_name, callable)
90
+ case callable
91
+ when nil
92
+ %(block)
93
+ when Proc
94
+ %(-> { self.class.instance_method(:"__call_#{slot_name}__").bind_call(self, *args, **kwargs, &block) })
95
+ else
96
+ %(#{callable}.new(*args, **kwargs, &block))
56
97
  end
57
98
  end
58
99
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex-slotable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - stephann
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-12 00:00:00.000000000 Z
11
+ date: 2024-02-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: phlex
@@ -37,6 +37,7 @@ files:
37
37
  - LICENSE.txt
38
38
  - README.md
39
39
  - Rakefile
40
+ - benchmark/main.rb
40
41
  - lib/phlex/slotable.rb
41
42
  - lib/phlex/slotable/version.rb
42
43
  homepage: https://github.com/stephannv/phlex-slotable