rails icon indicating copy to clipboard operation
rails copied to clipboard

Action View: Reduce public API of `tag` helper

Open seanpdoyle opened this issue 8 months ago • 1 comments

Motivation / Background

The TagBuilder instance returned by the ActionView::Helpers::TagHelper#tag method has the ability to build various HTML elements through its reliance on method_missing. The magic of that instance hinges on the fact that it has a minimal public interface, and transforms missing methods names into HTML elements. For example, calling tag.div invokes a missing #div method, which renders a <div> element.

There are two exceptional cases:

  • tag.p is defined, since the existing Kernel#p definition would prevent the underlying #method_missing invocation
  • tag.attributes is a defined method to transform Hash instances and keyword arguments in HTML attribute strings

In addition to those two intentional exceptions, there are also several methods that are incidentally part of the public interface:

  • #tag_string
  • #content_tag_string
  • #tag_options
  • #boolean_tag_option
  • #tag_option

Along with those methods, the class also includes OutputSafetyHelper and CaptureHelper, which expand surface area of the class's public interface even further.

While it's unlikely that these methods would collide with method invocations intended to construct HTML elements, they still impact that design of the object.

The "public" nature of the TagBuilder interface has some subtle nuances. While it is marked with :nodoc:, instances are returned by a public tag method, despite it being considered a private class only meant for internal consumption.

That same is true for the incidentally public methods mentioned above: no consuming applications should be invoking #tag_string or #tag_options. While that's true, those methods are being invoked directly by other Action View classes.

Detail

This commit removes those invocations, and instead replaces them with public method calls.

In cases where the tag name is statically know ahead of time, they're replaced with calls to that method (for example, tag(:option) becomes tag.option). When they're dynamic, they're replaced by calls to public_send (for example, tag(name) becomes tag.public_send(name)).

The majority of these changes are painless, with one exception. Some calls to the #tag helper also pass an open positional argument that isn't part of the TagBuilder class's public interface. As a result, that becomes slightly more complicated, and requires some String mutation to continue to pass the test suite.

This commit also removes the OutputSafetyHelper and CaptureHelper modules from the class, and delegates to the methods it depended on to j@view_context.

Hopefully, calls to #tag with positional arguments are less and less common, since the documentation describes it as a Legacy Syntax marked for deprecation in future versions of Rails.

Checklist

Before submitting the PR make sure the following are checked:

  • [x] This Pull Request is related to one change. Changes that are unrelated should be opened in separate PRs.
  • [x] Commit message has a detailed description of what changed and why. If this PR fixes a related issue include it in the commit message. Ex: [Fix #issue-number]
  • [x] Tests are added or updated if you fix a bug or add a feature.
  • [x] CHANGELOG files are updated for the changed libraries if there is a behavior change or additional feature. Minor bug fixes and documentation changes should not be included.

seanpdoyle avatar Sep 24 '23 21:09 seanpdoyle