Skip to content

Commit 2e73698

Browse files
committed
TeamForm: Switch to contributte/forms-multiplier
`kdyby/replicator` served us well for a long time but unfortunately, it has been unmaintained for a while now. Let’s switch to an actively maintained library. Multiplier manages adding and removing for us and handles default values correctly. Currently, we cannot control per-category person limits at submission.
1 parent 162bfa8 commit 2e73698

7 files changed

Lines changed: 145 additions & 183 deletions

File tree

app/Components/TeamForm.php

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use Contributte\Translation\Wrappers\NotTranslate;
1313
use Nette\Application\UI;
1414
use Nette\Forms\Container;
15-
use Nette\Forms\Controls;
1615
use Nette\Utils\Json;
1716

1817
/**
@@ -30,26 +29,6 @@ public function __construct(
3029
parent::__construct();
3130
}
3231

33-
public function onRender(): void {
34-
/** @var \Kdyby\Replicator\Container */
35-
$persons = $this['persons'];
36-
$count = iterator_count($persons->getContainers());
37-
$minMembers = $this->entries->minMembers;
38-
$maxMembers = $this->entries->maxMembers;
39-
40-
if ($count >= $maxMembers) {
41-
/** @var Controls\SubmitButton */
42-
$add = $this['add'];
43-
$add->setDisabled();
44-
}
45-
46-
if ($count <= $minMembers) {
47-
/** @var Controls\SubmitButton */
48-
$remove = $this['remove'];
49-
$remove->setDisabled();
50-
}
51-
}
52-
5332
public function addCustomFields(array $fields, Container $container): void {
5433
foreach ($fields as $field) {
5534
$name = $field->name;

app/Config/common.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ extensions:
4242
translation: Contributte\Translation\DI\TranslationExtension
4343
orm: Nextras\Orm\Bridges\NetteDI\OrmExtension
4444
dbal: Nextras\Dbal\Bridges\NetteDI\DbalExtension
45-
replicator: Kdyby\Replicator\DI\ReplicatorExtension
45+
multiplier: Contributte\FormMultiplier\DI\MultiplierExtension
4646
contribMail: Contributte\Mail\DI\MailExtension
4747

4848
contribMail:

app/Forms/TeamFormFactory.php

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@
66

77
use App\Components\CategoryEntry;
88
use App\Components\TeamForm;
9-
use App\Helpers\Iter;
109
use App\Model\Configuration\Entries;
10+
use Contributte\FormMultiplier\Multiplier;
1111
use Contributte\Translation\Wrappers\Message;
12-
use Kdyby\Replicator\Container as ReplicatorContainer;
1312
use Nette;
1413
use Nette\Forms\Container;
15-
use Nette\Forms\Controls\SubmitButton;
14+
use Nette\Forms\Form;
1615
use Nette\Localization\Translator;
1716
use Nextras\FormComponents\Controls\DateControl;
1817
use Nextras\FormsRendering\Renderers\Bs5FormRenderer;
@@ -54,6 +53,13 @@ public function create(
5453
// Handled in TeamPresenter::renderCreate.
5554
$initialMembers = $this->entries->minMembers;
5655

56+
// Group for top submit button, since DefaultFormRenderer renders group before all ungrouped Controls.
57+
$group = $form->addGroup();
58+
$group->setOption('container', 'div aria-hidden="true" class="visually-hidden"');
59+
// Browsers consider the first submit button a default submit button for use when submitting the form using Enter key.
60+
// Let’s add the save button to the top, to prevent the remove button of the first container from being picked.
61+
$form->addSubmit('save_default_submit', 'messages.team.action.register')->getControlPrototype()->setHtmlAttribute('aria-hidden', 'true')->setHtmlAttribute('tabindex', '-1');
62+
5763
$form->addProtection();
5864
$form->addGroup('messages.team.info.label');
5965
$form->addText('name', 'messages.team.name.label')->setRequired();
@@ -77,20 +83,20 @@ public function create(
7783
$rule->addRule(function(CategoryEntry $entry) use ($defaultMaxMembers): bool {
7884
$category = $entry->getValue();
7985
$maxMembers = $this->entries->categories->allCategories[$category]->maxMembers ?? $defaultMaxMembers;
80-
/** @var ReplicatorContainer */
81-
$replicator = $entry->form['persons'];
86+
/** @var Multiplier */ // For PHPStan.
87+
$multiplier = $entry->form['persons'];
8288

83-
return iterator_count($replicator->getContainers()) <= $maxMembers;
89+
return $multiplier->getCopyNumber() <= $maxMembers;
8490
}, 'messages.team.error.too_many_members_simple'); // TODO: add params like in add/remove buttons
8591

8692
$rule = $category->addCondition(true); // not to block the export of rules to JS
8793
$rule->addRule(function(CategoryEntry $entry) use ($defaultMinMembers): bool {
8894
$category = $entry->getValue();
8995
$minMembers = $this->entries->categories->allCategories[$category]->minMembers ?? $defaultMinMembers;
90-
/** @var ReplicatorContainer */
91-
$replicator = $entry->form['persons'];
96+
/** @var Multiplier */ // For PHPStan.
97+
$multiplier = $entry->form['persons'];
9298

93-
return iterator_count($replicator->getContainers()) >= $minMembers;
99+
return $multiplier->getCopyNumber() >= $minMembers;
94100
}, 'messages.team.error.too_few_members_simple');
95101

96102
$fields = $this->entries->teamFields;
@@ -99,36 +105,11 @@ public function create(
99105
$form->addTextArea('message', 'messages.team.message.label');
100106

101107
$form->setCurrentGroup();
102-
$form->addSubmit('save', $isEditing ? 'messages.team.action.edit' : 'messages.team.action.register');
103-
$form->addSubmit('add', 'messages.team.action.add')->setValidationScope([])->onClick[] = function(SubmitButton $button) use ($defaultMaxMembers): void {
104-
$category = $button->form->getUnsafeValues(null)['category'];
105-
$maxMembers = $this->entries->categories->allCategories[$category]->maxMembers ?? $defaultMaxMembers;
106-
/** @var ReplicatorContainer */
107-
$replicator = $button->form['persons'];
108-
if (iterator_count($replicator->getContainers()) < $maxMembers) {
109-
$replicator->createOne();
110-
} else {
111-
$button->form->addError($this->translator->translate('messages.team.error.too_many_members', $maxMembers, ['category' => $category]), false);
112-
}
113-
};
114-
$form->addSubmit('remove', 'messages.team.action.remove')->setValidationScope([])->onClick[] = function(SubmitButton $button) use ($defaultMinMembers): void {
115-
$category = $button->form->getUnsafeValues(null)['category'];
116-
$minMembers = $this->entries->categories->allCategories[$category]->minMembers ?? $defaultMinMembers;
117-
/** @var ReplicatorContainer */ // For PHPStan.
118-
$replicator = $button->form['persons'];
119-
if (iterator_count($replicator->getContainers()) > $minMembers) {
120-
$lastPerson = Iter::last($replicator->getContainers());
121-
if ($lastPerson !== null) {
122-
$replicator->remove($lastPerson, true);
123-
}
124-
} else {
125-
$button->form->addError($this->translator->translate('messages.team.error.too_few_members', $minMembers, ['category' => $category]), false);
126-
}
127-
};
108+
$renderer->primaryButton = $form->addSubmit('save', $isEditing ? 'messages.team.action.edit' : 'messages.team.action.register');
128109

129110
$fields = $this->entries->personFields;
130111
$i = 0;
131-
$form->addDynamic('persons', function(Container $container) use (&$i, $fields, $form): void {
112+
$multiplier = $form->addMultiplier('persons', function(Container $container, TeamForm $form) use (&$i, $fields): void {
132113
++$i;
133114
$group = $form->addGroup();
134115
$group->setOption('label', new Message('messages.team.person.label', $i));
@@ -151,7 +132,22 @@ public function create(
151132
$email->setRequired();
152133
$group->setOption('description', 'messages.team.person.isContact');
153134
}
154-
}, $initialMembers, true);
135+
}, $initialMembers, $defaultMaxMembers);
136+
$multiplier->onCreateComponents[] = function(Multiplier $multiplier) use ($form, $defaultMaxMembers): void {
137+
if (!$form->isSubmitted()) {
138+
return;
139+
}
140+
141+
$category = $form->getUnsafeValues(null)['category'];
142+
$maxMembers = $this->entries->categories->allCategories[$category]->maxMembers ?? $defaultMaxMembers;
143+
$count = iterator_count($multiplier->getContainers());
144+
if ($count >= $maxMembers) {
145+
$form->addError($this->translator->translate('messages.team.error.too_many_members', $maxMembers, ['category' => $category]), false);
146+
}
147+
};
148+
$multiplier->setMinCopies($defaultMinMembers);
149+
$multiplier->addCreateButton('messages.team.action.add')->setNoValidate();
150+
$multiplier->addRemoveButton('messages.team.action.remove');
155151

156152
return $form;
157153
}

app/Presenters/TeamPresenter.php

Lines changed: 42 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
use App\Model\Orm\Invoice\Invoice;
1414
use App\Model\Orm\ItemReservation\ItemReservation;
1515
use App\Model\Orm\Team\Team;
16+
use Contributte\FormMultiplier\Multiplier;
1617
use DateTimeImmutable;
1718
use Exception;
18-
use Kdyby\Replicator\Container as ReplicatorContainer;
1919
use Latte;
2020
use Nette;
2121
use Nette\Application\ForbiddenRequestException;
@@ -122,10 +122,10 @@ public function renderRegister(): void {
122122
if (!$form->isSubmitted()) {
123123
// Create sufficient number of person subforms for the most common team size (when it is greater than minimum team size).
124124
$remainingMembers = $this->entries->initialMembers - $this->entries->minMembers;
125-
/** @var ReplicatorContainer */
126-
$replicator = $form['persons'];
125+
/** @var Multiplier */ // For PHPStan.
126+
$multiplier = $form['persons'];
127127
for ($i = $remainingMembers; $i > 0; --$i) {
128-
$replicator->createOne();
128+
$multiplier->addCopy();
129129
}
130130
}
131131
}
@@ -155,50 +155,48 @@ public function renderEdit(int $id = null): void {
155155
}
156156

157157
$form = $this->getComponent('teamForm');
158-
if (!$form->isSubmitted()) {
159-
$default = [];
160-
$default['name'] = $team->name;
161-
$default['category'] = $team->category;
162-
$default['message'] = $team->message;
163-
$default['persons'] = [];
164-
165-
$fields = $this->entries->teamFields;
158+
$default = [];
159+
$default['name'] = $team->name;
160+
$default['category'] = $team->category;
161+
$default['message'] = $team->message;
162+
$default['persons'] = [];
163+
164+
$fields = $this->entries->teamFields;
165+
foreach ($fields as $field) {
166+
$name = $field->name;
167+
if (isset($team->getJsonData()->$name)) {
168+
$default[$name] = $team->getJsonData()->$name;
169+
} elseif ($field instanceof Fields\SportidentField) {
170+
$default[$name] = [
171+
SportidentControl::NAME_NEEDED => true,
172+
];
173+
}
174+
}
175+
176+
$fields = $this->entries->personFields;
177+
foreach ($team->persons as $person) {
178+
$personDefault = [
179+
'firstname' => $person->firstname,
180+
'lastname' => $person->lastname,
181+
'gender' => $person->gender,
182+
'email' => $person->email,
183+
'birth' => $person->birth,
184+
];
185+
166186
foreach ($fields as $field) {
167187
$name = $field->name;
168-
if (isset($team->getJsonData()->$name)) {
169-
$default[$name] = $team->getJsonData()->$name;
188+
if (isset($person->getJsonData()->$name)) {
189+
$personDefault[$name] = $person->getJsonData()->$name;
170190
} elseif ($field instanceof Fields\SportidentField) {
171-
$default[$name] = [
191+
$personDefault[$name] = [
172192
SportidentControl::NAME_NEEDED => true,
173193
];
174194
}
175195
}
176196

177-
$fields = $this->entries->personFields;
178-
foreach ($team->persons as $person) {
179-
$personDefault = [
180-
'firstname' => $person->firstname,
181-
'lastname' => $person->lastname,
182-
'gender' => $person->gender,
183-
'email' => $person->email,
184-
'birth' => $person->birth,
185-
];
186-
187-
foreach ($fields as $field) {
188-
$name = $field->name;
189-
if (isset($person->getJsonData()->$name)) {
190-
$personDefault[$name] = $person->getJsonData()->$name;
191-
} elseif ($field instanceof Fields\SportidentField) {
192-
$personDefault[$name] = [
193-
SportidentControl::NAME_NEEDED => true,
194-
];
195-
}
196-
}
197-
198-
$default['persons'][] = $personDefault;
199-
}
200-
$form->setValues($default);
197+
$default['persons'][] = $personDefault;
201198
}
199+
$form->setDefaults($default);
202200
}
203201
}
204202

@@ -296,14 +294,12 @@ protected function createComponentTeamForm(): Form {
296294
isEditing: $isEditing,
297295
);
298296

299-
/** @var \Nette\Forms\Controls\SubmitButton */
300-
$save = $form['save'];
301-
$save->onClick[] = $this->processTeamForm(...);
297+
$form->onSuccess[] = $this->processTeamForm(...);
302298

303299
return $form;
304300
}
305301

306-
private function processTeamForm(Nette\Forms\Controls\SubmitButton $button): void {
302+
private function processTeamForm(App\Components\TeamForm $form): void {
307303
$today = new DateTimeImmutable();
308304
if (!$this->user->isInRole('admin')) {
309305
if ($this->entries->closing !== null && $this->entries->closing < $today) {
@@ -313,8 +309,6 @@ private function processTeamForm(Nette\Forms\Controls\SubmitButton $button): voi
313309
}
314310
}
315311

316-
/** @var App\Components\TeamForm $form */
317-
$form = $button->form;
318312
/** @var array */ // actually \ArrayAccess but PHPStan does not handle that very well.
319313
$values = $form->getValues();
320314
/** @var string $password */
@@ -485,9 +479,9 @@ private function processTeamForm(Nette\Forms\Controls\SubmitButton $button): voi
485479
/** @var ?string $firstMemberName */
486480
$firstMemberName = null;
487481

488-
/** @var ReplicatorContainer */
489-
$replicator = $form['persons'];
490-
$personContainers = iterator_to_array($replicator->getContainers());
482+
/** @var Multiplier */ // For PHPStan.
483+
$multiplier = $form['persons'];
484+
$personContainers = iterator_to_array($multiplier->getContainers());
491485
foreach ($values['persons'] as $personKey => $member) {
492486
$personContainer = $personContainers[$personKey];
493487
$firstname = $member['firstname'];

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
],
1313
"require": {
1414
"php": ">= 8.1",
15+
"contributte/forms-multiplier": "^3.3",
1516
"contributte/mail": "^0.6.0",
1617
"contributte/translation": "^2.0",
17-
"kdyby/forms-replicator": "^2.0.0",
1818
"latte/latte": "~2.5",
1919
"moneyphp/money": "^4.0",
2020
"nette/application": "~3.0",

0 commit comments

Comments
 (0)