Skip to content

Commit

Permalink
Add lambda slots (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephannv authored Feb 13, 2024
1 parent 61b0441 commit f222109
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 3 deletions.
51 changes: 48 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,55 @@ class BlogComponent < Phlex::HTML
end
```

#### Lambda slots
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
```ruby

class BlogComponent < Phlex::HTML
include Phlex::Slotable

slot :header, ->(size:, &content) { render HeaderComponent.new(size: size, color: "blue"), &content }
slot :post, ->(featured:, &content) { span(class: featured ? "featured" : nil, &content) }, many: true
end

class MyPage < Phlex::HTML
def template
render BlogComponent.new do |blog|
blog.with_header(size: :lg) { "Hello World!" }

blog.with_post(featured: true) { "Post A" }
blog.with_post { "Post B" }
blog.with_post { "Post C" }
end
end
end
```

You can access the internal view state within lambda slots.For example:
```ruby
class BlogComponent < Phlex::HTML
include Phlex::Slotable

slot :header, ->(size:, &content) { render HeaderComponent.new(size: size, color: @header_color), &content }

def initialize(header_color:)
@header_color = header_color
end
end

class MyPage < Phlex::HTML
def template
render BlogComponent.new(header_color: "red") do |blog|
blog.with_header(size: :lg) { "Hello World!" }
end
end
end
```

## Roadmap
- Accepts Strings as view class name
- Allow lambda slots
- Allow polymorphic slots
- ~~Accept Strings as view class name~~
- ~~Allow lambda slots~~
- 🕐 Allow polymorphic slots

## Development

Expand Down
9 changes: 9 additions & 0 deletions lib/phlex/slotable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ module ClassMethods
def slot(slot_name, callable = nil, many: false)
include Phlex::DeferredRender

if callable.is_a?(Proc)
define_method :"__call_#{slot_name}__", &callable
private :"__call_#{slot_name}__"
end

if many
define_method :"with_#{slot_name}" do |*args, **kwargs, &block|
instance_variable_set(:"@#{slot_name}_slots", []) unless instance_variable_defined?(:"@#{slot_name}_slots")
Expand All @@ -21,6 +26,8 @@ def slot(slot_name, callable = nil, many: false)
block
when String
self.class.const_get(callable).new(*args, **kwargs, &block)
when Proc
-> { self.class.instance_method(:"__call_#{slot_name}__").bind_call(self, *args, **kwargs, &block) }
else
callable.new(*args, **kwargs, &block)
end
Expand All @@ -44,6 +51,8 @@ def slot(slot_name, callable = nil, many: false)
block
when String
self.class.const_get(callable).new(*args, **kwargs, &block)
when Proc
-> { self.class.instance_method(:"__call_#{slot_name}__").bind_call(self, *args, **kwargs, &block) }
else
callable.new(*args, **kwargs, &block)
end
Expand Down
98 changes: 98 additions & 0 deletions test/phlex/test_multi_slot_with_lambda.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# frozen_string_literal: true

require "test_helper"

class Phlex::TestMultiSlotWithLambda < Minitest::Test
class HeadlineComponent < Phlex::HTML
include Phlex::Slotable

slot :icon
slot :title

def initialize(size:, bg_color:)
@size = size
@bg_color = bg_color
end

def template
div class: "headline text-#{@size} bg-#{@bg_color}" do
render icon_slot
render title_slot
end
end
end

class Blog < Phlex::HTML
include Phlex::Slotable

slot :post, ->(featured: false, &content) { p(class: featured ? "featured" : nil, &content) }, many: true
slot :headline, ->(size:, &content) do
render HeadlineComponent.new(size: size, bg_color: @headline_bg_color), &content
end, many: true

def initialize(headline_bg_color: nil)
@headline_bg_color = headline_bg_color
end

def template
if post_slots?
main do
headline_slots.each do |slot|
render slot
end
post_slots.each do |slot|
render slot
end
end
end

footer { post_slots.size }
end
end

def test_slots
output = Blog.new(headline_bg_color: "blue").call do |c|
c.with_post(featured: true) { "Post A" }
c.with_post { "Post B" }
c.with_post { "Post C" }

c.with_headline(size: "lg") do |h|
h.with_title { "Headline A" }
h.with_icon { h.i(class: "star") }
end
c.with_headline(size: "md") do |h|
h.with_title { "Headline B" }
h.with_icon { h.i(class: "heart") }
end
end

expected_html = <<~HTML.join_lines
<main>
<div class="headline text-lg bg-blue">
<i class="star"></i>
Headline A
</div>
<div class="headline text-md bg-blue">
<i class="heart"></i>
Headline B
</div>
<p class="featured">Post A</p>
<p>Post B</p>
<p>Post C</p>
</main>
<footer>
3
</footer>
HTML

assert_equal expected_html, output
end

def test_empty_slots
output = Blog.new.call

assert_equal output, "<footer>0</footer>"
end
end
78 changes: 78 additions & 0 deletions test/phlex/test_single_slot_with_lambda.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# frozen_string_literal: true

require "test_helper"

class Phlex::TestSingleSlotWithLambda < Minitest::Test
class SubtitleComponent < Phlex::HTML
include Phlex::Slotable

slot :icon
slot :content

def initialize(size:, bg_color:)
@size = size
@bg_color = bg_color
end

def template(&content)
h3(class: "text-#{@size} bg-#{@bg_color}") do
render icon_slot
render content_slot
end
end
end

class Blog < Phlex::HTML
include Phlex::Slotable

slot :title, ->(size:, &content) { h1(class: "text-#{size}", &content) }
slot :subtitle, ->(size:, &content) do
render SubtitleComponent.new(size: size, bg_color: @subtitle_bg_color), &content
end

def initialize(subtitle_bg_color: nil)
@subtitle_bg_color = subtitle_bg_color
end

def template
div id: "header" do
render title_slot if title_slot?
render subtitle_slot if subtitle_slot?
end

main { "My posts" }
end
end

def test_slot
output = Blog.new(subtitle_bg_color: "gray").call do |c|
c.with_title(size: :lg) { "Hello World!" }
c.with_subtitle(size: :sm) do |s|
s.with_icon { s.i(class: "home") }
s.with_content { "Welcome to your posts" }
end
end

assert_equal output, <<~HTML.join_lines
<div id="header">
<h1 class="text-lg">Hello World!</h1>
<h3 class="text-sm bg-gray">
<i class="home"></i>
Welcome to your posts
</h3>
</div>
<main>
My posts
</main>
HTML
end

def test_empty_slot
output = Blog.new.call

assert_equal output, <<~HTML.join_lines
<div id="header"></div>
<main>My posts</main>
HTML
end
end

0 comments on commit f222109

Please sign in to comment.