Skip to content

Commit 6ea109c

Browse files
committed
FormNode: {form scope name} as an alternative to {formContext name}
The scope keyword before the form name makes {form} skip the <form> tag while still pushing the form on the stack, so {input}/{label}/ {inputError} resolve against it. When nested inside another form, scope resolves the name relatively to it (like {formContainer}); at the top level it looks the name up in the registry. {formContext} keeps its legacy behavior of always resolving from the registry. Passing arguments to {form scope} or {formContext} now throws, since neither renders a <form> tag to apply them to.
1 parent 8d0b87b commit 6ea109c

6 files changed

Lines changed: 223 additions & 13 deletions

File tree

src/Bridges/FormsLatte/Nodes/FormNode.php

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,23 @@
1515
use Latte\Compiler\Nodes\StatementNode;
1616
use Latte\Compiler\PrintContext;
1717
use Latte\Compiler\Tag;
18+
use function in_array;
1819

1920

2021
/**
21-
* {form name [, attributes]} ... {/form}
22+
* {form [scope] name [, attributes]} ... {/form}
2223
* {formContext name} ... {/formContext}
2324
* Renders form tags and initializes form context.
2425
*/
2526
class FormNode extends StatementNode
2627
{
28+
private const ModeLegacyScope = 'context';
29+
private const ModeScope = 'scope';
30+
2731
public ExpressionNode $name;
2832
public ArrayNode $attributes;
2933
public AreaNode $content;
30-
public bool $print;
34+
public ?string $mode = null;
3135

3236

3337
/** @return \Generator<int, ?list<string>, array{AreaNode, ?Tag}, static> */
@@ -40,13 +44,21 @@ public static function create(Tag $tag): \Generator
4044
$tag->outputMode = $tag::OutputKeepIndentation;
4145
$tag->expectArguments();
4246
$node = $tag->node = new static;
47+
$node->mode = match (true) {
48+
$tag->name === 'formContext' => self::ModeLegacyScope,
49+
in_array($tag->parser->stream->tryPeek()?->text, [self::ModeScope], strict: true) => $tag->parser->stream->consume()->text,
50+
default => null,
51+
};
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+
if ($node->mode !== null && $node->attributes->items) {
59+
$label = '{' . $tag->name . ($node->mode === self::ModeScope ? ' scope' : '') . '}';
60+
throw new CompileException("Arguments are not allowed in $label because it does not render a <form> tag.", $tag->position);
61+
}
5062

5163
[$node->content, $endTag] = yield;
5264
if ($endTag && $node->name instanceof StringNode) {
@@ -59,19 +71,21 @@ public static function create(Tag $tag): \Generator
5971

6072
public function print(PrintContext $context): string
6173
{
74+
$renderBegin = 'echo $this->global->forms->renderFormBegin(%node) %1.line;';
75+
$renderEnd = 'echo $this->global->forms->renderFormEnd() %4.line;';
76+
6277
return $context->format(
6378
'$this->global->forms->begin($form = '
64-
. ($this->name instanceof StringNode
65-
? '$this->global->uiControl[%node]'
66-
: '(is_object($ʟ_tmp = %node) ? $ʟ_tmp : $this->global->uiControl[$ʟ_tmp])')
79+
. (match (true) {
80+
$this->mode === self::ModeScope => '(is_object($ʟ_tmp = %node) ? $ʟ_tmp : ($this->global->forms->isNested() ? $this->global->forms->get($ʟ_tmp, Nette\Forms\Container::class) : $this->global->uiControl[$ʟ_tmp]))',
81+
$this->name instanceof StringNode => '$this->global->uiControl[%node]',
82+
default => '(is_object($ʟ_tmp = %node) ? $ʟ_tmp : $this->global->uiControl[$ʟ_tmp])',
83+
})
6784
. ') %line;'
68-
. ($this->print
69-
? 'echo $this->global->forms->renderFormBegin(%node) %1.line;'
70-
: '')
71-
. ' %3.node '
72-
. ($this->print
73-
? 'echo $this->global->forms->renderFormEnd() %4.line;'
74-
: '')
85+
. (match ($this->mode) {
86+
self::ModeScope, self::ModeLegacyScope => ' %3.node ',
87+
default => $renderBegin . ' %3.node ' . $renderEnd,
88+
})
7589
. '$this->global->forms->end();'
7690
. "\n\n",
7791
$this->name,

src/Bridges/FormsLatte/Runtime.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,11 @@ public function getScope(): Container
120120
{
121121
return end($this->stack) ?: throw new Nette\InvalidStateException('Form declaration is missing, did you use {form} or <form n:name> tag?');
122122
}
123+
124+
125+
/** Are we nested inside an already open form? */
126+
public function isNested(): bool
127+
{
128+
return (bool) $this->stack;
129+
}
123130
}
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+
5+
6+
7+
8+
<label for="frm-name">Name:</label>
9+
<input type="text" name="name" id="frm-name">
10+
11+
12+
13+
<form action="" method="post" class="outer">
14+
<input type="text" name="name" id="frm-name">
15+
16+
17+
<input type="text" name="person[city]" id="frm-person-city">
18+
19+
</form>
20+
21+
22+
23+
<form action="" method="post" class="outer">
24+
<input type="text" name="name" id="frm-name">
25+
26+
<input type="text" name="person[city]" id="frm-person-city">
27+
</form>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php %A%
2+
$this->global->forms->begin($form = (is_object($ʟ_tmp = 'myForm') ? $ʟ_tmp : ($this->global->forms->isNested() ? $this->global->forms->get($ʟ_tmp, Nette\Forms\Container::class) : $this->global->uiControl[$ʟ_tmp]))) /* pos 3:1 */;
3+
echo '
4+
';
5+
echo ($ʟ_label = $this->global->forms->get('name')->getLabel()) /* pos 4:2 */;
6+
echo '
7+
';
8+
echo $this->global->forms->get('name')->getControl() /* pos 5:2 */;
9+
echo "\n";
10+
$this->global->forms->end();
11+
12+
echo '
13+
14+
15+
';
16+
$this->global->forms->begin($form = $this->global->uiControl['myForm']) /* pos 11:1 */;
17+
echo '
18+
';
19+
echo ($ʟ_label = $this->global->forms->get('name')->getLabel()) /* pos 12:2 */;
20+
echo '
21+
';
22+
echo $this->global->forms->get('name')->getControl() /* pos 13:2 */;
23+
echo "\n";
24+
$this->global->forms->end();
25+
26+
echo '
27+
28+
29+
';
30+
$this->global->forms->begin($form = $this->global->uiControl['myForm']) /* pos 19:1 */;
31+
echo $this->global->forms->renderFormBegin(['class' => 'outer']) /* pos 19:1 */;
32+
echo '
33+
';
34+
echo $this->global->forms->get('name')->getControl() /* pos 20:2 */;
35+
echo '
36+
37+
';
38+
$this->global->forms->begin($form = (is_object($ʟ_tmp = 'person') ? $ʟ_tmp : ($this->global->forms->isNested() ? $this->global->forms->get($ʟ_tmp, Nette\Forms\Container::class) : $this->global->uiControl[$ʟ_tmp]))) /* pos 22:2 */;
39+
echo '
40+
';
41+
echo $this->global->forms->get('city')->getControl() /* pos 23:3 */;
42+
echo '
43+
';
44+
$this->global->forms->end();
45+
46+
echo "\n";
47+
echo $this->global->forms->renderFormEnd() /* pos 25:1 */;
48+
$this->global->forms->end();
49+
50+
echo '
51+
52+
53+
';
54+
$this->global->forms->begin($form = $this->global->uiControl['myForm']) /* pos 30:1 */;
55+
echo $this->global->forms->renderFormBegin(['class' => 'outer']) /* pos 30:1 */;
56+
echo '
57+
';
58+
echo $this->global->forms->get('name')->getControl() /* pos 31:2 */;
59+
echo '
60+
61+
';
62+
$this->global->forms->begin($formContainer = $this->global->forms->get('person', Nette\Forms\Container::class)) /* pos 33:2 */;
63+
echo ' ';
64+
echo $this->global->forms->get('city')->getControl() /* pos 34:3 */;
65+
echo "\n";
66+
67+
$this->global->forms->end();
68+
$formContainer = $this->global->forms->getScope();
69+
echo $this->global->forms->renderFormEnd() /* pos 36:1 */;
70+
$this->global->forms->end();
71+
72+
echo "\n";
73+
%A%

tests/Forms.Latte/forms.scope.phpt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
$person = $form->addContainer('person');
13+
$person->addText('city', 'City:');
14+
15+
$latte = new Latte\Engine;
16+
$latte->addExtension(new FormsExtension);
17+
$latte->addProvider('uiControl', ['myForm' => $form]);
18+
19+
Assert::matchFile(
20+
__DIR__ . '/expected/forms.scope.php',
21+
$latte->compile(__DIR__ . '/templates/forms.scope.latte'),
22+
);
23+
24+
Assert::matchFile(
25+
__DIR__ . '/expected/forms.scope.html',
26+
$latte->renderToString(__DIR__ . '/templates/forms.scope.latte'),
27+
);
28+
29+
30+
test('arguments are rejected in {form scope} (no <form> to apply them to)', function () {
31+
$latte = new Latte\Engine;
32+
$latte->setLoader(new Latte\Loaders\StringLoader);
33+
$latte->addExtension(new FormsExtension);
34+
35+
Assert::exception(
36+
fn() => $latte->compile('{form scope myForm, class: x}{/form}'),
37+
Latte\CompileException::class,
38+
'Arguments are not allowed in {form scope}%a%',
39+
);
40+
});
41+
42+
43+
test('arguments are rejected in {formContext}', function () {
44+
$latte = new Latte\Engine;
45+
$latte->setLoader(new Latte\Loaders\StringLoader);
46+
$latte->addExtension(new FormsExtension);
47+
48+
Assert::exception(
49+
fn() => $latte->compile('{formContext myForm, class: x}{/formContext}'),
50+
Latte\CompileException::class,
51+
'Arguments are not allowed in {formContext}%a%',
52+
);
53+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
{/form}
7+
8+
9+
{* Same with {formContext} — produces identical output. *}
10+
11+
{formContext myForm}
12+
{label name /}
13+
{input name}
14+
{/formContext}
15+
16+
17+
{* Nested: {form scope} inside a form resolves a child container relatively, like {formContainer}. *}
18+
19+
{form myForm, class: "outer"}
20+
{input name}
21+
22+
{form scope person}
23+
{input city}
24+
{/form}
25+
{/form}
26+
27+
28+
{* The same nested block written with {formContainer} — resolves the same child container. *}
29+
30+
{form myForm, class: "outer"}
31+
{input name}
32+
33+
{formContainer person}
34+
{input city}
35+
{/formContainer}
36+
{/form}

0 commit comments

Comments
 (0)