File: 0004-slots-separate-getter-setter.md

package info (click to toggle)
ruby-view-component 2.74.1-1
  • links: PTS, VCS
  • area: contrib
  • in suites: bookworm
  • size: 3,156 kB
  • sloc: ruby: 6,731; sh: 163; javascript: 10; makefile: 4
file content (93 lines) | stat: -rw-r--r-- 2,919 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
---
layout: default
title: 4. Separate Slot Getters and Setters
parent: Architectural decisions
nav_order: 4
---

# 4. Separate Slot Getters and Setters

Date: 2022/03/22

## Author

Blake Williams

## Status

Accepted

## Context

Currently, slots implement a single method for both getting and setting a slot. For example, given a slot named `header`:

```ruby
class MyComponent < ViewComponent::Base
  renders_one :header
end

c = MyComponent.new

c.header { "Hello world!" } # sets the slot
c.header # gets the slot
```

This API was built with the assumption that a slot will always be set by passing an argument and/or passing a block.

This assumption hasn't remained valid. Specifically, `with_content` breaks the assumption when passing static content to a slot:

```ruby
class MyComponent < ViewComponent::Base
  renders_one :header
end

c = MyComponent.new

# c.header returns nil because the getter path is being executed due to having
# no arguments and no block passed: https://github.com/viewcomponent/view_component/blob/main/lib/view_component/slotable_v2.rb#L70-L74
#
c.header.with_content("Hello world!") # undefined method `with_content' for nil:NilClass (NoMethodError)
```

The above example shows off the gap in the slots API via `with_content`, but it's likely that as the library continues to grow this gap will appear in other valid use-cases.

## Decision

Split the slots API into a getter and setter. Keeping the slot name as the getter makes the most sense, but the setter can be renamed to `with_#{slot_name}`.

For example, the above would become:

```ruby
class MyComponent < ViewComponent::Base
  renders_one :header
end

c = MyComponent.new

# New API for setting slots
c.with_header { "hello world" }

# Now `with_content` is valid when defining slots
c.with_header.with_content("Hello world!")
```

## Alternatives Considered

We've spoken about a few alternatives:

* Making a special `NilSlot` class that responds to `with_content`.
  * Results in extra allocations
  * Can't treat the `NilSlot` as falsy. So `if header` would no longer work
    even though you would expect it to.
* Introducing an API for the `header.with_content("Hello world!")` pattern as explained above (like: `c.with_header_content("Hello world!")`):
  * The API gap still exists and requires a specific work around for
    `with_content`, leaving the gap for future API's.
  * This API doesn't allow arguments to be passed 1-to-1 like the current setter API.

## Consequences

The largest consequence of this change is that we'll need to deprecate the old setter usage (`header { "Hello world!"}`) in favor of the new setter API (`with_header { "Hello world!" }`).

I propose that we make at least one release with the new API and no deprecation
warning followed by another release that includes the deprecation warning. This
will give teams some time to migrate before running into deprecation warnings.