Skip to content

Commit 6e1c2f7

Browse files
committed
Mention Strict Locals in more documentation
Motivation / Background --- Strict Locals support was introduced in [rails#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. [rails#45727]: rails#45727 [7.1 Release]: https://edgeguides.rubyonrails.org/7_1_release_notes.html#allow-templates-to-set-strict-locals
1 parent 39b0692 commit 6e1c2f7

File tree

5 files changed

+116
-5
lines changed

5 files changed

+116
-5
lines changed

actionview/lib/action_view/base.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,23 @@ module ActionView # :nodoc:
8080
# This is useful in cases where you aren't sure if the local variable has been assigned. Alternatively, you could also use
8181
# <tt>defined? headline</tt> to first check if the variable has been assigned before using it.
8282
#
83+
# By default, templates will accept any <tt>locals</tt> as keyword arguments. To restrict what <tt>locals</tt> a template accepts, add a <tt>locals:</tt> magic comment:
84+
#
85+
# <%# locals: (headline:) %>
86+
#
87+
# Headline: <%= headline %>
88+
#
89+
# In cases where the local variables are optional, declare the keyword argument with a default value:
90+
#
91+
# <%# locals: (headline: nil) %>
92+
#
93+
# <% unless headline.nil? %>
94+
# Headline: <%= headline %>
95+
# <% end %>
96+
#
97+
# Read more about strict locals in {Action View Overview}[https://guides.rubyonrails.org/action_view_overview.html#strict-locals]
98+
# in the guides.
99+
#
83100
# === Template caching
84101
#
85102
# 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
257274
message.
258275
gsub("unknown keyword:", "unknown local:").
259276
gsub("missing keyword:", "missing local:").
260-
gsub("no keywords accepted", "no locals accepted")
277+
gsub("no keywords accepted", "no locals accepted").
278+
concat(" for #{@current_template.short_identifier}")
261279
)
262280
end
263281
else

actionview/lib/action_view/template.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,20 @@ class Template
148148
# <p><%= alert %></p>
149149
# <% end %>
150150
#
151+
# By default, templates will accept any <tt>locals</tt> as keyword arguments
152+
# and make them available to <tt>local_assigns</tt>. To restrict what
153+
# <tt>local_assigns</tt> a template will accept, add a <tt>locals:</tt> magic comment:
154+
#
155+
# <%# locals: (headline:, alerts: []) %>
156+
#
157+
# <h1><%= headline %></h1>
158+
#
159+
# <% alerts.each do |alert| %>
160+
# <p><%= alert %></p>
161+
# <% end %>
162+
#
163+
# Read more about strict locals in {Action View Overview}[https://guides.rubyonrails.org/action_view_overview.html#strict-locals]
164+
# in the guides.
151165

152166
eager_autoload do
153167
autoload :Error

actionview/test/template/template_test.rb

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def test_locals_can_be_disabled
155155
render(foo: "bar")
156156
end
157157

158-
assert_match(/no locals accepted/, error.message)
158+
assert_match(/no locals accepted for hello template/, error.message)
159159
end
160160

161161
def test_locals_can_not_be_specified_with_positional_arguments
@@ -172,6 +172,25 @@ def test_locals_can_be_specified_with_splat_arguments
172172
assert_equal "bar", render(foo: "bar")
173173
end
174174

175+
def test_locals_can_be_specified_with_keyword_and_splat_arguments
176+
@template = new_template("<%# locals: (id:, **attributes) -%>\n<%= tag.hr(id: id, **attributes) %>")
177+
assert_equal '<hr id="1" class="h-1">', render(id: 1, class: "h-1")
178+
end
179+
180+
def test_locals_cannot_be_specified_with_positional_arguments
181+
@template = new_template("<%# locals: (argument = 'content') -%>\n<%= argument %>")
182+
assert_raises ActionView::Template::Error, match: "`argument` set as non-keyword argument for hello template. Locals can only be set as keyword arguments." do
183+
render
184+
end
185+
end
186+
187+
def test_locals_cannot_be_specified_with_block_arguments
188+
@template = new_template("<%# locals: (&block) -%>\n<%= tag.div(&block) %>")
189+
assert_raises ActionView::Template::Error, match: "`block` set as non-keyword argument for hello template. Locals can only be set as keyword arguments." do
190+
render { "content" }
191+
end
192+
end
193+
175194
def test_locals_can_be_specified
176195
@template = new_template("<%# locals: (message:) -%>\n<%= message %>")
177196
assert_equal "Hello", render(message: "Hello")
@@ -188,7 +207,7 @@ def test_required_locals_can_be_specified
188207
render
189208
end
190209

191-
assert_match(/missing local: :message/, error.message)
210+
assert_match(/missing local: :message for hello template/, error.message)
192211
end
193212

194213
def test_extra_locals_raises_error
@@ -197,7 +216,7 @@ def test_extra_locals_raises_error
197216
render(message: "Hi", foo: "bar")
198217
end
199218

200-
assert_match(/unknown local: :foo/, error.message)
219+
assert_match(/unknown local: :foo for hello template/, error.message)
201220
end
202221

203222
def test_rails_injected_locals_does_not_raise_error_if_not_passed

guides/source/7_1_release_notes.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,12 +318,23 @@ You can also set default values for these locals:
318318
<%= message %>
319319
```
320320

321+
Optional keyword arguments can be splatted:
322+
323+
```erb
324+
<%# locals: (message: "Hello, world!", **attributes) -%>
325+
<%= tag.p(message, **attributes) %>
326+
```
327+
321328
If you want to disable the use of locals entirely, you can do so like this:
322329

323330
```erb
324331
<%# locals: () %>
325332
```
326333

334+
Action View will process the `locals:` magic comment in any templating engine that supports `#`-prefixed comments, and will read the magic comment from any line in the partial.
335+
336+
CAUTION: Only keyword arguments are supported. Defining positional or block arguments will raise an Action View Error at render-time.
337+
327338
### Add `Rails.application.deprecators`
328339

329340
The new [`Rails.application.deprecators` method](https://github.com/rails/rails/pull/46049) returns a

guides/source/action_view_overview.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,8 @@ Combining Ruby 3.1's pattern matching assignment with calls to [Hash#with_defaul
346346
<% end %>
347347
```
348348

349+
INFO: By default, partials will accept any `locals` as keyword arguments. To define what `locals` a partial accepts, use a `locals:` magic comment. To learn more, read about [Strict Locals](#strict-locals).
350+
349351
[local_assigns]: https://api.rubyonrails.org/classes/ActionView/Template.html#method-i-local_assigns
350352

351353
### `render` without `partial` and `locals` Options
@@ -445,26 +447,73 @@ Rails will render the `_product_ruler` partial (with no data passed to it) betwe
445447

446448
### Strict Locals
447449

448-
By default, templates will accept any `locals` as keyword arguments. To define what `locals` a template accepts, add a `locals` magic comment:
450+
By default, templates will accept any `locals` as keyword arguments. To define what `locals` a template accepts, add a `locals:` magic comment:
449451

450452
```erb
453+
<%# app/views/messages/_message.html.erb %>
454+
451455
<%# locals: (message:) -%>
452456
<%= message %>
453457
```
454458

459+
Rendering the partial without a `:message` local variable argument will raise an exception:
460+
461+
```ruby
462+
render "messages/message"
463+
# => ActionView::Template::Error: missing local: :message for app/views/messages/_message.html.erb
464+
```
465+
455466
Default values can also be provided:
456467

457468
```erb
469+
<%# app/views/messages/_message.html.erb %>
470+
458471
<%# locals: (message: "Hello, world!") -%>
459472
<%= message %>
460473
```
461474

475+
Rendering the partial without a `:message` local variable uses the provided default value:
476+
477+
```ruby
478+
render "messages/message"
479+
# => "Hello, world!"
480+
```
481+
482+
Rendering the partial with additional local variable arguments will raise an exception:
483+
484+
```ruby
485+
render "messages/message", unknown_local: "will raise"
486+
# => ActionView::Template::Error: missing local: :unknown_local for app/views/messages/_message.html.erb
487+
```
488+
489+
Optional local variable arguments can be splatted:
490+
491+
```erb
492+
<%# app/views/messages/_message.html.erb %>
493+
494+
<%# locals: (message: "Hello, world!", **attributes) -%>
495+
<%= tag.p(message, **attributes) %>
496+
```
497+
462498
Or `locals` can be disabled entirely:
463499

464500
```erb
501+
<%# app/views/messages/_message.html.erb %>
502+
465503
<%# locals: () %>
466504
```
467505

506+
Rendering the partial with any local variable arguments will raise an exception:
507+
508+
```ruby
509+
render "messages/message", unknown_local: "will raise"
510+
# => ActionView::Template::Error: no locals accepted for app/views/messages/_message.html.erb
511+
```
512+
513+
Action View will process the `locals:` magic comment in any templating engine that supports `#`-prefixed comments, and will read the magic comment from any line in the partial.
514+
515+
CAUTION: Only keyword arguments are supported. Defining positional or block arguments will raise an Action View Error at render-time.
516+
468517
Layouts
469518
-------
470519

0 commit comments

Comments
 (0)