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, we cannot control per-category person limits at submission.
  • Loading branch information
jtojnar committed Feb 11, 2023
1 parent 162bfa8 commit 2e73698
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 183 deletions.
21 changes: 0 additions & 21 deletions app/Components/TeamForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use Contributte\Translation\Wrappers\NotTranslate;
use Nette\Application\UI;
use Nette\Forms\Container;
use Nette\Forms\Controls;
use Nette\Utils\Json;

/**
Expand All @@ -30,26 +29,6 @@ public function __construct(
parent::__construct();
}

public function onRender(): 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();
}

if ($count <= $minMembers) {
/** @var Controls\SubmitButton */
$remove = $this['remove'];
$remove->setDisabled();
}
}

public function addCustomFields(array $fields, Container $container): void {
foreach ($fields as $field) {
$name = $field->name;
Expand Down
2 changes: 1 addition & 1 deletion app/Config/common.neon
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ extensions:
translation: Contributte\Translation\DI\TranslationExtension
orm: Nextras\Orm\Bridges\NetteDI\OrmExtension
dbal: Nextras\Dbal\Bridges\NetteDI\DbalExtension
replicator: Kdyby\Replicator\DI\ReplicatorExtension
multiplier: Contributte\FormMultiplier\DI\MultiplierExtension
contribMail: Contributte\Mail\DI\MailExtension

contribMail:
Expand Down
70 changes: 33 additions & 37 deletions app/Forms/TeamFormFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@

use App\Components\CategoryEntry;
use App\Components\TeamForm;
use App\Helpers\Iter;
use App\Model\Configuration\Entries;
use Contributte\FormMultiplier\Multiplier;
use Contributte\Translation\Wrappers\Message;
use Kdyby\Replicator\Container as ReplicatorContainer;
use Nette;
use Nette\Forms\Container;
use Nette\Forms\Controls\SubmitButton;
use Nette\Forms\Form;
use Nette\Localization\Translator;
use Nextras\FormComponents\Controls\DateControl;
use Nextras\FormsRendering\Renderers\Bs5FormRenderer;
Expand Down Expand Up @@ -54,6 +53,13 @@ public function create(
// Handled in TeamPresenter::renderCreate.
$initialMembers = $this->entries->minMembers;

// Group for top submit button, since DefaultFormRenderer renders group before all ungrouped Controls.
$group = $form->addGroup();
$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.
$form->addSubmit('save_default_submit', 'messages.team.action.register')->getControlPrototype()->setHtmlAttribute('aria-hidden', 'true')->setHtmlAttribute('tabindex', '-1');

$form->addProtection();
$form->addGroup('messages.team.info.label');
$form->addText('name', 'messages.team.name.label')->setRequired();
Expand All @@ -77,20 +83,20 @@ public function create(
$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 @@ -99,36 +105,11 @@ public function create(
$form->addTextArea('message', 'messages.team.message.label');

$form->setCurrentGroup();
$form->addSubmit('save', $isEditing ? 'messages.team.action.edit' : 'messages.team.action.register');
$form->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);
}
};
$form->addSubmit('remove', 'messages.team.action.remove')->setValidationScope([])->onClick[] = function(SubmitButton $button) use ($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) {
$lastPerson = Iter::last($replicator->getContainers());
if ($lastPerson !== null) {
$replicator->remove($lastPerson, true);
}
} else {
$button->form->addError($this->translator->translate('messages.team.error.too_few_members', $minMembers, ['category' => $category]), false);
}
};
$renderer->primaryButton = $form->addSubmit('save', $isEditing ? 'messages.team.action.edit' : 'messages.team.action.register');

$fields = $this->entries->personFields;
$i = 0;
$form->addDynamic('persons', function(Container $container) use (&$i, $fields, $form): void {
$multiplier = $form->addMultiplier('persons', function(Container $container, TeamForm $form) use (&$i, $fields): void {
++$i;
$group = $form->addGroup();
$group->setOption('label', new Message('messages.team.person.label', $i));
Expand All @@ -151,7 +132,22 @@ public function create(
$email->setRequired();
$group->setOption('description', 'messages.team.person.isContact');
}
}, $initialMembers, true);
}, $initialMembers, $defaultMaxMembers);
$multiplier->onCreateComponents[] = function(Multiplier $multiplier) use ($form, $defaultMaxMembers): void {
if (!$form->isSubmitted()) {
return;
}

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

return $form;
}
Expand Down
90 changes: 42 additions & 48 deletions app/Presenters/TeamPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,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 @@ -122,10 +122,10 @@ public function renderRegister(): void {
if (!$form->isSubmitted()) {
// 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 @@ -155,50 +155,48 @@ public function renderEdit(int $id = null): void {
}

$form = $this->getComponent('teamForm');
if (!$form->isSubmitted()) {
$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 @@ -296,14 +294,12 @@ protected function createComponentTeamForm(): Form {
isEditing: $isEditing,
);

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

return $form;
}

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

/** @var App\Components\TeamForm $form */
$form = $button->form;
/** @var array */ // actually \ArrayAccess but PHPStan does not handle that very well.
$values = $form->getValues();
/** @var string $password */
Expand Down Expand Up @@ -485,9 +479,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 2e73698

Please sign in to comment.