Skip to content

Commit 065bd98

Browse files
committed
FormNode: {form scope name} as alternative to {formContext name}
The scope keyword (placed before the form name) makes {form} skip the <form> tag emission while still pushing the form on the stack, so {input}/{label}/{inputError} resolve against it. Semantically identical to {formContext}, but stays with the canonical {form} tag.
1 parent d7befba commit 065bd98

5 files changed

Lines changed: 164 additions & 2 deletions

File tree

src/Bridges/FormsLatte/Nodes/FormNode.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,22 @@
1818

1919

2020
/**
21-
* {form name [, attributes]} ... {/form}
21+
* {form [scope] name [, attributes]} ... {/form}
2222
* {formContext name} ... {/formContext}
2323
* Renders form tags and initializes form context.
24+
* The `scope` keyword makes {form} skip the <form> tag emission while still pushing the
25+
* form on the stack — semantically identical to {formContext}, but with the canonical
26+
* {form} tag.
2427
*/
2528
class FormNode extends StatementNode
2629
{
30+
private const Modes = ['scope'];
31+
2732
public ExpressionNode $name;
2833
public ArrayNode $attributes;
2934
public AreaNode $content;
3035
public bool $print;
36+
public ?string $mode = null;
3137

3238

3339
/** @return \Generator<int, ?list<string>, array{AreaNode, ?Tag}, static> */
@@ -40,13 +46,16 @@ public static function create(Tag $tag): \Generator
4046
$tag->outputMode = $tag::OutputKeepIndentation;
4147
$tag->expectArguments();
4248
$node = $tag->node = new static;
49+
$node->mode = $tag->name === 'form' && in_array($tag->parser->stream->tryPeek()?->text, self::Modes, strict: true)
50+
? $tag->parser->stream->consume()->text
51+
: null;
4352
$node->name = $tag->parser->parseUnquotedStringOrExpression();
4453
if (!$tag->parser->stream->tryConsume(',') && !$tag->parser->isEnd()) {
4554
$position = $tag->parser->stream->peek()->position;
4655
trigger_error("Missing comma before arguments in {{$tag->name}} tag $position.", E_USER_DEPRECATED);
4756
}
4857
$node->attributes = $tag->parser->parseArguments();
49-
$node->print = $tag->name === 'form';
58+
$node->print = $tag->name === 'form' && $node->mode !== 'scope';
5059

5160
[$node->content, $endTag] = yield;
5261
if ($endTag && $node->name instanceof StringNode) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
<label for="frm-name">Name:</label>
3+
<input type="text" name="name" id="frm-name">
4+
<input type="text" name="email" id="frm-email">
5+
6+
7+
8+
9+
<label for="frm-name">Name:</label>
10+
<input type="text" name="name" id="frm-name">
11+
<input type="text" name="email" id="frm-email">
12+
13+
14+
15+
16+
<input type="text" name="name" id="frm-name">
17+
18+
19+
20+
<form action="" method="post" class="outer">
21+
<input type="text" name="name" id="frm-name">
22+
23+
24+
<input type="text" name="email" id="frm-email">
25+
26+
</form>
27+
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php %A%
2+
$this->global->forms->begin($form = $this->global->uiControl['myForm']) /* pos 3:1 */;
3+
echo '
4+
';
5+
echo ($ʟ_label = $this->global->forms->item('name')->getLabel()) /* pos 4:2 */;
6+
echo '
7+
';
8+
echo $this->global->forms->item('name')->getControl() /* pos 5:2 */;
9+
echo '
10+
';
11+
echo $this->global->forms->item('email')->getControl() /* pos 6:2 */;
12+
echo "\n";
13+
$this->global->forms->end();
14+
15+
echo '
16+
17+
18+
';
19+
$this->global->forms->begin($form = $this->global->uiControl['myForm']) /* pos 12:1 */;
20+
echo '
21+
';
22+
echo ($ʟ_label = $this->global->forms->item('name')->getLabel()) /* pos 13:2 */;
23+
echo '
24+
';
25+
echo $this->global->forms->item('name')->getControl() /* pos 14:2 */;
26+
echo '
27+
';
28+
echo $this->global->forms->item('email')->getControl() /* pos 15:2 */;
29+
echo "\n";
30+
$this->global->forms->end();
31+
32+
echo '
33+
34+
35+
';
36+
$this->global->forms->begin($form = $this->global->uiControl['myForm']) /* pos 21:1 */;
37+
echo '
38+
';
39+
echo $this->global->forms->item('name')->getControl() /* pos 22:2 */;
40+
echo "\n";
41+
$this->global->forms->end();
42+
43+
echo '
44+
45+
46+
';
47+
$this->global->forms->begin($form = $this->global->uiControl['myForm']) /* pos 28:1 */;
48+
echo $this->global->forms->renderFormBegin(['class' => 'outer']) /* pos 28:1 */;
49+
echo '
50+
';
51+
echo $this->global->forms->item('name')->getControl() /* pos 29:2 */;
52+
echo '
53+
54+
';
55+
$this->global->forms->begin($form = $this->global->uiControl['myForm']) /* pos 31:2 */;
56+
echo '
57+
';
58+
echo $this->global->forms->item('email')->getControl() /* pos 32:3 */;
59+
echo '
60+
';
61+
$this->global->forms->end();
62+
63+
echo "\n";
64+
echo $this->global->forms->renderFormEnd() /* pos 34:1 */;
65+
$this->global->forms->end();
66+
%A%

tests/Forms.Latte/forms.scope.phpt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php declare(strict_types=1);
2+
3+
use Nette\Bridges\FormsLatte\FormsExtension;
4+
use Nette\Forms\Form;
5+
use Tester\Assert;
6+
7+
require __DIR__ . '/../bootstrap.php';
8+
9+
10+
$form = new Form;
11+
$form->addText('name', 'Name:');
12+
$form->addText('email', 'Email:');
13+
14+
$latte = new Latte\Engine;
15+
$latte->addExtension(new FormsExtension);
16+
$latte->addProvider('uiControl', ['myForm' => $form]);
17+
18+
Assert::matchFile(
19+
__DIR__ . '/expected/forms.scope.php',
20+
$latte->compile(__DIR__ . '/templates/forms.scope.latte'),
21+
);
22+
23+
Assert::matchFile(
24+
__DIR__ . '/expected/forms.scope.html',
25+
$latte->renderToString(__DIR__ . '/templates/forms.scope.latte'),
26+
);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{* {form scope name}: form context without emitting <form> tag. Equivalent to {formContext}. *}
2+
3+
{form scope myForm}
4+
{label name /}
5+
{input name}
6+
{input email}
7+
{/form}
8+
9+
10+
{* Same with {formContext} — produces identical output. *}
11+
12+
{formContext myForm}
13+
{label name /}
14+
{input name}
15+
{input email}
16+
{/formContext}
17+
18+
19+
{* Attributes are accepted but ignored in scope mode (no <form> tag to apply them to). *}
20+
21+
{form scope myForm, class: "ignored"}
22+
{input name}
23+
{/form}
24+
25+
26+
{* Nested use: outer {form} emits <form>, inner {form scope} only resolves inputs. *}
27+
28+
{form myForm, class: "outer"}
29+
{input name}
30+
31+
{form scope myForm}
32+
{input email}
33+
{/form}
34+
{/form}

0 commit comments

Comments
 (0)