Skip to content

Commit

Permalink
teamform: Switch to contributte/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.

The switch also allowed us to remove the outdated explicit anchoring
by passing parent and name in constructor, letting Nette handle it.
A disadvantage is the inability to access stuff like `htmlId`/`isSubmitted()`
without being anchored – but that can be solved by using `monitor()`.

Currently, we cannot control per-category person limits at submission.
  • Loading branch information
jtojnar committed Sep 18, 2022
1 parent 4de4175 commit 802063d
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 158 deletions.
25 changes: 2 additions & 23 deletions app/components/TeamForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use Contributte\Translation\Wrappers\NotTranslate;
use Nette\Application\UI;
use Nette\ComponentModel\IContainer;
use Nette\Forms\Container;
use Nette\Forms\Controls;
use Nette\Utils\Json;
Expand All @@ -24,33 +23,13 @@ class TeamForm extends UI\Form {
/** @var string */
private $locale;

public function __construct(array $countries, array $parameters, string $locale, IContainer $parent = null, string $name = null) {
parent::__construct($parent, $name);
public function __construct(array $countries, array $parameters, string $locale) {
parent::__construct();
$this->countries = $countries;
$this->parameters = $parameters;
$this->locale = $locale;
}

public function onRender(): void {
/** @var \Kdyby\Replicator\Container */
$persons = $this['persons'];
$count = iterator_count($persons->getContainers());
$minMembers = $this->parameters['minMembers'];
$maxMembers = $this->parameters['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 {
$locale = $this->locale;

Expand Down
16 changes: 9 additions & 7 deletions app/config/CustomInputModifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ class CustomInputModifier {
public static function modify(Control $input, IContainer $container): void {
// we also have some inputs that are based on Nextras\FormComponents\Fragments\UIComponent\BaseControl
if ($input instanceof BaseControl && $input->getName() === 'registry_address') {
$pairId = 'pair-' . $input->htmlId;
$input->setOption('id', $pairId);
$input->monitor(Form::class, function(Form $form) use ($input, $container): void {
$pairId = 'pair-' . $input->htmlId;
$input->setOption('id', $pairId);

/** @var BaseControl */
$country = $container->getComponent('country');
$country->addCondition(Form::NOT_EQUAL, 46)->toggle($pairId, false);
$input->setRequired(false);
$input->addConditionOn($country, Form::EQUAL, 46)->setRequired();
/** @var BaseControl */
$country = $container->getComponent('country');
$country->addCondition(Form::NOT_EQUAL, 46)->toggle($pairId, false);
$input->setRequired(false);
$input->addConditionOn($country, Form::EQUAL, 46)->setRequired();
});
}
}
}
2 changes: 1 addition & 1 deletion app/config/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,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
79 changes: 37 additions & 42 deletions app/forms/TeamFormFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,14 @@
use App\Components\CategoryEntry;
use App\Components\TeamForm;
use App\Model\CategoryData;
use Contributte\FormMultiplier\Multiplier;
use Contributte\Translation\Wrappers\Message;
use Kdyby\Replicator\Container as ReplicatorContainer;
use Nette;
use Nette\ComponentModel\IContainer;
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;
use function nspl\a\last;

final class TeamFormFactory {
use Nette\SmartObject;
Expand All @@ -36,8 +34,8 @@ public function __construct(CategoryData $categories, Nette\DI\Container $contex
$this->translator = $translator;
}

public function create(array $countries, string $locale, bool $editing = false, IContainer $parent = null, string $name = null): TeamForm {
$form = new TeamForm($countries, $this->parameters, $locale, $parent, $name);
public function create(array $countries, string $locale, bool $editing = false): TeamForm {
$form = new TeamForm($countries, $this->parameters, $locale);

$form->setTranslator($this->translator);
$renderer = new Bs5FormRenderer();
Expand All @@ -47,7 +45,14 @@ public function create(array $countries, string $locale, bool $editing = false,

$defaultMinMembers = $this->parameters['minMembers'];
$defaultMaxMembers = $this->parameters['maxMembers'];
$initialMembers = $form->isSubmitted() || $editing ? $defaultMinMembers : ($this->parameters['initialMembers'] ?? $defaultMinMembers);
$initialMembers = $this->parameters['initialMembers'] ?? $defaultMinMembers;

// 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');
Expand All @@ -69,20 +74,20 @@ public function create(array $countries, string $locale, bool $editing = false,
$rule->addRule(function(CategoryEntry $entry) use ($defaultMaxMembers): bool {
$category = $entry->getValue();
$maxMembers = $this->categories->getCategoryData()[$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->categories->getCategoryData()[$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->parameters['fields']['team'];
Expand All @@ -91,37 +96,11 @@ public function create(array $countries, string $locale, bool $editing = false,
$form->addTextArea('message', 'messages.team.message.label');

$form->setCurrentGroup();
$form->addSubmit('save', '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->categories->getCategoryData()[$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->categories->getCategoryData()[$category]['minMembers'] ?? $defaultMinMembers;
/** @var ReplicatorContainer */ // For PHPStan.
$replicator = $button->form['persons'];
if (iterator_count($replicator->getContainers()) > $minMembers) {
/** @var ?Container */ // For PHPStan.
$lastPerson = last($replicator->getContainers());
if ($lastPerson) {
$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', 'messages.team.action.register');

$fields = $this->parameters['fields']['person'];
$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 @@ -144,7 +123,23 @@ public function create(array $countries, string $locale, bool $editing = false,
$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'];
$categoryData = $this->categories->getCategoryData()[$category];
$maxMembers = $categoryData['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
15 changes: 6 additions & 9 deletions app/presenters/TeamPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,11 @@ public function actionExport(string $type = 'csv'): void {
}
}

protected function createComponentTeamForm(string $name): Form {
protected function createComponentTeamForm(): Form {
$idParam = $this->getParameter('id');
$editing = $idParam !== null;
$form = $this->teamFormFactory->create($this->countries->fetchIdNamePairs(), $this->locale, $editing, $this, $name);
if ($editing && !$form->isSubmitted()) {
$form = $this->teamFormFactory->create($this->countries->fetchIdNamePairs(), $this->locale, $editing);
if ($editing) {
\assert(\is_string($idParam)); // For PHPStan.
$id = (int) $idParam;
$team = $this->teams->getById($id);
Expand Down Expand Up @@ -258,21 +258,20 @@ protected function createComponentTeamForm(string $name): Form {

$default['persons'][] = $personDefault;
}
$form->setValues($default);
$form->setDefaults($default);
}
/** @var \Nette\Forms\Controls\SubmitButton */
$save = $form['save'];
if ($this->getParameter('id')) {
$save->caption = 'messages.team.action.edit';
}
/** @var callable(Nette\Forms\Controls\SubmitButton): void */
$processTeamForm = Closure::fromCallable([$this, 'processTeamForm']);
$save->onClick[] = $processTeamForm;
$form->onSuccess[] = $processTeamForm;

return $form;
}

private function processTeamForm(Nette\Forms\Controls\SubmitButton $button): void {
private function processTeamForm(App\Components\TeamForm $form): void {
if (!$this->user->isInRole('admin')) {
if ($this->context->parameters['entries']['closing']->diff(new DateTime())->invert === 0) {
throw new App\TooLateForAccessException();
Expand All @@ -281,8 +280,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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
],
"require": {
"php": ">= 8.0",
"contributte/forms-multiplier": "^3.2",
"contributte/mail": "^0.6.0",
"contributte/translation": "^1.0",
"ihor/nspl": "^1.3",
"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 802063d

Please sign in to comment.