From c8fb22611c81e1f63ac9bd231bd0b5611481157d Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Wed, 24 Jan 2024 08:26:47 -0500 Subject: [PATCH] Mention Strict Locals in more documentation Motivation / Background --- Strict Locals support was introduced in [#45727][] and announced as part of the [7.1 Release][]. There are several mentions across the Guides, but support is rarely mentioned in the API documentation. Detail ---- Mention the template short identifier (the pathname, in most cases) as part of the `ArgumentError` message. This commit adds two test cases to ensure support for splatting additional arguments, and for forbidding block and positional arguments. It also makes mention of strict locals in more places, and links to the guides. [#45727]: https://github.com/rails/rails/pull/45727 [7.1 Release]: https://edgeguides.rubyonrails.org/7_1_release_notes.html#allow-templates-to-set-strict-locals --- actionview/lib/action_view/base.rb | 20 +++++++++++++++++- actionview/lib/action_view/template.rb | 14 +++++++++++++ actionview/test/template/template_test.rb | 25 ++++++++++++++++++++--- guides/source/7_1_release_notes.md | 19 +++++++++++++++++ guides/source/action_view_overview.md | 23 ++++++++++++++++++++- 5 files changed, 96 insertions(+), 5 deletions(-) diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index cb1e485fedb37..1e2c312707c31 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -80,6 +80,23 @@ module ActionView # :nodoc: # This is useful in cases where you aren't sure if the local variable has been assigned. Alternatively, you could also use # defined? headline to first check if the variable has been assigned before using it. # + # By default, templates will accept any locals as keyword arguments. To define what locals a template accepts, add a locals: magic comment: + # + # <%# locals: (headline:) %> + # + # Headline: <%= headline %> + # + # In cases where the local variables are optional, declare the keyword argument with a default value: + # + # <%# locals: (headline: nil) %> + # + # <% unless headline.nil? %> + # Headline: <%= headline %> + # <% end %> + # + # Read more about strict locals in {Action View Overview}[https://guides.rubyonrails.org/action_view_overview.html#strict-locals] + # in the guides. + # # === Template caching # # By default, \Rails will compile each template to a method in order to render it. When you alter a template, @@ -257,7 +274,8 @@ def _run(method, template, locals, buffer, add_to_stack: true, has_strict_locals message. gsub("unknown keyword:", "unknown local:"). gsub("missing keyword:", "missing local:"). - gsub("no keywords accepted", "no locals accepted") + gsub("no keywords accepted", "no locals accepted"). + concat(" for #{@current_template.short_identifier}") ) end else diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 9a7479314d57e..c4ba28a23868a 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -148,6 +148,20 @@ class Template #

<%= alert %>

# <% end %> # + # By default, templates will accept any locals as keyword arguments + # and make them available to local_assigns. To define what + # local_assigns a template will accept, add a locals: magic comment: + # + # <%# locals: (headline:, alerts: []) %> + # + #

<%= headline %>

+ # + # <% alerts.each do |alert| %> + #

<%= alert %>

+ # <% end %> + # + # Read more about strict locals in {Action View Overview}[https://guides.rubyonrails.org/action_view_overview.html#strict-locals] + # in the guides. eager_autoload do autoload :Error diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb index 145fcca5be7bb..280c4b0874e4b 100644 --- a/actionview/test/template/template_test.rb +++ b/actionview/test/template/template_test.rb @@ -155,7 +155,7 @@ def test_locals_can_be_disabled render(foo: "bar") end - assert_match(/no locals accepted/, error.message) + assert_match(/no locals accepted for hello template/, error.message) end def test_locals_can_not_be_specified_with_positional_arguments @@ -172,6 +172,25 @@ def test_locals_can_be_specified_with_splat_arguments assert_equal "bar", render(foo: "bar") end + def test_locals_can_be_specified_with_keyword_and_splat_arguments + @template = new_template("<%# locals: (id:, **attributes) -%>\n<%= tag.hr(id: id, **attributes) %>") + assert_equal '
', render(id: 1, class: "h-1") + end + + def test_locals_cannot_be_specified_with_positional_arguments + @template = new_template("<%# locals: (argument = 'content') -%>\n<%= argument %>") + assert_raises ActionView::Template::Error, match: "`argument` set as non-keyword argument for hello template. Locals can only be set as keyword arguments." do + render + end + end + + def test_locals_cannot_be_specified_with_block_arguments + @template = new_template("<%# locals: (&block) -%>\n<%= tag.div(&block) %>") + assert_raises ActionView::Template::Error, match: "`block` set as non-keyword argument for hello template. Locals can only be set as keyword arguments." do + render { "content" } + end + end + def test_locals_can_be_specified @template = new_template("<%# locals: (message:) -%>\n<%= message %>") assert_equal "Hello", render(message: "Hello") @@ -188,7 +207,7 @@ def test_required_locals_can_be_specified render end - assert_match(/missing local: :message/, error.message) + assert_match(/missing local: :message for hello template/, error.message) end def test_extra_locals_raises_error @@ -197,7 +216,7 @@ def test_extra_locals_raises_error render(message: "Hi", foo: "bar") end - assert_match(/unknown local: :foo/, error.message) + assert_match(/unknown local: :foo for hello template/, error.message) end def test_rails_injected_locals_does_not_raise_error_if_not_passed diff --git a/guides/source/7_1_release_notes.md b/guides/source/7_1_release_notes.md index 1c4a628d61b35..95a831f51e124 100644 --- a/guides/source/7_1_release_notes.md +++ b/guides/source/7_1_release_notes.md @@ -318,12 +318,31 @@ You can also set default values for these locals: <%= message %> ``` +Optional keyword arguments can be splatted: + +```erb +<%# locals: (message: "Hello, world!", **attributes) -%> +<%= tag.p(message, **attributes) %> +``` + If you want to disable the use of locals entirely, you can do so like this: ```erb <%# locals: () %> ``` +All templating engines that support comments are supported: + +```ruby +# locals: (json:, message:) +json.message message +``` + +Action View supports reading the magic comment from any line in the partial. + +CAUTION: Only keyword arguments are supported. Defining positional or block arguments +will raise an Action View Error at render-time. + ### Add `Rails.application.deprecators` The new [`Rails.application.deprecators` method](https://github.com/rails/rails/pull/46049) returns a diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index 5521d05069c52..002b2bb27fa43 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -346,6 +346,8 @@ Combining Ruby 3.1's pattern matching assignment with calls to [Hash#with_defaul <% end %> ``` +INFO: By default, partials will accept any `locals` as keyword arguments. To define what `locals` a partials accepts, use a `locals:` magic comment. To learn more, read about [Strict Locals](#strict-locals). + [local_assigns]: https://api.rubyonrails.org/classes/ActionView/Template.html#method-i-local_assigns ### `render` without `partial` and `locals` Options @@ -445,7 +447,7 @@ Rails will render the `_product_ruler` partial (with no data passed to it) betwe ### Strict Locals -By default, templates will accept any `locals` as keyword arguments. To define what `locals` a template accepts, add a `locals` magic comment: +By default, templates will accept any `locals` as keyword arguments. To define what `locals` a template accepts, add a `locals:` magic comment: ```erb <%# locals: (message:) -%> @@ -459,12 +461,31 @@ Default values can also be provided: <%= message %> ``` +Optional keyword arguments can be splatted: + +```erb +<%# locals: (message: "Hello, world!", **attributes) -%> +<%= tag.p(message, **attributes) %> +``` + Or `locals` can be disabled entirely: ```erb <%# locals: () %> ``` +All templating engines that support comments are supported: + +```ruby +# locals: (json:, message:) +json.message message +``` + +Action View supports reading the magic comment from any line in the partial. + +CAUTION: Only keyword arguments are supported. Defining positional or block arguments +will raise an Action View Error at render-time. + Layouts -------