Closed joshuaclayton closed 10 months ago
👋 hey!
I'm not super familiar with refinements since I haven't used them often, but the view method is defined on the component class so I'd imagine they would work out of the box. If that's not the case, having a failing test would be helpful, but I also I'd accept a PR adding that functionality as long as it doesn't negatively impact performance.
I'd like to refine
Domain::Duration
at the ViewComponent level to introduceDomain::Duration#to_sortable_table
, which in turn callsto_f.round(4)
In case it's helpful in the meantime, for scenarios like this I'd generally suggest folks add a method to the component that accepts the Domain::Duration
object and formats it since that's a view concern and is more discoverable (using a refinement to add methods conditionally based on scope feels magical, for better or worse). I haven't used refinements much though, so I'd be curious to hear how that approach works out!
@camertron I know you have written expensively on the subject of monkey patches and refinements, maybe yoe have some insight here
@BlakeWilliams well hey! It's worth me calling out I ALSO don't have a ton of experience with refinements, but seemed like a reasonable approach for consolidating decorated behavior for an external dependency (e.g. the Stimulus controller).
My approach here most closely mirrors traits in Rust / typeclasses in Haskell wherein defining a standard method #to_sortable_table
can be configured on a per-class basis (but in this case, without muddying the implementation itself).
Minimum reproducible example is here: https://github.com/joshuaclayton/view_component-ruby-refinement
Totally noted on alternative approaches (e.g. a helper method in the component itself); my avoidance of this mirrors why I'd not use view helpers and instead use ViewComponent.
My assumption around this behavior is that the view context itself is a separate class, which makes sense then that the refinement doesn't propagate. There's AFAIK (and seemingly confirmed in https://www.alchemists.io/articles/ruby_refinements#_used) no way to resolve this. In theory, a solution would include any view context understanding the caller's list of refinements and reapplying so they're available within the view layer.
My approach here most closely mirrors traits in Rust / typeclasses in Haskell wherein defining a standard method
#to_sortable_table
can be configured on a per-class basis (but in this case, without muddying the implementation itself).
Fair! Although I imagine the type system is super helpful there. 🙂
My assumption around this behavior is that the view context itself is a separate class, which makes sense then that the refinement doesn't propagate. There's AFAIK (and seemingly confirmed in https://www.alchemists.io/articles/ruby_refinements#_used) no way to resolve this. In theory, a solution would include any view context understanding the caller's list of refinements and reapplying so they're available within the view layer.
That's true for Rails views, but not for components. We define the template method call
directly on the class itself: https://github.com/ViewComponent/view_component/blob/main/lib/view_component/compiler.rb#L104
Given that I think a refinement should work with components?
Hey @joshuaclayton, interesting issue! Unfortunately I think what you want here is largely impossible given how refinements work. I don't often use the word "impossible" in the context of Ruby, but in this case it's warranted.
Refinements are one of the only lexically scoped things in the Ruby language, which means they are only available inside the scope where using
was called. Lexical scoping is scoping from the point of view of the Ruby lexer, the thing that turns Ruby code into tokens that are eventually fed into the parser. The lexer has no knowledge of what the code actually does, only how it's structured. Any code that wants to make use of refined methods essentially has to be able to see - almost visually-speaking - the using
call. The lexical scoping issue is one of the most limiting aspects of refinements and probably a big factor in why they aren't used very often.
The reason the refinements in your RefineString
module aren't applied to the component class is because the call to class_eval
that defines the template methods is in the ViewComponent::Compiler
class and not in your HomeComponent
class. This is confusing because the methods themselves are defined on HomeComponent
and are therefore scoped to that class when they execute. The lexical scope when they were defined however is the Compiler
class, and therein lies the problem. You'd have to put a using RefineString
inside the definition of the Compiler
class for everything to work as you'd like.
Here's a minimal reproduction case illustrating the problem and the so-called "solution":
module RefineString
refine String do
def yell
upcase
end
end
end
class Compiler
# using RefineString # uncomment this line to make everything work
def initialize(klass)
@klass = klass
end
def compile!
@klass.class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
def bar
"hello world".yell
end
RUBY
end
end
class Foo
using RefineString
end
Compiler.new(Foo).compile!
puts Foo.new.bar
Unfortunately the Compiler
class is defined inside ViewComponent, so adding a using
statement to it isn't going to work for you. In my opinion, you'll have to consider alternative approaches.
@camertron I appreciate the explanation, and yes, based on what I'd poked at, would've required a fair bit of hackery to get working 😅. Thanks!
Feature Request
I'd like to enable Ruby refinements within the context of a view component where refined values are available within the template.
Motivation
Imagine the following scenario:
<td>
lexicographically or overridden via adata-
attribute the controller is aware of for both value and type. e.g.<td data-sortable-type="float" data-sortable-value="<%= example.duration.to_f.round(4) %>">Some other non-numeric content</td>
example.duration
is a domain object,Domain::Duration
, that operates as a PORO/value object that exists independent of the view.Domain::Duration
at the ViewComponent level to introduceDomain::Duration#to_sortable_table
, which in turn callsto_f.round(4)
With this approach,
Domain::Duration
can be decorated only within the ViewComponent and without muddying up theDomain::Duration
class with view-specific code. If additional domain classes also have long or complex method chains to be able to sort them, the refinements for e.g. that specific Stimulus sortable table can all exist in one spot.