Skip to content

Commit

Permalink
TeamForm: Switch to contributte/forms-multiplier
Browse files Browse the repository at this point in the history
`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, the per-category minimum person limits will not prevent submission.
  • Loading branch information
jtojnar committed Apr 4, 2023
1 parent 868ec5e commit 420a8e6
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 174 deletions.
103 changes: 46 additions & 57 deletions app/Components/TeamForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@
use App\Model\Configuration\Fields;
use App\Model\Configuration\Fields\Field;
use App\Model\InputModifier;
use Contributte\FormMultiplier\Multiplier;
use Contributte\Translation\Wrappers\Message;
use Contributte\Translation\Wrappers\NotTranslate;
use Kdyby\Replicator\Container as ReplicatorContainer;
use Nette\Application\UI;
use Nette\Forms\Container;
use Nette\Forms\Controls;
use Nette\Forms\Controls\SubmitButton;
use Nette\Localization\Translator;
use Nette\Utils\Json;
use Nextras\FormComponents\Controls\DateControl;
Expand Down Expand Up @@ -46,7 +45,8 @@ public function __construct(
$group->setOption('container', 'div aria-hidden="true" class="visually-hidden"');
// Browsers consider the first submit button a default submit button for use when submitting the form using Enter key.
// Let’s add the save button to the top, to prevent the remove button of the first container from being picked.
$this->addSubmit('save_default_submit', 'messages.team.action.register')->getControlPrototype()->setHtmlAttribute('aria-hidden', 'true')->setHtmlAttribute('tabindex', '-1');
$defaultSaveButton = $this->addSubmit('save_default_submit', 'messages.team.action.register');
$defaultSaveButton->getControlPrototype()->setHtmlAttribute('aria-hidden', 'true')->setHtmlAttribute('tabindex', '-1');

$this->addProtection();
$this->addGroup('messages.team.info.label');
Expand All @@ -60,20 +60,20 @@ public function __construct(
$rule->addRule(function(CategoryEntry $entry) use ($defaultMaxMembers): bool {
$category = $entry->getValue();
$maxMembers = $this->entries->categories->allCategories[$category]->maxMembers ?? $defaultMaxMembers;
/** @var ReplicatorContainer */
$replicator = $entry->form['persons'];
/** @var Multiplier */ // For PHPStan.
$multiplier = $entry->form['persons'];

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

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

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

$fields = $this->entries->teamFields;
Expand All @@ -82,23 +82,12 @@ public function __construct(
$this->addTextArea('message', 'messages.team.message.label');

$this->setCurrentGroup();
$this->addSubmit('save', $isEditing ? 'messages.team.action.edit' : 'messages.team.action.register');
$this->addSubmit('add', 'messages.team.action.add')->setValidationScope([])->onClick[] = function(SubmitButton $button) use ($defaultMaxMembers): void {
$category = $button->form->getUnsafeValues(null)['category'];
$maxMembers = $this->entries->categories->allCategories[$category]->maxMembers ?? $defaultMaxMembers;
/** @var ReplicatorContainer */
$replicator = $button->form['persons'];
if (iterator_count($replicator->getContainers()) < $maxMembers) {
$replicator->createOne();
} else {
$button->form->addError($this->translator->translate('messages.team.error.too_many_members', $maxMembers, ['category' => $category]), false);
}
};
$saveButton = $this->addSubmit('save', $isEditing ? 'messages.team.action.edit' : 'messages.team.action.register');

$fields = $this->entries->personFields;
$i = 0;
$persons = new ReplicatorContainer(
factory: function(Container $container) use (&$i, $fields, $defaultMinMembers): void {
$persons = new Multiplier(
factory: function(Container $container, self $form) use (&$i, $fields): void {
++$i;
$group = $this->addGroup();
$group->setOption('label', new Message('messages.team.person.label', $i));
Expand All @@ -121,48 +110,48 @@ public function __construct(
$email->setRequired();
$group->setOption('description', 'messages.team.person.isContact');
}

$container->addSubmit('remove', 'messages.team.action.remove')->setValidationScope([])->onClick[] = function(SubmitButton $button) use ($container, $defaultMinMembers): void {
$category = $button->form->getUnsafeValues(null)['category'];
$minMembers = $this->entries->categories->allCategories[$category]->minMembers ?? $defaultMinMembers;
/** @var ReplicatorContainer */ // For PHPStan.
$replicator = $button->form['persons'];
if (iterator_count($replicator->getContainers()) > $minMembers) {
$replicator->remove($container, true);
} else {
$button->form->addError($this->translator->translate('messages.team.error.too_few_members', $minMembers, ['category' => $category]), false);
}
};
},
createDefault: $initialMembers,
forceDefault: true,
copyNumber: $initialMembers,
maxCopies: $defaultMaxMembers,
);
$this['persons'] = $persons;
$persons->onCreateComponents[] = function(Multiplier $multiplier) use ($defaultMaxMembers): void {
if (!$this->isSubmitted()) {
return;
}

$category = $this->getUnsafeValues(null)['category'];
$count = iterator_count($multiplier->getContainers());

$multiplierReflection = new \ReflectionClass(Multiplier::class);
$httpData = $multiplierReflection->getProperty('httpData');
$httpData->setAccessible(true);
$values = $multiplierReflection->getProperty('values');
$values->setAccessible(true);
$resolver = new \Contributte\FormMultiplier\ComponentResolver($httpData->getValue($multiplier), $values->getValue($multiplier), $multiplier->getMaxCopies(), $multiplier->getMinCopies());

$maxMembers = $this->entries->categories->allCategories[$category]->maxMembers ?? $defaultMaxMembers;
if ($resolver->isCreateAction() && $count >= $maxMembers) {
$this->addError($this->translator->translate('messages.team.error.too_many_members', $maxMembers, ['category' => $category]), false);
}
};
$persons->setMinCopies($defaultMinMembers);
$persons->addCreateButton('messages.team.action.add')->setNoValidate();
$persons->addRemoveButton('messages.team.action.remove');

$this->onRender[] = $this->updatePersonButtonsState(...);
$this->onValidate[] = $this->checkCategoryConstraints(...);
}

private function updatePersonButtonsState(): void {
/** @var \Kdyby\Replicator\Container */
$persons = $this['persons'];
$count = iterator_count($persons->getContainers());
$minMembers = $this->entries->minMembers;
$maxMembers = $this->entries->maxMembers;

if ($count >= $maxMembers) {
/** @var Controls\SubmitButton */
$add = $this['add'];
$add->setDisabled();
}
$minMembersCheck = function(Controls\SubmitButton $button) use ($persons, $defaultMinMembers): void {
$category = $this->getUnsafeValues(null)['category'];
$count = iterator_count($persons->getContainers());

if ($count <= $minMembers) {
foreach ($persons->getContainers() as $person) {
/** @var Controls\SubmitButton */
$remove = $person['remove'];
$remove->setDisabled();
$minMembers = $this->entries->categories->allCategories[$category]->minMembers ?? $defaultMinMembers;
if ($count < $minMembers) {
$this->addError($this->translator->translate('messages.team.error.too_few_members', $minMembers, ['category' => $category]), false);
}
}
};
$defaultSaveButton->onClick[] = $minMembersCheck;
$saveButton->onClick[] = $minMembersCheck;
}

private function addCustomFields(array $fields, Container $container): void {
Expand Down
82 changes: 40 additions & 42 deletions app/Presenters/TeamPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
use App\Model\Orm\Invoice\Invoice;
use App\Model\Orm\ItemReservation\ItemReservation;
use App\Model\Orm\Team\Team;
use Contributte\FormMultiplier\Multiplier;
use DateTimeImmutable;
use Exception;
use Kdyby\Replicator\Container as ReplicatorContainer;
use Latte;
use Nette;
use Nette\Application\ForbiddenRequestException;
Expand Down Expand Up @@ -123,10 +123,10 @@ public function renderRegister(): void {
if ($form->isSubmitted() === false) {
// Create sufficient number of person subforms for the most common team size (when it is greater than minimum team size).
$remainingMembers = $this->entries->initialMembers - $this->entries->minMembers;
/** @var ReplicatorContainer */
$replicator = $form['persons'];
/** @var Multiplier */ // For PHPStan.
$multiplier = $form['persons'];
for ($i = $remainingMembers; $i > 0; --$i) {
$replicator->createOne();
$multiplier->addCopy();
}
}
}
Expand Down Expand Up @@ -156,50 +156,48 @@ public function renderEdit(int $id = null): void {
}

$form = $this->getComponent('teamForm');
if ($form->isSubmitted() === false) {
$default = [];
$default['name'] = $team->name;
$default['category'] = $team->category;
$default['message'] = $team->message;
$default['persons'] = [];

$fields = $this->entries->teamFields;
$default = [];
$default['name'] = $team->name;
$default['category'] = $team->category;
$default['message'] = $team->message;
$default['persons'] = [];

$fields = $this->entries->teamFields;
foreach ($fields as $field) {
$name = $field->name;
if (isset($team->getJsonData()->$name)) {
$default[$name] = $team->getJsonData()->$name;
} elseif ($field instanceof Fields\SportidentField) {
$default[$name] = [
SportidentControl::NAME_NEEDED => true,
];
}
}

$fields = $this->entries->personFields;
foreach ($team->persons as $person) {
$personDefault = [
'firstname' => $person->firstname,
'lastname' => $person->lastname,
'gender' => $person->gender,
'email' => $person->email,
'birth' => $person->birth,
];

foreach ($fields as $field) {
$name = $field->name;
if (isset($team->getJsonData()->$name)) {
$default[$name] = $team->getJsonData()->$name;
if (isset($person->getJsonData()->$name)) {
$personDefault[$name] = $person->getJsonData()->$name;
} elseif ($field instanceof Fields\SportidentField) {
$default[$name] = [
$personDefault[$name] = [
SportidentControl::NAME_NEEDED => true,
];
}
}

$fields = $this->entries->personFields;
foreach ($team->persons as $person) {
$personDefault = [
'firstname' => $person->firstname,
'lastname' => $person->lastname,
'gender' => $person->gender,
'email' => $person->email,
'birth' => $person->birth,
];

foreach ($fields as $field) {
$name = $field->name;
if (isset($person->getJsonData()->$name)) {
$personDefault[$name] = $person->getJsonData()->$name;
} elseif ($field instanceof Fields\SportidentField) {
$personDefault[$name] = [
SportidentControl::NAME_NEEDED => true,
];
}
}

$default['persons'][] = $personDefault;
}
$form->setValues($default);
$default['persons'][] = $personDefault;
}
$form->setDefaults($default);
}
}

Expand Down Expand Up @@ -489,9 +487,9 @@ private function processTeamForm(Nette\Forms\Controls\SubmitButton $button): voi
/** @var ?string $firstMemberName */
$firstMemberName = null;

/** @var ReplicatorContainer */
$replicator = $form['persons'];
$personContainers = iterator_to_array($replicator->getContainers());
/** @var Multiplier */ // For PHPStan.
$multiplier = $form['persons'];
$personContainers = iterator_to_array($multiplier->getContainers());
foreach ($values['persons'] as $personKey => $member) {
$personContainer = $personContainers[$personKey];
$firstname = $member['firstname'];
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
],
"require": {
"php": ">= 8.1",
"contributte/forms-multiplier": "^3.3",
"contributte/mail": "^0.6.0",
"contributte/translation": "^2.0",
"kdyby/forms-replicator": "^2.0.0",
"latte/latte": "~2.5",
"moneyphp/money": "^4.0",
"nette/application": "~3.0",
Expand Down
Loading

0 comments on commit 420a8e6

Please sign in to comment.