From 28250df46a52753724b530048bd18cfca9df95db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Fri, 9 May 2025 12:58:11 +0200 Subject: [PATCH 01/54] Refactors and enhances BaseForm component - Improves type safety by adding typehints and return types. - Disallows form submission via disabled submit buttons. - Uses untrusted values instead of unsafe values. - Removes the deprecated getForm() method. --- src/BaseForm.php | 42 ++++++++++++++---------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/src/BaseForm.php b/src/BaseForm.php index e1d38a2..043d5dc 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -7,12 +7,11 @@ use Nette\Application\UI\Presenter; use Nette\Utils\ArrayHash; use Nette\Utils\Callback; -use Nette\Utils\Reflection; use Nette\Utils\Type; use ReflectionException; +use ReflectionParameter; /** - * @property-read Form $form * @method onBeforeInitForm($form) * @method onAfterInitForm($form) * @method onBeforeValidateForm($form) @@ -21,16 +20,12 @@ */ abstract class BaseForm extends Control { - /** @var Form */ - protected $form; + private Form $form; - /** @var string|null */ public ?string $templateFilename = null; - /** @var bool */ public bool $isAjax = true; - /** @var bool */ public bool $emptyHiddenToggleControls = false; /** @var callable[] */ @@ -68,7 +63,7 @@ abstract class BaseForm extends Control public function __construct() { - $this->paramResolvers[] = function(string $type, $values = null) { + $this->paramResolvers[] = function(string $type, object|array|null $values) { if ($type === Form::class || is_subclass_of($type, Form::class)) { return $this->form; } elseif ($type === Presenter::class || is_subclass_of($type, Presenter::class)) { @@ -114,7 +109,7 @@ public function __construct() if ($form->isSubmitted()) { if (is_bool($form->isSubmitted()) || $form->isSubmitted()->isDisabled()) { - $form->setSubmittedBy(null); + throw new Exception('The form must be submitted using the specific submit button.'); } elseif ($form->isSubmitted()->getValidationScope() !== null) { $form->onValidate = []; @@ -131,7 +126,7 @@ final public function validateFormCallback(Form $form): void $this->onBeforeValidateForm($form); if ($form->isValid() && method_exists($this, 'validateForm')) { - $this->invokeHandler([$this, 'validateForm'], $form->getUnsafeValues(null)); + $this->invokeHandler([$this, 'validateForm'], $form->getUntrustedValues()); } } @@ -203,31 +198,31 @@ protected function _() return call_user_func_array([$this->form->getTranslator(), 'translate'], func_get_args()); } - public function setOnBeforeInitForm(callable $onBeforeInitForm) + public function setOnBeforeInitForm(callable $onBeforeInitForm): static { $this->onBeforeInitForm[] = $onBeforeInitForm; return $this; } - public function setOnAfterInitForm(callable $onAfterInitForm) + public function setOnAfterInitForm(callable $onAfterInitForm): static { $this->onAfterInitForm[] = $onAfterInitForm; return $this; } - public function setOnBeforeValidateForm(callable $onBeforeValidateForm) + public function setOnBeforeValidateForm(callable $onBeforeValidateForm): static { $this->onBeforeValidateForm[] = $onBeforeValidateForm; return $this; } - public function setOnBeforeProcessForm(callable $onBeforeProcessForm) + public function setOnBeforeProcessForm(callable $onBeforeProcessForm): static { $this->onBeforeProcessForm[] = $onBeforeProcessForm; return $this; } - public function setOnSuccess(callable $onSuccess) + public function setOnSuccess(callable $onSuccess): static { $this->onSuccess[] = $onSuccess; return $this; @@ -237,9 +232,9 @@ public function setOnSuccess(callable $onSuccess) * @throws ReflectionException * @throws Exception */ - private function invokeHandler($handler, $formValues = null) + private function invokeHandler(callable $handler, object|array|null $formValues = null): void { - $types = array_map(function(\ReflectionParameter $param) { + $types = array_map(function(ReflectionParameter $param) { return Type::resolve($param->getType()->getName(), $param); }, Callback::toReflection($handler)->getParameters()); @@ -251,7 +246,7 @@ private function invokeHandler($handler, $formValues = null) $param = null; foreach ($this->paramResolvers as $_paramResolver) { - if (($param = $_paramResolver($_type, $formValues)) !== false) { + if (($param = $_paramResolver($_type, $formValues, $handler[1])) !== false) { $params[] = $param; break; } @@ -265,16 +260,7 @@ private function invokeHandler($handler, $formValues = null) $handler(...$params); } - /** - * @deprecated Use $this->form instead - * @return Form - */ - public function getForm() - { - return $this['form']; - } - - protected function processToggles(Form $form, bool $emptyValue) + protected function processToggles(Form $form, bool $emptyValue): void { if ($this->emptyHiddenToggleControls) { $toggles = $form->getToggles(); From 6cd30fd2e52eeef4a7f562f7565c19a8e60c77bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Fri, 9 May 2025 14:52:34 +0200 Subject: [PATCH 02/54] Update BaseForm.php --- src/BaseForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BaseForm.php b/src/BaseForm.php index 043d5dc..770660a 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -20,7 +20,7 @@ */ abstract class BaseForm extends Control { - private Form $form; + protected Form $form; public ?string $templateFilename = null; From 5e84dbc708e2399208ed1964b4a94eb1d75cb74c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Wed, 14 May 2025 22:28:55 +0200 Subject: [PATCH 03/54] Refactors form component creation Simplifies form creation by removing the explicit instantiation of the form component. Additionally, it ensures hidden toggle controls are initially empty for better UX. --- src/BaseForm.php | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/BaseForm.php b/src/BaseForm.php index 770660a..ba1aa3e 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -21,12 +21,9 @@ abstract class BaseForm extends Control { protected Form $form; - - public ?string $templateFilename = null; - - public bool $isAjax = true; - - public bool $emptyHiddenToggleControls = false; + protected ?string $templateFilename = null; + protected bool $isAjax = true; + protected bool $emptyHiddenToggleControls = true; /** @var callable[] */ protected array $paramResolvers = []; @@ -80,7 +77,7 @@ public function __construct() }; $this->monitor(Presenter::class, function() { - $form = $this->form = $this['form'] = $this->createComponentForm(); + $form = $this->form = $this['form']; /** @link BaseForm::validateFormCallback() */ $form->onValidate[] = [$this, 'validateFormCallback']; @@ -188,7 +185,7 @@ public function render(): void $this->template->render(); } - protected function createComponentForm(): Form + protected function createComponentForm() { return new Form(); } From bff731fde54eb5e37dae939684c38501d6b4eec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Thu, 15 May 2025 12:29:48 +0200 Subject: [PATCH 04/54] Simplifies param resolvers in BaseForm Removes unused `$handler` parameter from the param resolver callback, simplifying the resolver's signature and reducing unnecessary information being passed to it. --- src/BaseForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BaseForm.php b/src/BaseForm.php index ba1aa3e..dea8610 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -243,7 +243,7 @@ private function invokeHandler(callable $handler, object|array|null $formValues $param = null; foreach ($this->paramResolvers as $_paramResolver) { - if (($param = $_paramResolver($_type, $formValues, $handler[1])) !== false) { + if (($param = $_paramResolver($_type, $formValues)) !== false) { $params[] = $param; break; } From 8693c9f0af7aaf97009a61b4701c111ac4a533fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Sun, 25 May 2025 14:22:57 +0200 Subject: [PATCH 05/54] Improves template path handling Refactors template path determination to use a getter method. This change replaces direct access to the `$templateFilename` property with a call to `getTemplateFilename()`. This provides more flexibility for overriding the template path determination logic in subclasses, and allows for a consistent way to retrieve the template path. --- src/BaseForm.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/BaseForm.php b/src/BaseForm.php index dea8610..4838f81 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -21,7 +21,6 @@ abstract class BaseForm extends Control { protected Form $form; - protected ?string $templateFilename = null; protected bool $isAjax = true; protected bool $emptyHiddenToggleControls = true; @@ -161,8 +160,8 @@ public function render(): void $this->template->setFile(__DIR__ . DIRECTORY_SEPARATOR . 'form.latte'); $customTemplatePath = ( - (!empty($this->templateFilename)) - ? $this->templateFilename + (!empty($this->getTemplateFilename())) + ? $this->getTemplateFilename() : str_replace('.php', '.latte', $this->getReflection()->getFileName()) ); @@ -277,4 +276,9 @@ protected function processToggles(Form $form, bool $emptyValue): void } } } + + protected function getTemplateFilename(): ?string + { + return null; + } } From 911423507027d65ffdec1debdf899612fb75d61a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Wed, 4 Jun 2025 08:54:10 +0200 Subject: [PATCH 06/54] Changes method visibility to protected Updates the visibility of a method from private to protected. This adjustment enables derived classes to override or extend the functionality of this method, promoting code reusability and extensibility. --- src/BaseForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BaseForm.php b/src/BaseForm.php index 4838f81..ff4c954 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -228,7 +228,7 @@ public function setOnSuccess(callable $onSuccess): static * @throws ReflectionException * @throws Exception */ - private function invokeHandler(callable $handler, object|array|null $formValues = null): void + protected function invokeHandler(callable $handler, object|array|null $formValues = null): void { $types = array_map(function(ReflectionParameter $param) { return Type::resolve($param->getType()->getName(), $param); From fd82dc2794eea22040e8374ea0278da27867df72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Wed, 4 Jun 2025 11:09:02 +0200 Subject: [PATCH 07/54] Updates return value for handler invocation Changes the `invokeHandler` method to return the value of the handler. This allows for greater flexibility in how the handler is used and enables the caller to access the result of the handler execution. --- src/BaseForm.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BaseForm.php b/src/BaseForm.php index ff4c954..f714a0f 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -228,7 +228,7 @@ public function setOnSuccess(callable $onSuccess): static * @throws ReflectionException * @throws Exception */ - protected function invokeHandler(callable $handler, object|array|null $formValues = null): void + protected function invokeHandler(callable $handler, object|array|null $formValues = null) { $types = array_map(function(ReflectionParameter $param) { return Type::resolve($param->getType()->getName(), $param); @@ -253,7 +253,7 @@ protected function invokeHandler(callable $handler, object|array|null $formValue } } - $handler(...$params); + return $handler(...$params); } protected function processToggles(Form $form, bool $emptyValue): void From 2297330609786514aa72957541597f5eb54ef11c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Thu, 10 Jul 2025 22:00:50 +0200 Subject: [PATCH 08/54] Ensures array typehint returns array Converts the `ArrayHash` object to a standard array when the return typehint is an array. This prevents potential type errors and ensures consistency when the user expects a native PHP array. --- src/BaseForm.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/BaseForm.php b/src/BaseForm.php index f714a0f..c8eff89 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -7,6 +7,7 @@ use Nette\Application\UI\Presenter; use Nette\Utils\ArrayHash; use Nette\Utils\Callback; +use Nette\Utils\Json; use Nette\Utils\Type; use ReflectionException; use ReflectionParameter; @@ -68,7 +69,7 @@ public function __construct() if ($type === ArrayHash::class) { return $values; } elseif ($type === 'array') { - return (array) $values; + return Json::decode(Json::encode($values), forceArrays: true); } } From 2ad400c7c41c999b974cf7c67f69a8cd8d7816b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Fri, 11 Jul 2025 12:14:28 +0200 Subject: [PATCH 09/54] Converts ArrayHash to array recursively Replaces Json encoding/decoding with a custom recursive conversion to handle ArrayHash to array conversion. This approach avoids potential issues with JSON encoding and decoding, especially with complex data structures. --- src/BaseForm.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/BaseForm.php b/src/BaseForm.php index c8eff89..a582a83 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -7,7 +7,6 @@ use Nette\Application\UI\Presenter; use Nette\Utils\ArrayHash; use Nette\Utils\Callback; -use Nette\Utils\Json; use Nette\Utils\Type; use ReflectionException; use ReflectionParameter; @@ -69,7 +68,7 @@ public function __construct() if ($type === ArrayHash::class) { return $values; } elseif ($type === 'array') { - return Json::decode(Json::encode($values), forceArrays: true); + return $this->convertArrayHashToArray($values); } } @@ -282,4 +281,19 @@ protected function getTemplateFilename(): ?string { return null; } + + protected function convertArrayHashToArray($data) + { + if ($data instanceof ArrayHash) { + $data = (array) $data; + } + + if (is_array($data)) { + foreach ($data as $key => $value) { + $data[$key] = $this->convertArrayHashToArray($value); + } + } + + return $data; + } } From 3e5bb0bd77e7cb42d4b75b6d03f3196f44bdd3f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Sat, 12 Jul 2025 13:09:36 +0200 Subject: [PATCH 10/54] Avoids validation issues during AJAX select processing Adds a check to prevent validation errors when processing AJAX select inputs, specifically addressing an issue where only partially rendered form values were available during validation. This resolves a problem encountered with Sobit pokladna, where `$form->getUntrustedValues()` returned values only from already rendered inputs, leading to incomplete data during validation when AJAX selects were involved. --- src/BaseForm.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/BaseForm.php b/src/BaseForm.php index a582a83..142b7cf 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -119,6 +119,16 @@ public function __construct() */ final public function validateFormCallback(Form $form): void { + // pridal jsem kvuli sobit pokladne, kde jsme meli ajax select a kde nam to na + // $validItems = $this->getAjaxEntity()->formatValues($this->getAjaxEntity()->hydrateValues($validValues, $this->getForm()->getValues('array'))); + // hazelo + // Nette\Forms\Container::getValues() invoked but the form is not valid (form 'form') + // primarni problem je ten, ze $form->getUntrustedValues() vraci hodnoty jen z inputu + // ktere uz jsou vykreslene a tudiz tam jde treba jen pulka hodnot + if ($form->isSubmitted()->getValidationScope() !== null) { + return; + } + $this->onBeforeValidateForm($form); if ($form->isValid() && method_exists($this, 'validateForm')) { From e91ea4af0db06292013a3d09d0ed2986730ab6ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Sat, 12 Jul 2025 15:23:35 +0200 Subject: [PATCH 11/54] Fixes form submission check The original check for form submission was failing because submit buttons were not always defined before ajax select elements. This commit adds a check to ensure that the form submission is not an instance of a submitter control before validating the submission scope, ensuring all values are correctly processed. --- src/BaseForm.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/BaseForm.php b/src/BaseForm.php index 142b7cf..623cc44 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -5,6 +5,7 @@ use Exception; use Nette\Application\UI\Control; use Nette\Application\UI\Presenter; +use Nette\Forms\SubmitterControl; use Nette\Utils\ArrayHash; use Nette\Utils\Callback; use Nette\Utils\Type; @@ -125,7 +126,9 @@ final public function validateFormCallback(Form $form): void // Nette\Forms\Container::getValues() invoked but the form is not valid (form 'form') // primarni problem je ten, ze $form->getUntrustedValues() vraci hodnoty jen z inputu // ktere uz jsou vykreslene a tudiz tam jde treba jen pulka hodnot - if ($form->isSubmitted()->getValidationScope() !== null) { + // aby nemusely byt submit buttony definovane pred tim ajax selectem, tak jeste pridavame + // !$form->isSubmitted() instanceof SubmitterControl::class + if (!$form->isSubmitted() instanceof SubmitterControl || $form->isSubmitted()->getValidationScope() !== null) { return; } From 821551451e70a9709c808b6d23d34cc3da3751a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Sat, 9 Aug 2025 18:25:48 +0200 Subject: [PATCH 12/54] Update BaseForm.php --- src/BaseForm.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/BaseForm.php b/src/BaseForm.php index 623cc44..b335189 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -281,6 +281,9 @@ protected function processToggles(Form $form, bool $emptyValue): void foreach ($_group->getControls() as $_control) { $_control->setOption('hidden', true); if ($emptyValue) { + if (method_exists($_control, 'setNullable')) { + $_control->setNullable(true); + } $_control->setValue(null); } } From 94b62ad4fc8af5e9f3aeeb1f8041e51d618058c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Thu, 28 Aug 2025 16:17:07 +0200 Subject: [PATCH 13/54] Fixes snippet rendering order Ensures correct rendering order of snippet and tag by swapping their positions. This resolves a potential issue where the snippet might not be properly initialized before the tag is rendered, leading to unexpected behavior. --- src/form.latte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/form.latte b/src/form.latte index 597e2fd..39d4e42 100644 --- a/src/form.latte +++ b/src/form.latte @@ -39,7 +39,7 @@ {if str_starts_with((string) $group->getOption('label'), 'snippet-')} {var $snippetName = explode('-', $group->getOption('label'))} {var $snippetName = end($snippetName)} -
+
{include renderContainer container => $group}
{else} From fdd7254672e567edc8730a08d64e0e262add7c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Sat, 13 Sep 2025 09:55:34 +0200 Subject: [PATCH 14/54] add error to form instead throwing an exception --- src/BaseForm.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BaseForm.php b/src/BaseForm.php index b335189..726b78b 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -106,7 +106,7 @@ public function __construct() if ($form->isSubmitted()) { if (is_bool($form->isSubmitted()) || $form->isSubmitted()->isDisabled()) { - throw new Exception('The form must be submitted using the specific submit button.'); + $form->addError('The form must be submitted using the specific submit button.'); } elseif ($form->isSubmitted()->getValidationScope() !== null) { $form->onValidate = []; @@ -312,4 +312,4 @@ protected function convertArrayHashToArray($data) return $data; } -} +} \ No newline at end of file From 90c83d2ee544b659320dc6131a03302cc8d094f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Sun, 19 Oct 2025 12:12:52 +0200 Subject: [PATCH 15/54] Improves form rendering and functionality Updates the form component to enhance rendering flexibility and adds support for dynamic and static containers with groups. - Migrates to a new Latte template for improved form structure and rendering. - Adds support for nested groups within forms, enabling more complex layouts. - Introduces BlockName interface to allow customizing block names. - Adds functionality to handle form sections and their redrawing on change. - Bumps the minimum PHP version to 8.3. --- composer.json | 2 +- src/BaseContainer.php | 16 ++- src/BaseForm.latte | 80 +++++++++++++++ src/BaseForm.php | 28 +++-- src/BlockName.php | 8 ++ src/Form.php | 220 +++++++++++++++++++++++++++++++++++++++- src/StaticContainer.php | 9 +- src/form.latte | 72 ------------- 8 files changed, 337 insertions(+), 98 deletions(-) create mode 100644 src/BaseForm.latte create mode 100644 src/BlockName.php delete mode 100644 src/form.latte diff --git a/composer.json b/composer.json index 8f44086..9c80509 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "type": "library", "license": ["MIT"], "require": { - "php": ">=8.1", + "php": ">=8.3", "nette/application": "^3.0", "nette/forms": "^3.2.3", "nette/utils": "^3.0 | ^4.0" diff --git a/src/BaseContainer.php b/src/BaseContainer.php index 3cbeb66..ff4caff 100644 --- a/src/BaseContainer.php +++ b/src/BaseContainer.php @@ -13,7 +13,7 @@ abstract class BaseContainer extends Container // we have to create an IControl instance and call "addError" on it // the control must not be an instance of "HiddenField" // otherwise the error will be added to the form instead of the container - const ERROR_CONTROL_NAME = '_containerError_'; + const string ERROR_CONTROL_NAME = '_containerError_'; private array $options = []; @@ -24,7 +24,7 @@ abstract class BaseContainer extends Container * @param string|null $message * @return static */ - public function setRequired(?string $message) + public function setRequired(?string $message): static { $this->requiredMessage = $message; return $this; @@ -43,7 +43,7 @@ protected function isRequired(): bool } - public function setOption($key, $value) + public function setOption($key, $value): static { if ($value === null) { unset($this->options[$key]); @@ -76,15 +76,21 @@ public function addError($message, bool $translate = true): void public static function register(): void { Container::extensionMethod('addStaticContainer', function (Container $_this, string $name, Closure $factory, ?string $isFilledComponentName = null, ?string $isRequiredMessage = null) { - return $_this[$name] = (new StaticContainerFactory($name, $factory, $isFilledComponentName)) + $control = (new StaticContainerFactory($name, $factory, $isFilledComponentName)) ->create() ->setRequired($isRequiredMessage); + $control->currentGroup = $_this->currentGroup; + $_this->currentGroup?->add($control); + return $_this[$name] = $control; }); Container::extensionMethod('addDynamicContainer', function (Container $_this, string $name, Closure $factory, ?string $isFilledComponentName = null, ?string $isRequiredMessage = null) { - return $_this[$name] = (new DynamicContainer) + $control = (new DynamicContainer) ->setStaticContainerFactory(new StaticContainerFactory($name, $factory, $isFilledComponentName)) ->setRequired($isRequiredMessage); + $control->currentGroup = $_this->currentGroup; + $_this->currentGroup?->add($control); + return $_this[$name] = $control; }); } } diff --git a/src/BaseForm.latte b/src/BaseForm.latte new file mode 100644 index 0000000..8f966c7 --- /dev/null +++ b/src/BaseForm.latte @@ -0,0 +1,80 @@ +{varType ADT\Forms\Form $form} + +{define errors} + {$control['form']->getRenderer()->renderErrors()|noescape} +{/define} + +{define renderEl} + {if $el['type'] === 'section'} +
+ {if $el['section']->getOption('redrawOnChange')} +
+ {include renderSection, el => $el} +
+ {else} +
+ {include renderSection, el => $el} +
+ {/if} +
+ + {if $el['section']->getOption('redrawOnChange')} + {var $fields = $el['section']->getOption('redrawOnChange')} + {var $jsEl = implode(',', array_map(fn($field) => "[name=\"$field\"]", $fields))} + {var $elName = '#' . $el['name']} + {var $redrawHandler = '[name="' . $el['section']->getOption('redrawHandler') . '"]'} + + {/if} + {else} + {var $c = $el['component']} + + {var $blockName = 'component-' . $c->getName()} + {ifset #$blockName} + {include #$blockName, item => $c} + {else} + {ifset $el['children']} + {foreach $el['children'] as $_el} + {include renderEl, el => $_el} + {/foreach} + {else} + {if $c instanceof \Nette\Forms\Controls\SubmitButton && !$c->getCaption()} + {input $c hidden => true} + {else} + {formPair $c} + {/if} + {/ifset} + {/ifset} + {/if} +{/define} + +{define renderSection} + {var $blockName = 'section-' . $el['name']} + {ifset #$blockName} + {include #$blockName, item => $el} + {elseif $el['section']->getOption('blockName')} + {var $blockName = $el['section']->getOption('blockName')} + {include #$blockName, item => $el} + {else} + {foreach $el['children'] as $_el} + {include renderEl, el => $_el} + {/foreach} + {/ifset} +{/define} + +{snippetArea formArea} + {block form} + {form form} + {include errors} + + {foreach $form->build() as $_el} + {include renderEl el => $_el} + {/foreach} + {/form} + {/block} +{/snippetArea} diff --git a/src/BaseForm.php b/src/BaseForm.php index 726b78b..7ea6264 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -21,6 +21,8 @@ */ abstract class BaseForm extends Control { + const string ORIGINAL_TEMPLATE = __DIR__ . DIRECTORY_SEPARATOR . 'BaseForm.latte'; + protected Form $form; protected bool $isAjax = true; protected bool $emptyHiddenToggleControls = true; @@ -170,17 +172,8 @@ final public function processFormCallback(Form $form): void */ public function render(): void { - $this->template->setFile(__DIR__ . DIRECTORY_SEPARATOR . 'form.latte'); - - $customTemplatePath = ( - (!empty($this->getTemplateFilename())) - ? $this->getTemplateFilename() - : str_replace('.php', '.latte', $this->getReflection()->getFileName()) - ); - - if (file_exists($customTemplatePath)) { - $this->template->customTemplatePath = $customTemplatePath; - } + $this->template->originalTemplate = $this->getOriginalTemplate(); + $this->template->setFile($this->getTemplateFile()); if ($this->isAjax) { $this->form->getElementPrototype()->class[] = 'ajax'; @@ -275,8 +268,8 @@ protected function processToggles(Form $form, bool $emptyValue): void $toggles = $form->getToggles(); foreach ($form->getGroups() as $_group) { $toggleName = ''; - foreach (explode('_', (string)$_group->getOption('label')) as $_togglePart) { - $toggleName = trim($toggleName . '_' . $_togglePart, '_'); + foreach (explode(Form::GROUP_LEVEL_SEPARATOR, (string)$_group->getOption('label')) as $_togglePart) { + $toggleName = trim($toggleName . Form::GROUP_LEVEL_SEPARATOR . $_togglePart, Form::GROUP_LEVEL_SEPARATOR); if (isset($toggles[$toggleName]) && $toggles[$toggleName] === false) { foreach ($_group->getControls() as $_control) { $_control->setOption('hidden', true); @@ -293,9 +286,9 @@ protected function processToggles(Form $form, bool $emptyValue): void } } - protected function getTemplateFilename(): ?string + protected function getTemplateFile(): string { - return null; + return self::ORIGINAL_TEMPLATE; } protected function convertArrayHashToArray($data) @@ -312,4 +305,9 @@ protected function convertArrayHashToArray($data) return $data; } + + protected function getOriginalTemplate(): string + { + return self::ORIGINAL_TEMPLATE; + } } \ No newline at end of file diff --git a/src/BlockName.php b/src/BlockName.php new file mode 100644 index 0000000..2c5cc80 --- /dev/null +++ b/src/BlockName.php @@ -0,0 +1,8 @@ +processGroups($this, $this->buildComponentGroupMap()); + } + + /** + * Vytvoří mapu, která přiřazuje komponenty k jejich groupám + */ + private function buildComponentGroupMap(): array + { + $map = []; + + foreach ($this->getGroups() as $group) { + foreach ($group->getControls() as $control) { + $map[spl_object_id($control)] = $group; + } + } + + return $map; + } + + /** + * Zpracuje všechny groupy hierarchicky podle '__' + */ + private function processGroups($form, array $componentToGroup): array + { + $allGroups = []; + $groupFirstComponent = []; // Sleduje první komponentu každé groupy + + // Sesbíráme všechny groupy a jejich komponenty + foreach ($form->getGroups() as $group) { + $groupName = $group->getOption('label') ?? ''; + $items = []; + + foreach ($group->getControls() as $control) { + // Přeskočíme hidden fieldy + if ($control instanceof Nette\Forms\Controls\HiddenField) { + continue; + } + + // Zaznamenáme první komponentu groupy pro určení pořadí + if (!isset($groupFirstComponent[$groupName])) { + $groupFirstComponent[$groupName] = $control; + } + + $items[] = [ + 'name' => $control->getName(), + 'type' => 'input', + 'component' => $control + ]; + } + + if (!empty($items)) { + $allGroups[$groupName] = [ + 'name' => $groupName, + 'type' => 'section', + 'section' => $group, + 'children' => $items + ]; + } + } + + // Vytvoříme hierarchii - vnořené groupy přidáme do parent groups + foreach ($allGroups as $groupName => $groupData) { + $parentName = $this->getParentGroupName($groupName); + + if ($parentName !== null && isset($allGroups[$parentName])) { + // Přidáme tuto groupu do parent groupy + $allGroups[$parentName]['children'][] = &$allGroups[$groupName]; + } + } + + // Nyní projdeme všechny komponenty formuláře v původním pořadí + // a sestavíme výsledek + $result = []; + $processedGroups = []; + + foreach ($form->getComponents() as $component) { + // Přeskočíme hidden fieldy + if ($component instanceof Nette\Forms\Controls\HiddenField) { + continue; + } + + $group = $componentToGroup[spl_object_id($component)] ?? null; + + if ($group !== null) { + $groupName = $group->getOption('label') ?? ''; + + // Pokud je to root level group a ještě jsme ji nezpracovali + if (!str_contains($groupName, static::GROUP_LEVEL_SEPARATOR) && !isset($processedGroups[$groupName])) { + if (isset($allGroups[$groupName])) { + $result[] = $allGroups[$groupName]; + $processedGroups[$groupName] = true; + } + } + } else { + // Komponenta bez groupy + if ($component instanceof Container) { + $children = $this->collectUngroupedComponents($component, $componentToGroup); + $result[] = [ + 'name' => $component->getName(), + 'type' => 'container', + 'component' => $component, + 'children' => $children + ]; + } else { + $result[] = [ + 'name' => $component->getName(), + 'type' => 'input', + 'component' => $component + ]; + } + } + } + + return $result; + } + + /** + * Sesbírá všechny komponenty bez groupy + */ + private function collectUngroupedComponents($container, array $componentToGroup): array + { + $result = []; + + foreach ($container->getComponents() as $component) { + // Přeskočíme hidden fieldy + if ($component instanceof Nette\Forms\Controls\HiddenField) { + continue; + } + + $group = $componentToGroup[spl_object_id($component)] ?? null; + + if ($group === null) { + if ($component instanceof Container) { + $children = $this->collectUngroupedComponents($component, $componentToGroup); + $result[] = [ + 'name' => $component->getName(), + 'type' => 'container', + 'component' => $component, + 'children' => $children + ]; + } else { + $result[] = [ + 'name' => $component->getName(), + 'type' => 'input', + 'component' => $component + ]; + } + } + } + + return $result; + } + + /** + * Zjistí parent group name z group name + */ + private function getParentGroupName(string $groupName): ?string + { + $pos = strrpos($groupName, static::GROUP_LEVEL_SEPARATOR); + if ($pos === false) { + return null; + } + return substr($groupName, 0, $pos); + } + + public function addGroup(Stringable|string|null $caption = null, bool $setAsCurrent = true): ControlGroup + { + $this->nestedGroups[] = parent::addGroup($caption, $setAsCurrent); + return end($this->nestedGroups); + } + + /** + * @throws Exception + */ + public function addSection(?callable $factory = null, ?string $name = null, ?BlockName $blockName = null, array $redrawOnChange = [], ?callable $onRedraw = null): ControlGroup + { + if ($redrawOnChange && (!$name || !$onRedraw)) { + throw new Exception('Name and onRedraw are required when redrawOnChange is set.'); + } + + if ($this->getCurrentGroup() !== null) { + $name = $this->getCurrentGroup()->getOption('label') . static::GROUP_LEVEL_SEPARATOR . $name; + } + $group = $this->addGroup($name); + $group->setOption('blockName', $blockName?->getName()); + $group->setOption('redrawOnChange', $redrawOnChange); + $factory && $factory(); + array_pop($this->nestedGroups); + $this->setCurrentGroup($this->nestedGroups ? end($this->nestedGroups) : null); + + if ($redrawOnChange) { + $redrawHandler = 'redraw' . ucfirst($name); + $group->setOption('redrawHandler', $redrawHandler); + $this->addSubmit($redrawHandler) + ->setValidationScope([]) + ->onClick[] = function() use ($onRedraw) { + $onRedraw(); + $this->getParent()->redrawControl($this->getSectionName('price')); + }; + } + + return $group; + } + + public function getSectionName(string ...$path): string + { + return implode(static::GROUP_LEVEL_SEPARATOR, $path); + } } diff --git a/src/StaticContainer.php b/src/StaticContainer.php index 2203256..aefafe0 100644 --- a/src/StaticContainer.php +++ b/src/StaticContainer.php @@ -5,6 +5,7 @@ use Closure; use Nette\Application\UI\Presenter; use Nette\Forms\Controls\BaseControl; +use Nette\Http\FileUpload; class StaticContainer extends BaseContainer { @@ -53,16 +54,16 @@ public function setIsFilledComponent(BaseControl $isFilledComponent): self } - public function isEmpty($excludeIsFilledComponent = false): bool + public function isEmpty(bool $excludeIsFilledComponent = false): bool { - // we don't want to validate the controls, just check if they are empty or not + // we don't want to validate the controls, just check if they are empty, or not // getValues causes a loop - $values = $this->getUnsafeValues('array'); + $values = $this->getUntrustedValues('array'); if ($excludeIsFilledComponent) { unset($values[$this->getIsFilledComponent()->getName()]); } foreach ($values as &$_value) { - if ($_value instanceof Nette\Http\FileUpload && !$_value->isOk()) { + if ($_value instanceof FileUpload && !$_value->isOk()) { $_value = null; } } diff --git a/src/form.latte b/src/form.latte deleted file mode 100644 index 39d4e42..0000000 --- a/src/form.latte +++ /dev/null @@ -1,72 +0,0 @@ -{define errors} - {$control['form']->getRenderer()->renderErrors()|noescape} -{/define} - -{define renderContainer} - {if $container instanceof Nette\Forms\Container} - {var $form = $container->getForm()} - {/if} - {if $container instanceof Nette\Forms\Form && $container->getGroups()} - {foreach $container->getGroups() as $_group} - {continueIf !$_group->getOption('visual')} - - {include renderGroup form => $container, group => $_group} - {/foreach} - {else} - {foreach $container->getControls() as $c} - {continueIf $c instanceof \Nette\Forms\Controls\HiddenField} - {continueIf $c->getOption('rendered')} - - {if $c instanceof \Nette\Forms\Container} - {include renderContainer, container => $c} - {else} - {if $c instanceof \Nette\Forms\Controls\SubmitButton && !$c->getCaption()} - {input $c hidden => true} - {else} - {formPair $c} - {/if} - {/if} - {/foreach} - {/if} -{/define} - -{define renderGroup} - {if is_string($group)} - {var $group = $form->getGroup($group)} - {/if} - - {var $groupName = (string) $group->getOption('label')} - {if str_starts_with((string) $group->getOption('label'), 'snippet-')} - {var $snippetName = explode('-', $group->getOption('label'))} - {var $snippetName = end($snippetName)} -
- {include renderContainer container => $group} -
- {else} - -
- {include renderContainer container => $group} - - {foreach $form->getGroups() as $_group} - {var $_groupName = (string) $_group->getOption('label')} - {if str_starts_with($_groupName, $groupName . '_')} - {include renderGroup form => $container, group => $_group} - {/if} - {/foreach} -
- {/if} - - {php $group->setOption('visual', false)} -{/define} - -{snippetArea formArea} - {ifset $customTemplatePath} - {include $customTemplatePath with blocks} - {else} - {form form} - {include errors} - - {include renderContainer container => $form} - {/form} - {/ifset} -{/snippetArea} From 0d0789387decbc34c058e278385b20ff95f1275b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Sun, 19 Oct 2025 22:08:56 +0200 Subject: [PATCH 16/54] Refactors form rendering for better customization Moves form rendering logic to a separate template and allows defining custom form templates. This change provides more flexibility in customizing the appearance of forms. --- src/BaseForm.latte | 28 +++++++++++++++++++--------- src/BaseForm.php | 17 +++++++---------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/BaseForm.latte b/src/BaseForm.latte index 8f966c7..1967ad9 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -1,3 +1,7 @@ +{if $templateFile} + {import $templateFile} +{/if} + {varType ADT\Forms\Form $form} {define errors} @@ -67,14 +71,20 @@ {/ifset} {/define} -{snippetArea formArea} - {block form} - {form form} - {include errors} +{define renderForm} + {form form} + {include errors} + + {foreach $form->build() as $_el} + {include renderEl el => $_el} + {/foreach} + {/form} +{/define} - {foreach $form->build() as $_el} - {include renderEl el => $_el} - {/foreach} - {/form} - {/block} +{snippetArea formArea} + {ifset #form} + {include #form} + {else} + {include renderForm} + {/ifset} {/snippetArea} diff --git a/src/BaseForm.php b/src/BaseForm.php index 7ea6264..5a87172 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -21,8 +21,6 @@ */ abstract class BaseForm extends Control { - const string ORIGINAL_TEMPLATE = __DIR__ . DIRECTORY_SEPARATOR . 'BaseForm.latte'; - protected Form $form; protected bool $isAjax = true; protected bool $emptyHiddenToggleControls = true; @@ -172,9 +170,6 @@ final public function processFormCallback(Form $form): void */ public function render(): void { - $this->template->originalTemplate = $this->getOriginalTemplate(); - $this->template->setFile($this->getTemplateFile()); - if ($this->isAjax) { $this->form->getElementPrototype()->class[] = 'ajax'; } @@ -187,7 +182,9 @@ public function render(): void $this->invokeHandler([$this, 'renderForm']); } - $this->template->render(); + $this->getTemplate()->templateFile = $this->getTemplateFile(); + $this->getTemplate()->setFile(static::getDefaultTemplateFile()); + $this->getTemplate()->render(); } protected function createComponentForm() @@ -286,9 +283,9 @@ protected function processToggles(Form $form, bool $emptyValue): void } } - protected function getTemplateFile(): string + protected function getTemplateFile(): ?string { - return self::ORIGINAL_TEMPLATE; + return null; } protected function convertArrayHashToArray($data) @@ -306,8 +303,8 @@ protected function convertArrayHashToArray($data) return $data; } - protected function getOriginalTemplate(): string + public static function getDefaultTemplateFile(): string { - return self::ORIGINAL_TEMPLATE; + return __DIR__ . '/BaseForm.latte'; } } \ No newline at end of file From eddd3cc8b28392c1fc9ddc25bfa45f79f15af662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Mon, 20 Oct 2025 10:51:52 +0200 Subject: [PATCH 17/54] Handles nested form containers in groups Improves the processing of form groups to correctly handle nested containers. It ensures that container components within groups are processed only once, preventing duplicates. It also handles nested containers within ungrouped components. This change ensures correct component structure representation when form contains nested containers in groups. --- src/Form.php | 95 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/src/Form.php b/src/Form.php index 2906761..e001651 100644 --- a/src/Form.php +++ b/src/Form.php @@ -76,12 +76,13 @@ private function buildComponentGroupMap(): array private function processGroups($form, array $componentToGroup): array { $allGroups = []; - $groupFirstComponent = []; // Sleduje první komponentu každé groupy + $processedContainersGlobal = []; // Globální sledování zpracovaných kontejnerů // Sesbíráme všechny groupy a jejich komponenty foreach ($form->getGroups() as $group) { $groupName = $group->getOption('label') ?? ''; $items = []; + $processedContainers = []; // Sledujeme již zpracované kontejnery v této groupě foreach ($group->getControls() as $control) { // Přeskočíme hidden fieldy @@ -89,16 +90,36 @@ private function processGroups($form, array $componentToGroup): array continue; } - // Zaznamenáme první komponentu groupy pro určení pořadí - if (!isset($groupFirstComponent[$groupName])) { - $groupFirstComponent[$groupName] = $control; - } + $parent = $control->getParent(); - $items[] = [ - 'name' => $control->getName(), - 'type' => 'input', - 'component' => $control - ]; + // Pokud je komponenta přímo ve formuláři + if ($parent === $form) { + $items[] = [ + 'name' => $control->getName(), + 'type' => 'input', + 'component' => $control + ]; + } else { + // Komponenta je v nějakém kontejneru - najdeme top-level kontejner + $topContainer = $parent; + while ($topContainer->getParent() !== $form) { + $topContainer = $topContainer->getParent(); + } + + // Přidáme kontejner jen jednou + $containerId = spl_object_id($topContainer); + if (!isset($processedContainers[$containerId])) { + $children = $this->collectAllComponents($topContainer, $componentToGroup); + $items[] = [ + 'name' => $topContainer->getName(), + 'type' => 'container', + 'component' => $topContainer, + 'children' => $children + ]; + $processedContainers[$containerId] = true; + $processedContainersGlobal[$containerId] = true; // Označíme globálně + } + } } if (!empty($items)) { @@ -147,13 +168,17 @@ private function processGroups($form, array $componentToGroup): array } else { // Komponenta bez groupy if ($component instanceof Container) { - $children = $this->collectUngroupedComponents($component, $componentToGroup); - $result[] = [ - 'name' => $component->getName(), - 'type' => 'container', - 'component' => $component, - 'children' => $children - ]; + // Kontrola, zda jsme tento kontejner už nezpracovali v nějaké groupě + $containerId = spl_object_id($component); + if (!isset($processedContainersGlobal[$containerId])) { + $children = $this->collectAllComponents($component, $componentToGroup); + $result[] = [ + 'name' => $component->getName(), + 'type' => 'container', + 'component' => $component, + 'children' => $children + ]; + } } else { $result[] = [ 'name' => $component->getName(), @@ -168,9 +193,9 @@ private function processGroups($form, array $componentToGroup): array } /** - * Sesbírá všechny komponenty bez groupy + * Sesbírá VŠECHNY komponenty z kontejneru (rekurzivně) */ - private function collectUngroupedComponents($container, array $componentToGroup): array + private function collectAllComponents($container, array $componentToGroup): array { $result = []; @@ -180,24 +205,20 @@ private function collectUngroupedComponents($container, array $componentToGroup) continue; } - $group = $componentToGroup[spl_object_id($component)] ?? null; - - if ($group === null) { - if ($component instanceof Container) { - $children = $this->collectUngroupedComponents($component, $componentToGroup); - $result[] = [ - 'name' => $component->getName(), - 'type' => 'container', - 'component' => $component, - 'children' => $children - ]; - } else { - $result[] = [ - 'name' => $component->getName(), - 'type' => 'input', - 'component' => $component - ]; - } + if ($component instanceof Container) { + $children = $this->collectAllComponents($component, $componentToGroup); + $result[] = [ + 'name' => $component->getName(), + 'type' => 'container', + 'component' => $component, + 'children' => $children + ]; + } else { + $result[] = [ + 'name' => $component->getName(), + 'type' => 'input', + 'component' => $component + ]; } } From db9827fcb67c4952aaf9fb1a76d6bab46976483d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Mon, 20 Oct 2025 11:00:56 +0200 Subject: [PATCH 18/54] Refactors Latte template for form rendering Improves template readability and consistency by renaming variables and adjusting include parameters in the Latte templates. This change clarifies the purpose of the passed data and streamlines the rendering process for form elements and sections. --- src/BaseForm.latte | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/BaseForm.latte b/src/BaseForm.latte index 1967ad9..d1fb535 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -13,11 +13,11 @@
{if $el['section']->getOption('redrawOnChange')}
- {include renderSection, el => $el} + {include renderSection, section => $el}
{else}
- {include renderSection, el => $el} + {include renderSection, section => $el}
{/if}
@@ -36,21 +36,21 @@ {/if} {else} - {var $c = $el['component']} + {var $component = $el['component']} - {var $blockName = 'component-' . $c->getName()} + {var $blockName = 'component-' . $component->getName()} {ifset #$blockName} - {include #$blockName, item => $c} + {include #$blockName, component => $component} {else} {ifset $el['children']} {foreach $el['children'] as $_el} {include renderEl, el => $_el} {/foreach} {else} - {if $c instanceof \Nette\Forms\Controls\SubmitButton && !$c->getCaption()} - {input $c hidden => true} + {if $component instanceof \Nette\Forms\Controls\SubmitButton && !$component->getCaption()} + {input $component hidden => true} {else} - {formPair $c} + {formPair $component} {/if} {/ifset} {/ifset} @@ -58,14 +58,14 @@ {/define} {define renderSection} - {var $blockName = 'section-' . $el['name']} + {var $blockName = 'section-' . $section['name']} {ifset #$blockName} - {include #$blockName, item => $el} - {elseif $el['section']->getOption('blockName')} - {var $blockName = $el['section']->getOption('blockName')} - {include #$blockName, item => $el} + {include #$blockName, section => $section} + {elseif $section['section']->getOption('blockName')} + {var $blockName = $section['section']->getOption('blockName')} + {include #$blockName, section => $section} {else} - {foreach $el['children'] as $_el} + {foreach $section['children'] as $_el} {include renderEl, el => $_el} {/foreach} {/ifset} From 45408844a0db3788cbeec3bf69b5ed720f9de79b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Tue, 21 Oct 2025 08:15:05 +0200 Subject: [PATCH 19/54] Improves form section handling and group logic Refactors how form sections and control groups are handled, particularly when dealing with nested containers and redrawing. It ensures that control groups are correctly identified and processed, preventing duplicates and improving the logic for handling redraws based on user interactions. Also fixes an issue where the name was required even without onRedraw. The validation scope is now properly passed to the submit button. --- src/Form.php | 55 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/src/Form.php b/src/Form.php index e001651..110194c 100644 --- a/src/Form.php +++ b/src/Form.php @@ -170,7 +170,21 @@ private function processGroups($form, array $componentToGroup): array if ($component instanceof Container) { // Kontrola, zda jsme tento kontejner už nezpracovali v nějaké groupě $containerId = spl_object_id($component); - if (!isset($processedContainersGlobal[$containerId])) { + + // Zkontrolujeme, jestli děti tohoto kontejneru nemají nějakou groupu + $childGroup = $this->getContainerChildGroup($component, $componentToGroup); + + if ($childGroup !== null) { + // Děti mají groupu - zkontrolujeme, jestli už byla přidána + $childGroupName = $childGroup->getOption('label') ?? ''; + if (!str_contains($childGroupName, static::GROUP_LEVEL_SEPARATOR) && + !isset($processedGroups[$childGroupName]) && + isset($allGroups[$childGroupName])) { + $result[] = $allGroups[$childGroupName]; + $processedGroups[$childGroupName] = true; + } + } elseif (!isset($processedContainersGlobal[$containerId])) { + // Kontejner ani jeho děti nemají groupu - přidáme ho normálně $children = $this->collectAllComponents($component, $componentToGroup); $result[] = [ 'name' => $component->getName(), @@ -192,6 +206,29 @@ private function processGroups($form, array $componentToGroup): array return $result; } + /** + * Zjistí, jestli děti kontejneru mají nějakou groupu + */ + private function getContainerChildGroup($container, array $componentToGroup): ?ControlGroup + { + foreach ($container->getComponents() as $component) { + $group = $componentToGroup[spl_object_id($component)] ?? null; + if ($group !== null) { + return $group; + } + + // Rekurzivně zkontrolujeme vnořené kontejnery + if ($component instanceof Container) { + $childGroup = $this->getContainerChildGroup($component, $componentToGroup); + if ($childGroup !== null) { + return $childGroup; + } + } + } + + return null; + } + /** * Sesbírá VŠECHNY komponenty z kontejneru (rekurzivně) */ @@ -246,10 +283,10 @@ public function addGroup(Stringable|string|null $caption = null, bool $setAsCurr /** * @throws Exception */ - public function addSection(?callable $factory = null, ?string $name = null, ?BlockName $blockName = null, array $redrawOnChange = [], ?callable $onRedraw = null): ControlGroup + public function addSection(?callable $factory = null, ?string $name = null, ?BlockName $blockName = null, array $watchForRedraw = [], ?callable $onRedraw = null, array $validationScope = []): ControlGroup { - if ($redrawOnChange && (!$name || !$onRedraw)) { - throw new Exception('Name and onRedraw are required when redrawOnChange is set.'); + if ($onRedraw && !$name) { + throw new Exception('Name is required when onRedraw is set.'); } if ($this->getCurrentGroup() !== null) { @@ -257,19 +294,19 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo } $group = $this->addGroup($name); $group->setOption('blockName', $blockName?->getName()); - $group->setOption('redrawOnChange', $redrawOnChange); + $group->setOption('watchForRedraw', $watchForRedraw); $factory && $factory(); array_pop($this->nestedGroups); $this->setCurrentGroup($this->nestedGroups ? end($this->nestedGroups) : null); - if ($redrawOnChange) { + if ($onRedraw) { $redrawHandler = 'redraw' . ucfirst($name); $group->setOption('redrawHandler', $redrawHandler); $this->addSubmit($redrawHandler) - ->setValidationScope([]) - ->onClick[] = function() use ($onRedraw) { + ->setValidationScope($validationScope) + ->onClick[] = function() use ($onRedraw, $name) { $onRedraw(); - $this->getParent()->redrawControl($this->getSectionName('price')); + $this->getParent()->redrawControl($name); }; } From f01f5473a1b94188521fc6044ae077ddffed4209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Tue, 21 Oct 2025 22:27:55 +0200 Subject: [PATCH 20/54] Refactors form rendering and section handling Improves form structure by introducing a `SectionTrait` for managing form sections and their redrawing behavior. This centralizes section-related logic and simplifies the form rendering process. The changes also ensure correct redrawing of sections based on user interactions with specific form fields. --- src/BaseForm.latte | 50 +++--- src/Form.php | 273 +-------------------------------- src/SectionTrait.php | 329 ++++++++++++++++++++++++++++++++++++++++ src/StaticContainer.php | 1 + 4 files changed, 359 insertions(+), 294 deletions(-) create mode 100644 src/SectionTrait.php diff --git a/src/BaseForm.latte b/src/BaseForm.latte index d1fb535..2f5d837 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -11,30 +11,32 @@ {define renderEl} {if $el['type'] === 'section'}
- {if $el['section']->getOption('redrawOnChange')} -
+ {if $el['section']->getOption('redrawHandler')} +
{include renderSection, section => $el}
+ + {include renderInput input => $el['section']->getOption('redrawHandler')} + + {var $fields = $el['section']->getOption('watchForRedraw')} + {var $jsEl = implode(',', array_map(fn($field) => "[name=\"$field\"],[name$=\"[$field]\"]", $fields))} + {var $elName = '#' . $el['name']} + {var $redrawHandler = '[name="' . $el['section']->getOption('redrawHandler')->getName() . '"],[name$="[' . $el['section']->getOption('redrawHandler')->getName() . ']"]'} + {else}
{include renderSection, section => $el}
{/if}
- - {if $el['section']->getOption('redrawOnChange')} - {var $fields = $el['section']->getOption('redrawOnChange')} - {var $jsEl = implode(',', array_map(fn($field) => "[name=\"$field\"]", $fields))} - {var $elName = '#' . $el['name']} - {var $redrawHandler = '[name="' . $el['section']->getOption('redrawHandler') . '"]'} - - {/if} {else} {var $component = $el['component']} @@ -47,11 +49,7 @@ {include renderEl, el => $_el} {/foreach} {else} - {if $component instanceof \Nette\Forms\Controls\SubmitButton && !$component->getCaption()} - {input $component hidden => true} - {else} - {formPair $component} - {/if} + {include renderInput, input => $component} {/ifset} {/ifset} {/if} @@ -71,11 +69,19 @@ {/ifset} {/define} +{define renderInput} + {if $input instanceof \Nette\Forms\Controls\SubmitButton && !$input->getCaption()} + {input $input hidden => true} + {else} + {formPair $input} + {/if} +{/define} + {define renderForm} {form form} {include errors} - {foreach $form->build() as $_el} + {foreach $form->getStructure() as $_el} {include renderEl el => $_el} {/foreach} {/form} diff --git a/src/Form.php b/src/Form.php index 110194c..40b00b6 100644 --- a/src/Form.php +++ b/src/Form.php @@ -13,11 +13,9 @@ class Form extends Nette\Application\UI\Form { use AnnotationsTrait; use GetComponentTrait; - - const string GROUP_LEVEL_SEPARATOR = '__'; + use SectionTrait; private ?BootstrapFormRenderer $renderer = null; - private array $nestedGroups = []; public function __construct(?Nette\ComponentModel\IContainer $parent = null, ?string $name = null) { @@ -48,273 +46,4 @@ public function addError($message, bool $translate = true): void } parent::addError($message, false); } - - public function build(): array - { - return $this->processGroups($this, $this->buildComponentGroupMap()); - } - - /** - * Vytvoří mapu, která přiřazuje komponenty k jejich groupám - */ - private function buildComponentGroupMap(): array - { - $map = []; - - foreach ($this->getGroups() as $group) { - foreach ($group->getControls() as $control) { - $map[spl_object_id($control)] = $group; - } - } - - return $map; - } - - /** - * Zpracuje všechny groupy hierarchicky podle '__' - */ - private function processGroups($form, array $componentToGroup): array - { - $allGroups = []; - $processedContainersGlobal = []; // Globální sledování zpracovaných kontejnerů - - // Sesbíráme všechny groupy a jejich komponenty - foreach ($form->getGroups() as $group) { - $groupName = $group->getOption('label') ?? ''; - $items = []; - $processedContainers = []; // Sledujeme již zpracované kontejnery v této groupě - - foreach ($group->getControls() as $control) { - // Přeskočíme hidden fieldy - if ($control instanceof Nette\Forms\Controls\HiddenField) { - continue; - } - - $parent = $control->getParent(); - - // Pokud je komponenta přímo ve formuláři - if ($parent === $form) { - $items[] = [ - 'name' => $control->getName(), - 'type' => 'input', - 'component' => $control - ]; - } else { - // Komponenta je v nějakém kontejneru - najdeme top-level kontejner - $topContainer = $parent; - while ($topContainer->getParent() !== $form) { - $topContainer = $topContainer->getParent(); - } - - // Přidáme kontejner jen jednou - $containerId = spl_object_id($topContainer); - if (!isset($processedContainers[$containerId])) { - $children = $this->collectAllComponents($topContainer, $componentToGroup); - $items[] = [ - 'name' => $topContainer->getName(), - 'type' => 'container', - 'component' => $topContainer, - 'children' => $children - ]; - $processedContainers[$containerId] = true; - $processedContainersGlobal[$containerId] = true; // Označíme globálně - } - } - } - - if (!empty($items)) { - $allGroups[$groupName] = [ - 'name' => $groupName, - 'type' => 'section', - 'section' => $group, - 'children' => $items - ]; - } - } - - // Vytvoříme hierarchii - vnořené groupy přidáme do parent groups - foreach ($allGroups as $groupName => $groupData) { - $parentName = $this->getParentGroupName($groupName); - - if ($parentName !== null && isset($allGroups[$parentName])) { - // Přidáme tuto groupu do parent groupy - $allGroups[$parentName]['children'][] = &$allGroups[$groupName]; - } - } - - // Nyní projdeme všechny komponenty formuláře v původním pořadí - // a sestavíme výsledek - $result = []; - $processedGroups = []; - - foreach ($form->getComponents() as $component) { - // Přeskočíme hidden fieldy - if ($component instanceof Nette\Forms\Controls\HiddenField) { - continue; - } - - $group = $componentToGroup[spl_object_id($component)] ?? null; - - if ($group !== null) { - $groupName = $group->getOption('label') ?? ''; - - // Pokud je to root level group a ještě jsme ji nezpracovali - if (!str_contains($groupName, static::GROUP_LEVEL_SEPARATOR) && !isset($processedGroups[$groupName])) { - if (isset($allGroups[$groupName])) { - $result[] = $allGroups[$groupName]; - $processedGroups[$groupName] = true; - } - } - } else { - // Komponenta bez groupy - if ($component instanceof Container) { - // Kontrola, zda jsme tento kontejner už nezpracovali v nějaké groupě - $containerId = spl_object_id($component); - - // Zkontrolujeme, jestli děti tohoto kontejneru nemají nějakou groupu - $childGroup = $this->getContainerChildGroup($component, $componentToGroup); - - if ($childGroup !== null) { - // Děti mají groupu - zkontrolujeme, jestli už byla přidána - $childGroupName = $childGroup->getOption('label') ?? ''; - if (!str_contains($childGroupName, static::GROUP_LEVEL_SEPARATOR) && - !isset($processedGroups[$childGroupName]) && - isset($allGroups[$childGroupName])) { - $result[] = $allGroups[$childGroupName]; - $processedGroups[$childGroupName] = true; - } - } elseif (!isset($processedContainersGlobal[$containerId])) { - // Kontejner ani jeho děti nemají groupu - přidáme ho normálně - $children = $this->collectAllComponents($component, $componentToGroup); - $result[] = [ - 'name' => $component->getName(), - 'type' => 'container', - 'component' => $component, - 'children' => $children - ]; - } - } else { - $result[] = [ - 'name' => $component->getName(), - 'type' => 'input', - 'component' => $component - ]; - } - } - } - - return $result; - } - - /** - * Zjistí, jestli děti kontejneru mají nějakou groupu - */ - private function getContainerChildGroup($container, array $componentToGroup): ?ControlGroup - { - foreach ($container->getComponents() as $component) { - $group = $componentToGroup[spl_object_id($component)] ?? null; - if ($group !== null) { - return $group; - } - - // Rekurzivně zkontrolujeme vnořené kontejnery - if ($component instanceof Container) { - $childGroup = $this->getContainerChildGroup($component, $componentToGroup); - if ($childGroup !== null) { - return $childGroup; - } - } - } - - return null; - } - - /** - * Sesbírá VŠECHNY komponenty z kontejneru (rekurzivně) - */ - private function collectAllComponents($container, array $componentToGroup): array - { - $result = []; - - foreach ($container->getComponents() as $component) { - // Přeskočíme hidden fieldy - if ($component instanceof Nette\Forms\Controls\HiddenField) { - continue; - } - - if ($component instanceof Container) { - $children = $this->collectAllComponents($component, $componentToGroup); - $result[] = [ - 'name' => $component->getName(), - 'type' => 'container', - 'component' => $component, - 'children' => $children - ]; - } else { - $result[] = [ - 'name' => $component->getName(), - 'type' => 'input', - 'component' => $component - ]; - } - } - - return $result; - } - - /** - * Zjistí parent group name z group name - */ - private function getParentGroupName(string $groupName): ?string - { - $pos = strrpos($groupName, static::GROUP_LEVEL_SEPARATOR); - if ($pos === false) { - return null; - } - return substr($groupName, 0, $pos); - } - - public function addGroup(Stringable|string|null $caption = null, bool $setAsCurrent = true): ControlGroup - { - $this->nestedGroups[] = parent::addGroup($caption, $setAsCurrent); - return end($this->nestedGroups); - } - - /** - * @throws Exception - */ - public function addSection(?callable $factory = null, ?string $name = null, ?BlockName $blockName = null, array $watchForRedraw = [], ?callable $onRedraw = null, array $validationScope = []): ControlGroup - { - if ($onRedraw && !$name) { - throw new Exception('Name is required when onRedraw is set.'); - } - - if ($this->getCurrentGroup() !== null) { - $name = $this->getCurrentGroup()->getOption('label') . static::GROUP_LEVEL_SEPARATOR . $name; - } - $group = $this->addGroup($name); - $group->setOption('blockName', $blockName?->getName()); - $group->setOption('watchForRedraw', $watchForRedraw); - $factory && $factory(); - array_pop($this->nestedGroups); - $this->setCurrentGroup($this->nestedGroups ? end($this->nestedGroups) : null); - - if ($onRedraw) { - $redrawHandler = 'redraw' . ucfirst($name); - $group->setOption('redrawHandler', $redrawHandler); - $this->addSubmit($redrawHandler) - ->setValidationScope($validationScope) - ->onClick[] = function() use ($onRedraw, $name) { - $onRedraw(); - $this->getParent()->redrawControl($name); - }; - } - - return $group; - } - - public function getSectionName(string ...$path): string - { - return implode(static::GROUP_LEVEL_SEPARATOR, $path); - } } diff --git a/src/SectionTrait.php b/src/SectionTrait.php new file mode 100644 index 0000000..023eafe --- /dev/null +++ b/src/SectionTrait.php @@ -0,0 +1,329 @@ +getCurrentGroup() !== null) { + $name = $this->getCurrentGroup()->getOption('label') . static::GROUP_LEVEL_SEPARATOR . $name; + } + $group = $this->addGroup($name); + $group->setOption('blockName', $blockName?->getName()); + $group->setOption('watchForRedraw', $watchForRedraw); + $group->setOption('htmlId', ($this instanceof Form ? $name : $this->getName() .'-' . $name)); + $factory && $factory(); + array_pop($this->nestedGroups); + $this->setCurrentGroup($this->nestedGroups ? end($this->nestedGroups) : null); + + if ($onRedraw) { + $redrawHandlerName = 'redraw' . ucfirst($name); + $redrawHandler = $this->addSubmit($redrawHandlerName) + ->setValidationScope($validationScope); + $redrawHandler->onClick[] = function() use ($onRedraw) { + $onRedraw(); + $this->getForm()->getParent()->redrawControl($this->getSectionName('price')); + }; + $group->setOption('redrawHandler', $redrawHandler); + } + + return $group; + } + + public function getSectionName(string ...$path): string + { + return implode(static::GROUP_LEVEL_SEPARATOR, $path); + } + + /** + * Adds fieldset group to the form. + */ + public function addGroup(string|\Stringable|null $caption = null, bool $setAsCurrent = true): ControlGroup + { + $group = new ControlGroup; + $group->setOption('label', $caption); + $group->setOption('visual', true); + + if ($setAsCurrent) { + $this->setCurrentGroup($group); + } + + return !is_scalar($caption) || isset($this->groups[$caption]) + ? $this->groups[] = $group + : $this->groups[$caption] = $group; + } + + /** + * Returns all defined groups. + * @return ControlGroup[] + */ + public function getGroups(): array + { + return $this->groups; + } + + public function getStructure(): array + { + if ($this->structure === null) { + $this->structure = $this->processGroups($this->buildComponentGroupMap()); + } + + return $this->structure; + } + + // VYPOCET SECTION + + /** + * Vytvoří mapu, která přiřazuje komponenty k jejich groupám + */ + private function buildComponentGroupMap(): array + { + $map = []; + + foreach ($this->getGroups() as $group) { + foreach ($group->getControls() as $control) { + $map[spl_object_id($control)] = $group; + } + } + + return $map; + } + + /** + * Zpracuje všechny groupy hierarchicky podle '__' + */ + private function processGroups(array $componentToGroup): array + { + $allGroups = []; + $processedContainersGlobal = []; + + // Sesbíráme všechny groupy, které mají komponenty v tomto kontejneru + foreach ($this->getGroups() as $group) { + $groupName = $group->getOption('label') ?? ''; + $items = []; + $processedContainers = []; + + foreach ($group->getControls() as $control) { + // Přeskočíme hidden fieldy + if ($control instanceof Nette\Forms\Controls\HiddenField) { + continue; + } + + // Kontrola: je kontrola součástí tohoto kontejneru (nebo jeho dětí)? + if (!$this->isComponentInContainer($control, $this)) { + continue; + } + + $parent = $control->getParent(); + + // Pokud je komponenta přímo v tomto kontejneru + if ($parent === $this) { + $items[$control->getName()] = [ + 'name' => $control->getName(), + 'type' => 'input', + 'component' => $control + ]; + } else { + // Komponenta je v nějakém sub-kontejneru - najdeme direct child kontejner + $topContainer = $parent; + while ($topContainer->getParent() !== $this) { + $topContainer = $topContainer->getParent(); + } + + $containerId = spl_object_id($topContainer); + if (!isset($processedContainers[$containerId])) { + $children = $this->collectAllComponents($topContainer, $componentToGroup); + $items[$topContainer->getName()] = [ + 'name' => $topContainer->getName(), + 'type' => 'container', + 'component' => $topContainer, + 'children' => $children + ]; + $processedContainers[$containerId] = true; + $processedContainersGlobal[$containerId] = true; + } + } + } + + if (!empty($items)) { + $allGroups[$groupName] = [ + 'name' => $groupName, + 'type' => 'section', + 'section' => $group, + 'children' => $items + ]; + } + } + + // Vytvoříme hierarchii - vnořené groupy přidáme do parent groups + foreach ($allGroups as $groupName => $groupData) { + $parentName = $this->getParentGroupName($groupName); + + if ($parentName !== null && isset($allGroups[$parentName])) { + // ZMĚNA: Použij groupName jako klíč + $allGroups[$parentName]['children'][$groupName] = &$allGroups[$groupName]; + } + } + + // Projdeme komponenty kontejneru v původním pořadí + $result = []; + $processedGroups = []; + + foreach ($this->getComponents() as $component) { + // Přeskočíme hidden fieldy + if ($component instanceof Nette\Forms\Controls\HiddenField) { + continue; + } + + $group = $componentToGroup[spl_object_id($component)] ?? null; + + if ($group !== null) { + $groupName = $group->getOption('label') ?? ''; + + // Pokud je to root level group (pro tento kontejner) a ještě jsme ji nezpracovali + if (!str_contains($groupName, static::GROUP_LEVEL_SEPARATOR) && !isset($processedGroups[$groupName])) { + if (isset($allGroups[$groupName])) { + // ZMĚNA: Použij groupName jako klíč + $result[$groupName] = $allGroups[$groupName]; + $processedGroups[$groupName] = true; + } + } + } else { + // Komponenta bez groupy + if ($component instanceof Container) { + $containerId = spl_object_id($component); + + $childGroup = $this->getContainerChildGroup($component, $componentToGroup); + + if ($childGroup !== null) { + $childGroupName = $childGroup->getOption('label') ?? ''; + if (!str_contains($childGroupName, static::GROUP_LEVEL_SEPARATOR) && + !isset($processedGroups[$childGroupName]) && + isset($allGroups[$childGroupName])) { + // ZMĚNA: Použij childGroupName jako klíč + $result[$childGroupName] = $allGroups[$childGroupName]; + $processedGroups[$childGroupName] = true; + } + } elseif (!isset($processedContainersGlobal[$containerId])) { + $children = $this->collectAllComponents($component, $componentToGroup); + $result[$component->getName()] = [ + 'name' => $component->getName(), + 'type' => 'container', + 'component' => $component, + 'children' => $children + ]; + } + } else { + $result[$component->getName()] = [ + 'name' => $component->getName(), + 'type' => 'input', + 'component' => $component + ]; + } + } + } + + return $result; + } + + /** + * Zkontroluje, zda je komponenta součástí daného kontejneru (nebo jeho dětí) + */ + private function isComponentInContainer($component, $container): bool + { + $parent = $component->getParent(); + + while ($parent !== null) { + if ($parent === $container) { + return true; + } + $parent = $parent->getParent(); + } + + return false; + } + + /** + * Zjistí, jestli děti kontejneru mají nějakou groupu + */ + private function getContainerChildGroup($container, array $componentToGroup): ?ControlGroup + { + foreach ($container->getComponents() as $component) { + $group = $componentToGroup[spl_object_id($component)] ?? null; + if ($group !== null) { + return $group; + } + + if ($component instanceof Container) { + $childGroup = $this->getContainerChildGroup($component, $componentToGroup); + if ($childGroup !== null) { + return $childGroup; + } + } + } + + return null; + } + + /** + * Sesbírá VŠECHNY komponenty z kontejneru (rekurzivně) + */ + private function collectAllComponents($container, array $componentToGroup): array + { + $result = []; + + foreach ($container->getComponents() as $component) { + if ($component instanceof Nette\Forms\Controls\HiddenField) { + continue; + } + + if ($component instanceof Container) { + $children = $this->collectAllComponents($component, $componentToGroup); + $result[$component->getName()] = [ + 'name' => $component->getName(), + 'type' => 'container', + 'component' => $component, + 'children' => $children + ]; + } else { + $result[$component->getName()] = [ + 'name' => $component->getName(), + 'type' => 'input', + 'component' => $component + ]; + } + } + + return $result; + } + + /** + * Zjistí parent group name z group name + */ + private function getParentGroupName(string $groupName): ?string + { + $pos = strrpos($groupName, static::GROUP_LEVEL_SEPARATOR); + if ($pos === false) { + return null; + } + return substr($groupName, 0, $pos); + } +} \ No newline at end of file diff --git a/src/StaticContainer.php b/src/StaticContainer.php index aefafe0..59307d1 100644 --- a/src/StaticContainer.php +++ b/src/StaticContainer.php @@ -10,6 +10,7 @@ class StaticContainer extends BaseContainer { use GetComponentTrait; + use SectionTrait; private ?BaseControl $isFilledComponent = null; private bool $isTemplate = false; From 24524a454ee5a6850a16206d0e34eceda1f2f6ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Wed, 22 Oct 2025 13:24:19 +0200 Subject: [PATCH 21/54] Improves section redraw and form handling Refactors section redraw logic to improve efficiency and prevent unnecessary redraws. Updates the form handling to ensure correct snippet redrawing, especially for sections with redraw handlers. This involves modifying the BaseForm class to handle snippet redrawing more effectively and adjusting the SectionTrait to properly manage redraw triggers and data attributes. It also skips hidden fields and components marked with redraw handlers in group processing. --- src/BaseForm.latte | 16 +--------------- src/BaseForm.php | 10 ++++++---- src/SectionTrait.php | 25 ++++++++++++++----------- 3 files changed, 21 insertions(+), 30 deletions(-) diff --git a/src/BaseForm.latte b/src/BaseForm.latte index 2f5d837..2f8c692 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -11,26 +11,12 @@ {define renderEl} {if $el['type'] === 'section'}
- {if $el['section']->getOption('redrawHandler')} + {if $el['section']->getOption('redrawHandler') && (!$control->isControlInvalid() || $control->isControlInvalid($el['section']->getOption('htmlId')))}
{include renderSection, section => $el}
{include renderInput input => $el['section']->getOption('redrawHandler')} - - {var $fields = $el['section']->getOption('watchForRedraw')} - {var $jsEl = implode(',', array_map(fn($field) => "[name=\"$field\"],[name$=\"[$field]\"]", $fields))} - {var $elName = '#' . $el['name']} - {var $redrawHandler = '[name="' . $el['section']->getOption('redrawHandler')->getName() . '"],[name$="[' . $el['section']->getOption('redrawHandler')->getName() . ']"]'} - {else}
{include renderSection, section => $el} diff --git a/src/BaseForm.php b/src/BaseForm.php index 5a87172..845b109 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -174,10 +174,6 @@ public function render(): void $this->form->getElementPrototype()->class[] = 'ajax'; } - if ($this->presenter->isAjax()) { - $this->redrawControl('formArea'); - } - if (method_exists($this, 'renderForm')) { $this->invokeHandler([$this, 'renderForm']); } @@ -307,4 +303,10 @@ public static function getDefaultTemplateFile(): string { return __DIR__ . '/BaseForm.latte'; } + + public function redrawControl(?string $snippet = null, bool $redraw = true): void + { + parent::redrawControl('formArea'); + parent::redrawControl($snippet, $redraw); + } } \ No newline at end of file diff --git a/src/SectionTrait.php b/src/SectionTrait.php index 023eafe..eed7951 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -27,9 +27,10 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo $name = $this->getCurrentGroup()->getOption('label') . static::GROUP_LEVEL_SEPARATOR . $name; } $group = $this->addGroup($name); + $prefixedName = $this instanceof Form ? $name : $this->getName() .'-' . $name; $group->setOption('blockName', $blockName?->getName()); $group->setOption('watchForRedraw', $watchForRedraw); - $group->setOption('htmlId', ($this instanceof Form ? $name : $this->getName() .'-' . $name)); + $group->setOption('htmlId', $prefixedName); $factory && $factory(); array_pop($this->nestedGroups); $this->setCurrentGroup($this->nestedGroups ? end($this->nestedGroups) : null); @@ -37,12 +38,16 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo if ($onRedraw) { $redrawHandlerName = 'redraw' . ucfirst($name); $redrawHandler = $this->addSubmit($redrawHandlerName) + ->setOption('redrawHandler', true) ->setValidationScope($validationScope); - $redrawHandler->onClick[] = function() use ($onRedraw) { + $redrawHandler->onClick[] = function() use ($onRedraw, $prefixedName) { $onRedraw(); - $this->getForm()->getParent()->redrawControl($this->getSectionName('price')); + $this->getForm()->getParent()->redrawControl($prefixedName); }; $group->setOption('redrawHandler', $redrawHandler); + foreach ($watchForRedraw as $_name) { + $this[$_name]->setHtmlAttribute('data-adt-redraw-snippet', $redrawHandler->getHtmlName()); + } } return $group; @@ -122,8 +127,8 @@ private function processGroups(array $componentToGroup): array $processedContainers = []; foreach ($group->getControls() as $control) { - // Přeskočíme hidden fieldy - if ($control instanceof Nette\Forms\Controls\HiddenField) { + // Přeskočíme hidden fieldy a komponenty s redrawHandler + if ($control instanceof Nette\Forms\Controls\HiddenField || $control->getOption('redrawHandler') === true) { continue; } @@ -178,7 +183,6 @@ private function processGroups(array $componentToGroup): array $parentName = $this->getParentGroupName($groupName); if ($parentName !== null && isset($allGroups[$parentName])) { - // ZMĚNA: Použij groupName jako klíč $allGroups[$parentName]['children'][$groupName] = &$allGroups[$groupName]; } } @@ -188,8 +192,8 @@ private function processGroups(array $componentToGroup): array $processedGroups = []; foreach ($this->getComponents() as $component) { - // Přeskočíme hidden fieldy - if ($component instanceof Nette\Forms\Controls\HiddenField) { + // Přeskočíme hidden fieldy a komponenty s redrawHandler + if ($component instanceof Nette\Forms\Controls\HiddenField || $component->getOption('redrawHandler') === true) { continue; } @@ -201,7 +205,6 @@ private function processGroups(array $componentToGroup): array // Pokud je to root level group (pro tento kontejner) a ještě jsme ji nezpracovali if (!str_contains($groupName, static::GROUP_LEVEL_SEPARATOR) && !isset($processedGroups[$groupName])) { if (isset($allGroups[$groupName])) { - // ZMĚNA: Použij groupName jako klíč $result[$groupName] = $allGroups[$groupName]; $processedGroups[$groupName] = true; } @@ -218,7 +221,6 @@ private function processGroups(array $componentToGroup): array if (!str_contains($childGroupName, static::GROUP_LEVEL_SEPARATOR) && !isset($processedGroups[$childGroupName]) && isset($allGroups[$childGroupName])) { - // ZMĚNA: Použij childGroupName jako klíč $result[$childGroupName] = $allGroups[$childGroupName]; $processedGroups[$childGroupName] = true; } @@ -291,7 +293,8 @@ private function collectAllComponents($container, array $componentToGroup): arra $result = []; foreach ($container->getComponents() as $component) { - if ($component instanceof Nette\Forms\Controls\HiddenField) { + // Přeskočíme hidden fieldy a komponenty s redrawHandler + if ($component instanceof Nette\Forms\Controls\HiddenField || $component->getOption('redrawHandler') === true) { continue; } From 58da3e95aedc9b81b561329b8f0d225d3c716ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Wed, 22 Oct 2025 14:11:10 +0200 Subject: [PATCH 22/54] Adds nested groups to the SectionTrait Adds the newly created control group to the nested groups array. This allows for more complex and structured control group arrangements within sections. --- src/SectionTrait.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SectionTrait.php b/src/SectionTrait.php index eed7951..4bd7ea2 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -13,7 +13,7 @@ trait SectionTrait protected ?array $structure = null; protected array $groups = []; protected array $nestedGroups = []; - + /** * @throws Exception */ @@ -63,7 +63,7 @@ public function getSectionName(string ...$path): string */ public function addGroup(string|\Stringable|null $caption = null, bool $setAsCurrent = true): ControlGroup { - $group = new ControlGroup; + $this->nestedGroups[] = $group = new ControlGroup; $group->setOption('label', $caption); $group->setOption('visual', true); From c621fdf8a2c1c73593fda7dd712f86f62ccc34d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Wed, 22 Oct 2025 19:45:01 +0200 Subject: [PATCH 23/54] Enhances form section rendering and template finding Improves form section structure by ensuring correct ordering of sections and components. This prevents rendering issues, especially when using `insertAfter` option. Additionally, it introduces a mechanism to automatically locate template files for forms, simplifying form template management. --- src/BaseForm.php | 8 +++ src/SectionTrait.php | 157 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 146 insertions(+), 19 deletions(-) diff --git a/src/BaseForm.php b/src/BaseForm.php index 845b109..8231ec0 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -281,6 +281,14 @@ protected function processToggles(Form $form, bool $emptyValue): void protected function getTemplateFile(): ?string { + $reflectionClass = new \ReflectionClass($this); + $templateName = $reflectionClass->getShortName() .'.latte'; + + $templateFile = dirname($reflectionClass->getFileName()) . '/' . $templateName; + if (file_exists($templateFile)) { + return $templateFile; + } + return null; } diff --git a/src/SectionTrait.php b/src/SectionTrait.php index 4bd7ea2..37c3c6d 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -2,9 +2,11 @@ namespace ADT\Forms; -use Nette\Application\UI\Presenter; +use Exception; use Nette\Forms\Container; use Nette\Forms\ControlGroup; +use Nette\Forms\Controls\HiddenField; +use Stringable; trait SectionTrait { @@ -13,6 +15,7 @@ trait SectionTrait protected ?array $structure = null; protected array $groups = []; protected array $nestedGroups = []; + protected ?ControlGroup $lastSection = null; /** * @throws Exception @@ -26,12 +29,19 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo if ($this->getCurrentGroup() !== null) { $name = $this->getCurrentGroup()->getOption('label') . static::GROUP_LEVEL_SEPARATOR . $name; } + + $lastComponent = $this->getForm()->getComponents(); + $lastComponent = end($lastComponent); + $inserAfter = $this->lastSection?->getOption('insertAfter') !== $lastComponent && $this->getComponentGroup($lastComponent) === $this->getCurrentGroup() ? $lastComponent : $this->lastSection; + $group = $this->addGroup($name); + $group->setOption('insertAfter', $inserAfter); $prefixedName = $this instanceof Form ? $name : $this->getName() .'-' . $name; $group->setOption('blockName', $blockName?->getName()); $group->setOption('watchForRedraw', $watchForRedraw); $group->setOption('htmlId', $prefixedName); $factory && $factory(); + $this->lastSection = $group; array_pop($this->nestedGroups); $this->setCurrentGroup($this->nestedGroups ? end($this->nestedGroups) : null); @@ -53,6 +63,16 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo return $group; } + public function getComponentGroup($component): ?ControlGroup + { + foreach ($this->getGroups() as $_group) { + if (in_array($component, $_group->getControls(), true)) { + return $_group; + } + } + return null; + } + public function getSectionName(string ...$path): string { return implode(static::GROUP_LEVEL_SEPARATOR, $path); @@ -61,7 +81,7 @@ public function getSectionName(string ...$path): string /** * Adds fieldset group to the form. */ - public function addGroup(string|\Stringable|null $caption = null, bool $setAsCurrent = true): ControlGroup + public function addGroup(string|Stringable|null $caption = null, bool $setAsCurrent = true): ControlGroup { $this->nestedGroups[] = $group = new ControlGroup; $group->setOption('label', $caption); @@ -94,6 +114,17 @@ public function getStructure(): array return $this->structure; } + public function getSections(): array + { + $sections = []; + foreach ($this->getStructure() as $_el) { + if ($_el['type'] === 'section') { + $sections[$_el['name']] = $_el; + } + } + return $sections; + } + // VYPOCET SECTION /** @@ -128,7 +159,7 @@ private function processGroups(array $componentToGroup): array foreach ($group->getControls() as $control) { // Přeskočíme hidden fieldy a komponenty s redrawHandler - if ($control instanceof Nette\Forms\Controls\HiddenField || $control->getOption('redrawHandler') === true) { + if ($control instanceof HiddenField || $control->getOption('redrawHandler') === true) { continue; } @@ -141,7 +172,7 @@ private function processGroups(array $componentToGroup): array // Pokud je komponenta přímo v tomto kontejneru if ($parent === $this) { - $items[$control->getName()] = [ + $items[] = [ 'name' => $control->getName(), 'type' => 'input', 'component' => $control @@ -156,7 +187,7 @@ private function processGroups(array $componentToGroup): array $containerId = spl_object_id($topContainer); if (!isset($processedContainers[$containerId])) { $children = $this->collectAllComponents($topContainer, $componentToGroup); - $items[$topContainer->getName()] = [ + $items[] = [ 'name' => $topContainer->getName(), 'type' => 'container', 'component' => $topContainer, @@ -168,7 +199,7 @@ private function processGroups(array $componentToGroup): array } } - if (!empty($items)) { + if (!empty($items) || $groupName !== '') { $allGroups[$groupName] = [ 'name' => $groupName, 'type' => 'section', @@ -178,23 +209,64 @@ private function processGroups(array $componentToGroup): array } } - // Vytvoříme hierarchii - vnořené groupy přidáme do parent groups + // Vytvoříme hierarchii - vnořené groupy vložíme do parent groups na správná místa foreach ($allGroups as $groupName => $groupData) { $parentName = $this->getParentGroupName($groupName); if ($parentName !== null && isset($allGroups[$parentName])) { - $allGroups[$parentName]['children'][$groupName] = &$allGroups[$groupName]; + // Vložíme nested groupu na správné místo podle insertAfter + $this->insertItemAtCorrectPosition( + $allGroups[$parentName]['children'], + $groupData, + $groupData['section']->getOption('insertAfter') + ); } } + // Připravíme seznam komponent bez hidden/redraw + $componentsList = []; + foreach ($this->getComponents() as $component) { + if ($component instanceof HiddenField || $component->getOption('redrawHandler') === true) { + continue; + } + $componentsList[] = $component; + } + // Projdeme komponenty kontejneru v původním pořadí $result = []; $processedGroups = []; - foreach ($this->getComponents() as $component) { - // Přeskočíme hidden fieldy a komponenty s redrawHandler - if ($component instanceof Nette\Forms\Controls\HiddenField || $component->getOption('redrawHandler') === true) { - continue; + foreach ($componentsList as $component) { + // Nejdřív zkontrolujeme, jestli před touto komponentou nemáme přidat prázdnou root groupu + foreach ($allGroups as $groupName => $groupData) { + if (isset($processedGroups[$groupName]) || str_contains($groupName, static::GROUP_LEVEL_SEPARATOR)) { + continue; + } + + $insertAfter = $groupData['section']->getOption('insertAfter'); + + // Pokud má group insertAfter a odpovídá předchozí komponentě + if ($insertAfter !== null) { + $lastProcessedItem = empty($result) ? null : end($result); + + if ($lastProcessedItem) { + $shouldInsert = false; + + // Kontrola zda insertAfter odpovídá poslední přidané komponentě + if (isset($lastProcessedItem['component']) && $lastProcessedItem['component'] === $insertAfter) { + $shouldInsert = true; + } + // Kontrola zda insertAfter odpovídá poslední přidané section + elseif (isset($lastProcessedItem['section']) && $lastProcessedItem['section'] === $insertAfter) { + $shouldInsert = true; + } + + if ($shouldInsert) { + $result[] = $groupData; + $processedGroups[$groupName] = true; + } + } + } } $group = $componentToGroup[spl_object_id($component)] ?? null; @@ -205,7 +277,7 @@ private function processGroups(array $componentToGroup): array // Pokud je to root level group (pro tento kontejner) a ještě jsme ji nezpracovali if (!str_contains($groupName, static::GROUP_LEVEL_SEPARATOR) && !isset($processedGroups[$groupName])) { if (isset($allGroups[$groupName])) { - $result[$groupName] = $allGroups[$groupName]; + $result[] = $allGroups[$groupName]; $processedGroups[$groupName] = true; } } @@ -221,12 +293,12 @@ private function processGroups(array $componentToGroup): array if (!str_contains($childGroupName, static::GROUP_LEVEL_SEPARATOR) && !isset($processedGroups[$childGroupName]) && isset($allGroups[$childGroupName])) { - $result[$childGroupName] = $allGroups[$childGroupName]; + $result[] = $allGroups[$childGroupName]; $processedGroups[$childGroupName] = true; } } elseif (!isset($processedContainersGlobal[$containerId])) { $children = $this->collectAllComponents($component, $componentToGroup); - $result[$component->getName()] = [ + $result[] = [ 'name' => $component->getName(), 'type' => 'container', 'component' => $component, @@ -234,7 +306,7 @@ private function processGroups(array $componentToGroup): array ]; } } else { - $result[$component->getName()] = [ + $result[] = [ 'name' => $component->getName(), 'type' => 'input', 'component' => $component @@ -243,9 +315,56 @@ private function processGroups(array $componentToGroup): array } } + // Přidáme zbylé root level groupy, které nebyly zpracované + foreach ($allGroups as $groupName => $groupData) { + if (!str_contains($groupName, static::GROUP_LEVEL_SEPARATOR) && !isset($processedGroups[$groupName])) { + $result[] = $groupData; + } + } + return $result; } + /** + * Vloží item na správné místo v children podle insertAfter + */ + private function insertItemAtCorrectPosition(array &$children, array $newItem, $insertAfter): void + { + if ($insertAfter === null) { + // Přidej na konec + $children[] = $newItem; + return; + } + + // Najdi pozici prvku, za který se má vložit + $insertPosition = null; + + foreach ($children as $index => $child) { + $matches = false; + + // Kontrola zda insertAfter je komponenta + if (isset($child['component']) && $child['component'] === $insertAfter) { + $matches = true; + } + // Kontrola zda insertAfter je section + elseif (isset($child['section']) && $child['section'] === $insertAfter) { + $matches = true; + } + + if ($matches) { + $insertPosition = $index + 1; + break; + } + } + + if ($insertPosition !== null) { + array_splice($children, $insertPosition, 0, [$newItem]); + } else { + // Pokud prvek nebyl nalezen, přidej na konec + $children[] = $newItem; + } + } + /** * Zkontroluje, zda je komponenta součástí daného kontejneru (nebo jeho dětí) */ @@ -294,20 +413,20 @@ private function collectAllComponents($container, array $componentToGroup): arra foreach ($container->getComponents() as $component) { // Přeskočíme hidden fieldy a komponenty s redrawHandler - if ($component instanceof Nette\Forms\Controls\HiddenField || $component->getOption('redrawHandler') === true) { + if ($component instanceof HiddenField || $component->getOption('redrawHandler') === true) { continue; } if ($component instanceof Container) { $children = $this->collectAllComponents($component, $componentToGroup); - $result[$component->getName()] = [ + $result[] = [ 'name' => $component->getName(), 'type' => 'container', 'component' => $component, 'children' => $children ]; } else { - $result[$component->getName()] = [ + $result[] = [ 'name' => $component->getName(), 'type' => 'input', 'component' => $component From 3a13bac6e1d00f1e5a9435de18025685c51dfeda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Wed, 22 Oct 2025 21:53:32 +0200 Subject: [PATCH 24/54] Normalizes section name for block naming Ensures consistent block naming by replacing double underscores in section names with single hyphens. This improves block name generation by normalizing section names, ensuring a consistent and predictable naming convention for blocks, enhancing maintainability. --- src/BaseForm.latte | 2 +- src/SectionTrait.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/BaseForm.latte b/src/BaseForm.latte index 2f8c692..622fa2c 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -42,7 +42,7 @@ {/define} {define renderSection} - {var $blockName = 'section-' . $section['name']} + {var $blockName = 'section-' . str_replace('__', '-', $section['name'])} {ifset #$blockName} {include #$blockName, section => $section} {elseif $section['section']->getOption('blockName')} diff --git a/src/SectionTrait.php b/src/SectionTrait.php index 37c3c6d..f5bfe31 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -22,7 +22,7 @@ trait SectionTrait */ public function addSection(?callable $factory = null, ?string $name = null, ?BlockName $blockName = null, array $watchForRedraw = [], ?callable $onRedraw = null, array $validationScope = []): ControlGroup { - if ($onRedraw && !$name) { + if (($watchForRedraw || $onRedraw) && !$name) { throw new Exception('Name is required when onRedraw is set.'); } @@ -45,13 +45,13 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo array_pop($this->nestedGroups); $this->setCurrentGroup($this->nestedGroups ? end($this->nestedGroups) : null); - if ($onRedraw) { + if ($watchForRedraw || $onRedraw) { $redrawHandlerName = 'redraw' . ucfirst($name); $redrawHandler = $this->addSubmit($redrawHandlerName) ->setOption('redrawHandler', true) ->setValidationScope($validationScope); $redrawHandler->onClick[] = function() use ($onRedraw, $prefixedName) { - $onRedraw(); + $onRedraw && $onRedraw(); $this->getForm()->getParent()->redrawControl($prefixedName); }; $group->setOption('redrawHandler', $redrawHandler); From 96e55f2e32ef086a207bd8abe51e577e5993a96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Thu, 23 Oct 2025 00:19:43 +0200 Subject: [PATCH 25/54] Improves form section rendering and grouping Enhances the form section rendering logic to correctly handle nested groups and hidden fields, ensuring accurate placement of form elements within their respective sections. This change addresses issues related to incorrect group ordering and the unwanted display of hidden fields within form sections. It improves the algorithm for hierarchical group creation and element filtering. --- src/BaseForm.latte | 2 +- src/SectionTrait.php | 85 ++++++++++++++++++++++++++++++++------------ 2 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/BaseForm.latte b/src/BaseForm.latte index 622fa2c..c33f934 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -56,7 +56,7 @@ {/define} {define renderInput} - {if $input instanceof \Nette\Forms\Controls\SubmitButton && !$input->getCaption()} + {if $input instanceof \Nette\Forms\Controls\SubmitButton && !$input->getCaption() || $input instanceof Nette\Forms\Controls\HiddenField} {input $input hidden => true} {else} {formPair $input} diff --git a/src/SectionTrait.php b/src/SectionTrait.php index f5bfe31..c319bc7 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -31,11 +31,11 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo } $lastComponent = $this->getForm()->getComponents(); - $lastComponent = end($lastComponent); - $inserAfter = $this->lastSection?->getOption('insertAfter') !== $lastComponent && $this->getComponentGroup($lastComponent) === $this->getCurrentGroup() ? $lastComponent : $this->lastSection; + $lastComponent = end($lastComponent) ?: null; + $insertAfter = $this->lastSection?->getOption('insertAfter') !== $lastComponent && $this->getComponentGroup($lastComponent) === $this->getCurrentGroup() ? $lastComponent : $this->lastSection; $group = $this->addGroup($name); - $group->setOption('insertAfter', $inserAfter); + $group->setOption('insertAfter', $insertAfter); $prefixedName = $this instanceof Form ? $name : $this->getName() .'-' . $name; $group->setOption('blockName', $blockName?->getName()); $group->setOption('watchForRedraw', $watchForRedraw); @@ -159,7 +159,14 @@ private function processGroups(array $componentToGroup): array foreach ($group->getControls() as $control) { // Přeskočíme hidden fieldy a komponenty s redrawHandler - if ($control instanceof HiddenField || $control->getOption('redrawHandler') === true) { + if ($control->getOption('redrawHandler') === true) { + continue; + } + + // Zjistíme, jestli control patří do nested groupy této groupy + $controlGroup = $componentToGroup[spl_object_id($control)] ?? null; + if ($controlGroup && $controlGroup !== $group) { + // Control patří do jiné groupy (nested), přeskočíme continue; } @@ -209,33 +216,65 @@ private function processGroups(array $componentToGroup): array } } - // Vytvoříme hierarchii - vnořené groupy vložíme do parent groups na správná místa + // Vytvoříme hierarchii - vnořené groupy přidáme do parent groups + // Seřadíme groupy podle počtu '__' (level) + $groupsByLevel = []; foreach ($allGroups as $groupName => $groupData) { - $parentName = $this->getParentGroupName($groupName); - - if ($parentName !== null && isset($allGroups[$parentName])) { - // Vložíme nested groupu na správné místo podle insertAfter - $this->insertItemAtCorrectPosition( - $allGroups[$parentName]['children'], - $groupData, - $groupData['section']->getOption('insertAfter') - ); + $level = substr_count($groupName, static::GROUP_LEVEL_SEPARATOR); + $groupsByLevel[$level][] = $groupName; + } + ksort($groupsByLevel); + + // Zpracujeme OD NEJVYŠŠÍHO LEVELU DOLŮ (aby nested měly své children než se přidají do parenta) + if (!empty($groupsByLevel)) { + $maxLevel = max(array_keys($groupsByLevel)); + + for ($level = $maxLevel; $level >= 1; $level--) { // ZMĚNA: Od max dolů + if (!isset($groupsByLevel[$level])) continue; + + foreach ($groupsByLevel[$level] as $groupName) { + $parentName = $this->getParentGroupName($groupName); + + if ($parentName !== null && isset($allGroups[$parentName])) { + // Vložíme nested groupu na správné místo podle insertAfter + $this->insertItemAtCorrectPosition( + $allGroups[$parentName]['children'], + $allGroups[$groupName], // Bereme aktuální data + $allGroups[$groupName]['section']->getOption('insertAfter') + ); + } + } + } + } + + // Inicializujeme result a processedGroups + $result = []; + $processedGroups = []; + + // Přidáme prázdné root groupy, které mají insertAfter === null (jsou první) + foreach ($allGroups as $groupName => $groupData) { + if (str_contains($groupName, static::GROUP_LEVEL_SEPARATOR)) { + continue; + } + + $insertAfter = $groupData['section']->getOption('insertAfter'); + + if ($insertAfter === null) { + $result[] = $allGroups[$groupName]; + $processedGroups[$groupName] = true; } } // Připravíme seznam komponent bez hidden/redraw $componentsList = []; foreach ($this->getComponents() as $component) { - if ($component instanceof HiddenField || $component->getOption('redrawHandler') === true) { + if ($component->getOption('redrawHandler') === true) { continue; } $componentsList[] = $component; } // Projdeme komponenty kontejneru v původním pořadí - $result = []; - $processedGroups = []; - foreach ($componentsList as $component) { // Nejdřív zkontrolujeme, jestli před touto komponentou nemáme přidat prázdnou root groupu foreach ($allGroups as $groupName => $groupData) { @@ -262,7 +301,7 @@ private function processGroups(array $componentToGroup): array } if ($shouldInsert) { - $result[] = $groupData; + $result[] = $allGroups[$groupName]; $processedGroups[$groupName] = true; } } @@ -318,7 +357,7 @@ private function processGroups(array $componentToGroup): array // Přidáme zbylé root level groupy, které nebyly zpracované foreach ($allGroups as $groupName => $groupData) { if (!str_contains($groupName, static::GROUP_LEVEL_SEPARATOR) && !isset($processedGroups[$groupName])) { - $result[] = $groupData; + $result[] = $allGroups[$groupName]; } } @@ -331,8 +370,8 @@ private function processGroups(array $componentToGroup): array private function insertItemAtCorrectPosition(array &$children, array $newItem, $insertAfter): void { if ($insertAfter === null) { - // Přidej na konec - $children[] = $newItem; + // Přidej na začátek (je to první section) + array_unshift($children, $newItem); return; } @@ -413,7 +452,7 @@ private function collectAllComponents($container, array $componentToGroup): arra foreach ($container->getComponents() as $component) { // Přeskočíme hidden fieldy a komponenty s redrawHandler - if ($component instanceof HiddenField || $component->getOption('redrawHandler') === true) { + if ($component->getOption('redrawHandler') === true) { continue; } From 07a4e107a2483b8537b2665a609f8f528f6e1de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Thu, 23 Oct 2025 14:15:53 +0200 Subject: [PATCH 26/54] Enables dynamic snippet redrawing for sections Allows sections to be dynamically redrawn based on user interactions by introducing a 'watchForSubmit' method to track submit events and trigger snippet updates. This change simplifies the redraw logic within the Latte template, making it more efficient and easier to manage. --- src/BaseForm.latte | 16 ++++++++-------- src/Form.php | 10 ++++++---- src/SectionTrait.php | 37 ++++++++++++++++++++----------------- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/BaseForm.latte b/src/BaseForm.latte index c33f934..4169245 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -11,16 +11,16 @@ {define renderEl} {if $el['type'] === 'section'}
- {if $el['section']->getOption('redrawHandler') && (!$control->isControlInvalid() || $control->isControlInvalid($el['section']->getOption('htmlId')))} -
- {include renderSection, section => $el} -
+ {if $el['section']->getOption('redrawHandler')} + {if (!$control->isControlInvalid() || $control->isControlInvalid($el['section']->getOption('htmlId')))} +
+ {include renderSection, section => $el} +
- {include renderInput input => $el['section']->getOption('redrawHandler')} + {include renderInput input => $el['section']->getOption('redrawHandler')} + {/if} {else} -
- {include renderSection, section => $el} -
+ {include renderSection, section => $el} {/if}
{else} diff --git a/src/Form.php b/src/Form.php index 40b00b6..6b9f763 100644 --- a/src/Form.php +++ b/src/Form.php @@ -2,12 +2,9 @@ namespace ADT\Forms; -use Exception; use Nette; use Nette\Application\UI\Presenter; -use Nette\Forms\Container; -use Nette\Forms\ControlGroup; -use Stringable; +use Nette\Forms\Controls\BaseControl; class Form extends Nette\Application\UI\Form { @@ -46,4 +43,9 @@ public function addError($message, bool $translate = true): void } parent::addError($message, false); } + + public function watchForSubmit(BaseControl $control): void + { + $control->setHtmlAttribute('data-adt-redraw-snippet', $this->addSubmit('submit')->getHtmlName()); + } } diff --git a/src/SectionTrait.php b/src/SectionTrait.php index c319bc7..e6992a9 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -5,7 +5,6 @@ use Exception; use Nette\Forms\Container; use Nette\Forms\ControlGroup; -use Nette\Forms\Controls\HiddenField; use Stringable; trait SectionTrait @@ -22,10 +21,6 @@ trait SectionTrait */ public function addSection(?callable $factory = null, ?string $name = null, ?BlockName $blockName = null, array $watchForRedraw = [], ?callable $onRedraw = null, array $validationScope = []): ControlGroup { - if (($watchForRedraw || $onRedraw) && !$name) { - throw new Exception('Name is required when onRedraw is set.'); - } - if ($this->getCurrentGroup() !== null) { $name = $this->getCurrentGroup()->getOption('label') . static::GROUP_LEVEL_SEPARATOR . $name; } @@ -38,25 +33,33 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo $group->setOption('insertAfter', $insertAfter); $prefixedName = $this instanceof Form ? $name : $this->getName() .'-' . $name; $group->setOption('blockName', $blockName?->getName()); - $group->setOption('watchForRedraw', $watchForRedraw); $group->setOption('htmlId', $prefixedName); $factory && $factory(); $this->lastSection = $group; array_pop($this->nestedGroups); $this->setCurrentGroup($this->nestedGroups ? end($this->nestedGroups) : null); - if ($watchForRedraw || $onRedraw) { - $redrawHandlerName = 'redraw' . ucfirst($name); - $redrawHandler = $this->addSubmit($redrawHandlerName) - ->setOption('redrawHandler', true) - ->setValidationScope($validationScope); - $redrawHandler->onClick[] = function() use ($onRedraw, $prefixedName) { - $onRedraw && $onRedraw(); - $this->getForm()->getParent()->redrawControl($prefixedName); - }; + if ($watchForRedraw) { + $redrawHandler = $this->addSubmit('redraw' . ucfirst($name)); + $redrawHandler->setValidationScope($validationScope); + $redrawHandler->setOption('redrawHandler', true); + + if (is_callable($onRedraw)) { + $redrawHandler->onClick[] = function () use ($onRedraw, $prefixedName) { + $onRedraw(); + $snippet = ''; + foreach (explode(self::GROUP_LEVEL_SEPARATOR, $prefixedName) as $_part) { + $snippet .= $_part; + $this->getForm()->getParent()->redrawControl($snippet); + $snippet .= self::GROUP_LEVEL_SEPARATOR; + + } + }; + } + $group->setOption('redrawHandler', $redrawHandler); - foreach ($watchForRedraw as $_name) { - $this[$_name]->setHtmlAttribute('data-adt-redraw-snippet', $redrawHandler->getHtmlName()); + foreach ($watchForRedraw as $_control) { + $_control->setHtmlAttribute('data-adt-redraw-snippet', $redrawHandler->getHtmlName()); } } From a501b4d2e17bdf5958b2d9cefdd86fd2040557da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Fri, 24 Oct 2025 18:22:11 +0200 Subject: [PATCH 27/54] Refactors form rendering and introduces control groups Improves form rendering logic by introducing control groups for better structure and organization. Changes the latte template to handle the new ControlGroup and use the structure to render elements. The structure trait allows forms and containers to manage their element structure in sections. --- src/BaseForm.latte | 42 ++-- src/ControlGroup.php | 61 ++++++ src/Form.php | 3 +- src/SectionTrait.php | 429 ++-------------------------------------- src/StaticContainer.php | 1 + src/StructureTrait.php | 93 +++++++++ 6 files changed, 189 insertions(+), 440 deletions(-) create mode 100644 src/ControlGroup.php create mode 100644 src/StructureTrait.php diff --git a/src/BaseForm.latte b/src/BaseForm.latte index 4169245..f4da843 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -9,57 +9,55 @@ {/define} {define renderEl} - {if $el['type'] === 'section'} -
- {if $el['section']->getOption('redrawHandler')} - {if (!$control->isControlInvalid() || $control->isControlInvalid($el['section']->getOption('htmlId')))} -
+ {if $el instanceof ADT\Forms\ControlGroup} +
getName()}id="{$el->getName()}"{/if}> + {if $el->getOption('redrawHandler')} + {if (!$control->isControlInvalid() || $control->isControlInvalid($el->getOption('htmlId')))} +
{include renderSection, section => $el}
- {include renderInput input => $el['section']->getOption('redrawHandler')} + {include renderControl control => $el->getOption('redrawHandler')} {/if} {else} {include renderSection, section => $el} {/if}
{else} - {var $component = $el['component']} - - {var $blockName = 'component-' . $component->getName()} + {var $blockName = 'component-' . $el->getName()} {ifset #$blockName} - {include #$blockName, component => $component} + {include #$blockName, component => $el} {else} - {ifset $el['children']} - {foreach $el['children'] as $_el} + {if $el instanceof Nette\Forms\Container} + {foreach $el->getStructure() as $_el} {include renderEl, el => $_el} {/foreach} {else} - {include renderInput, input => $component} - {/ifset} + {include renderControl, control => $el} + {/if} {/ifset} {/if} {/define} {define renderSection} - {var $blockName = 'section-' . str_replace('__', '-', $section['name'])} + {var $blockName = 'section-' . $section->getName()} {ifset #$blockName} {include #$blockName, section => $section} - {elseif $section['section']->getOption('blockName')} - {var $blockName = $section['section']->getOption('blockName')} + {elseif $section->getOption('blockName')} + {var $blockName = $section->getOption('blockName')} {include #$blockName, section => $section} {else} - {foreach $section['children'] as $_el} + {foreach $section->getStructure() as $_el} {include renderEl, el => $_el} {/foreach} {/ifset} {/define} -{define renderInput} - {if $input instanceof \Nette\Forms\Controls\SubmitButton && !$input->getCaption() || $input instanceof Nette\Forms\Controls\HiddenField} - {input $input hidden => true} +{define renderControl} + {if $control instanceof \Nette\Forms\Controls\SubmitButton && !$control->getCaption() || $control instanceof Nette\Forms\Controls\HiddenField} + {input $control hidden => true} {else} - {formPair $input} + {formPair $control} {/if} {/define} diff --git a/src/ControlGroup.php b/src/ControlGroup.php new file mode 100644 index 0000000..616be5c --- /dev/null +++ b/src/ControlGroup.php @@ -0,0 +1,61 @@ +parent = $parent; + $this->name = $name; + } + + public function addGroup($parent, ?string $name): ControlGroup + { + $this->groups[] = $group = new ControlGroup($parent, $name); + return $group; + } + + public function getName(): ?string + { + return $this->name; + } + + public function add(...$items): static + { + foreach ($items as $item) { + if ($item instanceof Control) { + $item->setOption('group', $this); + $this->controls[$item] = null; + + } elseif ($item instanceof Container) { + if ($item->getParent() instanceof DynamicContainer) { + continue; + } + $this->controls[$item] = null; + + } else { + $type = get_debug_type($item); + throw new InvalidArgumentException("Control or Container items expected, $type given."); + } + } + + return $this; + } + + public function getComponents(): array + { + return $this->getControls(); + } +} \ No newline at end of file diff --git a/src/Form.php b/src/Form.php index 6b9f763..5d896e4 100644 --- a/src/Form.php +++ b/src/Form.php @@ -11,6 +11,7 @@ class Form extends Nette\Application\UI\Form use AnnotationsTrait; use GetComponentTrait; use SectionTrait; + use StructureTrait; private ?BootstrapFormRenderer $renderer = null; @@ -18,7 +19,7 @@ public function __construct(?Nette\ComponentModel\IContainer $parent = null, ?st { parent::__construct($parent, $name); - $this->monitor(Presenter::class, function($presenter) { + $this->monitor(Presenter::class, function() { // must be called here because onError and onRender callbacks are set in the constructor $this->getRenderer(); }); diff --git a/src/SectionTrait.php b/src/SectionTrait.php index e6992a9..a8c98d3 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -5,14 +5,11 @@ use Exception; use Nette\Forms\Container; use Nette\Forms\ControlGroup; -use Stringable; trait SectionTrait { - const string GROUP_LEVEL_SEPARATOR = '__'; + const string GROUP_LEVEL_SEPARATOR = '_'; - protected ?array $structure = null; - protected array $groups = []; protected array $nestedGroups = []; protected ?ControlGroup $lastSection = null; @@ -22,14 +19,21 @@ trait SectionTrait public function addSection(?callable $factory = null, ?string $name = null, ?BlockName $blockName = null, array $watchForRedraw = [], ?callable $onRedraw = null, array $validationScope = []): ControlGroup { if ($this->getCurrentGroup() !== null) { - $name = $this->getCurrentGroup()->getOption('label') . static::GROUP_LEVEL_SEPARATOR . $name; + $name = $this->getCurrentGroup()->getName() . static::GROUP_LEVEL_SEPARATOR . $name; } $lastComponent = $this->getForm()->getComponents(); $lastComponent = end($lastComponent) ?: null; - $insertAfter = $this->lastSection?->getOption('insertAfter') !== $lastComponent && $this->getComponentGroup($lastComponent) === $this->getCurrentGroup() ? $lastComponent : $this->lastSection; - - $group = $this->addGroup($name); + $insertAfter = $this->lastSection?->getOption('insertAfter') !== $lastComponent && ($lastComponent instanceof Container ? $lastComponent->getCurrentGroup() : $lastComponent->getOption('group')) === $this->getCurrentGroup() ? $lastComponent : $this->lastSection; + + if ($this->getCurrentGroup()) { + $group = $this->getCurrentGroup()->addGroup($this, $name); + } else { + $group = new \ADT\Forms\ControlGroup($this, $name); + $this->groups[] = $group; + } + $this->setCurrentGroup($group); + $this->nestedGroups[] = $group; $group->setOption('insertAfter', $insertAfter); $prefixedName = $this instanceof Form ? $name : $this->getName() .'-' . $name; $group->setOption('blockName', $blockName?->getName()); @@ -66,57 +70,11 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo return $group; } - public function getComponentGroup($component): ?ControlGroup - { - foreach ($this->getGroups() as $_group) { - if (in_array($component, $_group->getControls(), true)) { - return $_group; - } - } - return null; - } - public function getSectionName(string ...$path): string { return implode(static::GROUP_LEVEL_SEPARATOR, $path); } - /** - * Adds fieldset group to the form. - */ - public function addGroup(string|Stringable|null $caption = null, bool $setAsCurrent = true): ControlGroup - { - $this->nestedGroups[] = $group = new ControlGroup; - $group->setOption('label', $caption); - $group->setOption('visual', true); - - if ($setAsCurrent) { - $this->setCurrentGroup($group); - } - - return !is_scalar($caption) || isset($this->groups[$caption]) - ? $this->groups[] = $group - : $this->groups[$caption] = $group; - } - - /** - * Returns all defined groups. - * @return ControlGroup[] - */ - public function getGroups(): array - { - return $this->groups; - } - - public function getStructure(): array - { - if ($this->structure === null) { - $this->structure = $this->processGroups($this->buildComponentGroupMap()); - } - - return $this->structure; - } - public function getSections(): array { $sections = []; @@ -127,367 +85,4 @@ public function getSections(): array } return $sections; } - - // VYPOCET SECTION - - /** - * Vytvoří mapu, která přiřazuje komponenty k jejich groupám - */ - private function buildComponentGroupMap(): array - { - $map = []; - - foreach ($this->getGroups() as $group) { - foreach ($group->getControls() as $control) { - $map[spl_object_id($control)] = $group; - } - } - - return $map; - } - - /** - * Zpracuje všechny groupy hierarchicky podle '__' - */ - private function processGroups(array $componentToGroup): array - { - $allGroups = []; - $processedContainersGlobal = []; - - // Sesbíráme všechny groupy, které mají komponenty v tomto kontejneru - foreach ($this->getGroups() as $group) { - $groupName = $group->getOption('label') ?? ''; - $items = []; - $processedContainers = []; - - foreach ($group->getControls() as $control) { - // Přeskočíme hidden fieldy a komponenty s redrawHandler - if ($control->getOption('redrawHandler') === true) { - continue; - } - - // Zjistíme, jestli control patří do nested groupy této groupy - $controlGroup = $componentToGroup[spl_object_id($control)] ?? null; - if ($controlGroup && $controlGroup !== $group) { - // Control patří do jiné groupy (nested), přeskočíme - continue; - } - - // Kontrola: je kontrola součástí tohoto kontejneru (nebo jeho dětí)? - if (!$this->isComponentInContainer($control, $this)) { - continue; - } - - $parent = $control->getParent(); - - // Pokud je komponenta přímo v tomto kontejneru - if ($parent === $this) { - $items[] = [ - 'name' => $control->getName(), - 'type' => 'input', - 'component' => $control - ]; - } else { - // Komponenta je v nějakém sub-kontejneru - najdeme direct child kontejner - $topContainer = $parent; - while ($topContainer->getParent() !== $this) { - $topContainer = $topContainer->getParent(); - } - - $containerId = spl_object_id($topContainer); - if (!isset($processedContainers[$containerId])) { - $children = $this->collectAllComponents($topContainer, $componentToGroup); - $items[] = [ - 'name' => $topContainer->getName(), - 'type' => 'container', - 'component' => $topContainer, - 'children' => $children - ]; - $processedContainers[$containerId] = true; - $processedContainersGlobal[$containerId] = true; - } - } - } - - if (!empty($items) || $groupName !== '') { - $allGroups[$groupName] = [ - 'name' => $groupName, - 'type' => 'section', - 'section' => $group, - 'children' => $items - ]; - } - } - - // Vytvoříme hierarchii - vnořené groupy přidáme do parent groups - // Seřadíme groupy podle počtu '__' (level) - $groupsByLevel = []; - foreach ($allGroups as $groupName => $groupData) { - $level = substr_count($groupName, static::GROUP_LEVEL_SEPARATOR); - $groupsByLevel[$level][] = $groupName; - } - ksort($groupsByLevel); - - // Zpracujeme OD NEJVYŠŠÍHO LEVELU DOLŮ (aby nested měly své children než se přidají do parenta) - if (!empty($groupsByLevel)) { - $maxLevel = max(array_keys($groupsByLevel)); - - for ($level = $maxLevel; $level >= 1; $level--) { // ZMĚNA: Od max dolů - if (!isset($groupsByLevel[$level])) continue; - - foreach ($groupsByLevel[$level] as $groupName) { - $parentName = $this->getParentGroupName($groupName); - - if ($parentName !== null && isset($allGroups[$parentName])) { - // Vložíme nested groupu na správné místo podle insertAfter - $this->insertItemAtCorrectPosition( - $allGroups[$parentName]['children'], - $allGroups[$groupName], // Bereme aktuální data - $allGroups[$groupName]['section']->getOption('insertAfter') - ); - } - } - } - } - - // Inicializujeme result a processedGroups - $result = []; - $processedGroups = []; - - // Přidáme prázdné root groupy, které mají insertAfter === null (jsou první) - foreach ($allGroups as $groupName => $groupData) { - if (str_contains($groupName, static::GROUP_LEVEL_SEPARATOR)) { - continue; - } - - $insertAfter = $groupData['section']->getOption('insertAfter'); - - if ($insertAfter === null) { - $result[] = $allGroups[$groupName]; - $processedGroups[$groupName] = true; - } - } - - // Připravíme seznam komponent bez hidden/redraw - $componentsList = []; - foreach ($this->getComponents() as $component) { - if ($component->getOption('redrawHandler') === true) { - continue; - } - $componentsList[] = $component; - } - - // Projdeme komponenty kontejneru v původním pořadí - foreach ($componentsList as $component) { - // Nejdřív zkontrolujeme, jestli před touto komponentou nemáme přidat prázdnou root groupu - foreach ($allGroups as $groupName => $groupData) { - if (isset($processedGroups[$groupName]) || str_contains($groupName, static::GROUP_LEVEL_SEPARATOR)) { - continue; - } - - $insertAfter = $groupData['section']->getOption('insertAfter'); - - // Pokud má group insertAfter a odpovídá předchozí komponentě - if ($insertAfter !== null) { - $lastProcessedItem = empty($result) ? null : end($result); - - if ($lastProcessedItem) { - $shouldInsert = false; - - // Kontrola zda insertAfter odpovídá poslední přidané komponentě - if (isset($lastProcessedItem['component']) && $lastProcessedItem['component'] === $insertAfter) { - $shouldInsert = true; - } - // Kontrola zda insertAfter odpovídá poslední přidané section - elseif (isset($lastProcessedItem['section']) && $lastProcessedItem['section'] === $insertAfter) { - $shouldInsert = true; - } - - if ($shouldInsert) { - $result[] = $allGroups[$groupName]; - $processedGroups[$groupName] = true; - } - } - } - } - - $group = $componentToGroup[spl_object_id($component)] ?? null; - - if ($group !== null) { - $groupName = $group->getOption('label') ?? ''; - - // Pokud je to root level group (pro tento kontejner) a ještě jsme ji nezpracovali - if (!str_contains($groupName, static::GROUP_LEVEL_SEPARATOR) && !isset($processedGroups[$groupName])) { - if (isset($allGroups[$groupName])) { - $result[] = $allGroups[$groupName]; - $processedGroups[$groupName] = true; - } - } - } else { - // Komponenta bez groupy - if ($component instanceof Container) { - $containerId = spl_object_id($component); - - $childGroup = $this->getContainerChildGroup($component, $componentToGroup); - - if ($childGroup !== null) { - $childGroupName = $childGroup->getOption('label') ?? ''; - if (!str_contains($childGroupName, static::GROUP_LEVEL_SEPARATOR) && - !isset($processedGroups[$childGroupName]) && - isset($allGroups[$childGroupName])) { - $result[] = $allGroups[$childGroupName]; - $processedGroups[$childGroupName] = true; - } - } elseif (!isset($processedContainersGlobal[$containerId])) { - $children = $this->collectAllComponents($component, $componentToGroup); - $result[] = [ - 'name' => $component->getName(), - 'type' => 'container', - 'component' => $component, - 'children' => $children - ]; - } - } else { - $result[] = [ - 'name' => $component->getName(), - 'type' => 'input', - 'component' => $component - ]; - } - } - } - - // Přidáme zbylé root level groupy, které nebyly zpracované - foreach ($allGroups as $groupName => $groupData) { - if (!str_contains($groupName, static::GROUP_LEVEL_SEPARATOR) && !isset($processedGroups[$groupName])) { - $result[] = $allGroups[$groupName]; - } - } - - return $result; - } - - /** - * Vloží item na správné místo v children podle insertAfter - */ - private function insertItemAtCorrectPosition(array &$children, array $newItem, $insertAfter): void - { - if ($insertAfter === null) { - // Přidej na začátek (je to první section) - array_unshift($children, $newItem); - return; - } - - // Najdi pozici prvku, za který se má vložit - $insertPosition = null; - - foreach ($children as $index => $child) { - $matches = false; - - // Kontrola zda insertAfter je komponenta - if (isset($child['component']) && $child['component'] === $insertAfter) { - $matches = true; - } - // Kontrola zda insertAfter je section - elseif (isset($child['section']) && $child['section'] === $insertAfter) { - $matches = true; - } - - if ($matches) { - $insertPosition = $index + 1; - break; - } - } - - if ($insertPosition !== null) { - array_splice($children, $insertPosition, 0, [$newItem]); - } else { - // Pokud prvek nebyl nalezen, přidej na konec - $children[] = $newItem; - } - } - - /** - * Zkontroluje, zda je komponenta součástí daného kontejneru (nebo jeho dětí) - */ - private function isComponentInContainer($component, $container): bool - { - $parent = $component->getParent(); - - while ($parent !== null) { - if ($parent === $container) { - return true; - } - $parent = $parent->getParent(); - } - - return false; - } - - /** - * Zjistí, jestli děti kontejneru mají nějakou groupu - */ - private function getContainerChildGroup($container, array $componentToGroup): ?ControlGroup - { - foreach ($container->getComponents() as $component) { - $group = $componentToGroup[spl_object_id($component)] ?? null; - if ($group !== null) { - return $group; - } - - if ($component instanceof Container) { - $childGroup = $this->getContainerChildGroup($component, $componentToGroup); - if ($childGroup !== null) { - return $childGroup; - } - } - } - - return null; - } - - /** - * Sesbírá VŠECHNY komponenty z kontejneru (rekurzivně) - */ - private function collectAllComponents($container, array $componentToGroup): array - { - $result = []; - - foreach ($container->getComponents() as $component) { - // Přeskočíme hidden fieldy a komponenty s redrawHandler - if ($component->getOption('redrawHandler') === true) { - continue; - } - - if ($component instanceof Container) { - $children = $this->collectAllComponents($component, $componentToGroup); - $result[] = [ - 'name' => $component->getName(), - 'type' => 'container', - 'component' => $component, - 'children' => $children - ]; - } else { - $result[] = [ - 'name' => $component->getName(), - 'type' => 'input', - 'component' => $component - ]; - } - } - - return $result; - } - - /** - * Zjistí parent group name z group name - */ - private function getParentGroupName(string $groupName): ?string - { - $pos = strrpos($groupName, static::GROUP_LEVEL_SEPARATOR); - if ($pos === false) { - return null; - } - return substr($groupName, 0, $pos); - } } \ No newline at end of file diff --git a/src/StaticContainer.php b/src/StaticContainer.php index 59307d1..69a091e 100644 --- a/src/StaticContainer.php +++ b/src/StaticContainer.php @@ -11,6 +11,7 @@ class StaticContainer extends BaseContainer { use GetComponentTrait; use SectionTrait; + use StructureTrait; private ?BaseControl $isFilledComponent = null; private bool $isTemplate = false; diff --git a/src/StructureTrait.php b/src/StructureTrait.php new file mode 100644 index 0000000..2f8538e --- /dev/null +++ b/src/StructureTrait.php @@ -0,0 +1,93 @@ +structure === null) { + $this->structure = $this->buildStructure(); + } + return $this->structure; + } + + private function buildStructure(): array + { + $result = []; + $processedGroups = []; + $lastComponent = null; + + // Přidáme groups s insertAfter === null na začátek + foreach ($this->groups as $group) { + $insertAfter = $group->getOption('insertAfter'); + if ($insertAfter === null) { + $result[] = $group; + $processedGroups[spl_object_id($group)] = true; + } + } + + // Projdeme komponenty a přidáme je + jejich groups + foreach ($this->getComponents() as $component) { + // Přeskočíme hidden a redraw + if ($component instanceof HiddenField || + $component->getOption('redrawHandler') === true) { + continue; + } + + // Před zpracováním komponenty zkontroluj, jestli nemáme přidat groupu + foreach ($this->groups as $group) { + $groupId = spl_object_id($group); + if (isset($processedGroups[$groupId])) { + continue; + } + + $insertAfter = $group->getOption('insertAfter'); + if ($insertAfter !== null && $lastComponent === $insertAfter) { + $result[] = $group; + $processedGroups[$groupId] = true; + } + } + + // ZMĚNA: Zjisti groupu podle typu komponenty + if ($component instanceof Container) { + $group = $component->getCurrentGroup(); + } else { + $group = $component->getOption('group'); + } + + if ($group !== null) { + // Pro ControlGroup - pokud komponenta patří DO TÉTO groupy, vykresli ji + if ($this instanceof ControlGroup && $group === $this) { + $result[] = $component; + $lastComponent = $component; + continue; + } + + // Přeskočíme komponentu (patří do jiné groupy) + continue; + } + + // Komponenta bez groupy + $result[] = $component; + $lastComponent = $component; + } + + // Přidáme zbylé groupy + foreach ($this->groups as $group) { + $groupId = spl_object_id($group); + if (!isset($processedGroups[$groupId])) { + $result[] = $group; + } + } + + return $result; + } +} \ No newline at end of file From 1c22552060880c6b010c9064878747ccb8ad1aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Fri, 24 Oct 2025 18:35:17 +0200 Subject: [PATCH 28/54] Refactors SectionTrait for better group handling Refactors SectionTrait to improve section and group management within forms. - Simplifies group instantiation by using the `ControlGroup` class directly. - Renames the redraw handler to prevent naming collisions. - Improves section retrieval by checking for `ControlGroup` instances instead of relying on a string `type` property. --- src/SectionTrait.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/SectionTrait.php b/src/SectionTrait.php index a8c98d3..9790d76 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -4,7 +4,6 @@ use Exception; use Nette\Forms\Container; -use Nette\Forms\ControlGroup; trait SectionTrait { @@ -29,7 +28,7 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo if ($this->getCurrentGroup()) { $group = $this->getCurrentGroup()->addGroup($this, $name); } else { - $group = new \ADT\Forms\ControlGroup($this, $name); + $group = new ControlGroup($this, $name); $this->groups[] = $group; } $this->setCurrentGroup($group); @@ -44,7 +43,7 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo $this->setCurrentGroup($this->nestedGroups ? end($this->nestedGroups) : null); if ($watchForRedraw) { - $redrawHandler = $this->addSubmit('redraw' . ucfirst($name)); + $redrawHandler = $this->addSubmit('_redraw' . ucfirst($name)); $redrawHandler->setValidationScope($validationScope); $redrawHandler->setOption('redrawHandler', true); @@ -79,8 +78,8 @@ public function getSections(): array { $sections = []; foreach ($this->getStructure() as $_el) { - if ($_el['type'] === 'section') { - $sections[$_el['name']] = $_el; + if ($_el instanceof ControlGroup) { + $sections[$_el->getName()] = $_el; } } return $sections; From fadfd5bf2c1d4c1dea70f23972551e53e04de3a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Fri, 24 Oct 2025 18:44:56 +0200 Subject: [PATCH 29/54] Fixes template path resolution in inheritance Ensures that the default template file path is correctly resolved in inherited classes by using reflection to determine the directory of the actual class. This prevents issues where the template path would incorrectly point to the base class's template when extended. --- src/BaseForm.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/BaseForm.php b/src/BaseForm.php index 8231ec0..34b7326 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -309,7 +309,8 @@ protected function convertArrayHashToArray($data) public static function getDefaultTemplateFile(): string { - return __DIR__ . '/BaseForm.latte'; + $reflection = new \ReflectionClass(static::class); + return dirname($reflection->getFileName()) . '/BaseForm.latte'; } public function redrawControl(?string $snippet = null, bool $redraw = true): void From a2b625aace1f580778539565e4cbecaccc96c111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Sat, 25 Oct 2025 12:02:16 +0200 Subject: [PATCH 30/54] Revert "Fixes template path resolution in inheritance" This reverts commit fadfd5bf2c1d4c1dea70f23972551e53e04de3a8. --- src/BaseForm.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/BaseForm.php b/src/BaseForm.php index 34b7326..8231ec0 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -309,8 +309,7 @@ protected function convertArrayHashToArray($data) public static function getDefaultTemplateFile(): string { - $reflection = new \ReflectionClass(static::class); - return dirname($reflection->getFileName()) . '/BaseForm.latte'; + return __DIR__ . '/BaseForm.latte'; } public function redrawControl(?string $snippet = null, bool $redraw = true): void From 7aeacfafd9eab668351f2c87ab977a018ef5090e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Sat, 25 Oct 2025 12:51:19 +0200 Subject: [PATCH 31/54] Refactors form structure handling Replaces the `StructureTrait` with `ElementsTrait` to streamline how form elements are accessed and rendered. This change simplifies the logic in templates and components by providing a more direct way to iterate over form elements, improving maintainability and reducing code duplication. The new `ElementsTrait` consolidates the logic for filtering and ordering elements, making it easier to manage the form structure. --- src/BaseForm.latte | 6 +-- src/ControlGroup.php | 2 +- src/ElementsTrait.php | 60 ++++++++++++++++++++++++++ src/Form.php | 2 +- src/SectionTrait.php | 3 +- src/StaticContainer.php | 2 +- src/StructureTrait.php | 93 ----------------------------------------- 7 files changed, 67 insertions(+), 101 deletions(-) create mode 100644 src/ElementsTrait.php delete mode 100644 src/StructureTrait.php diff --git a/src/BaseForm.latte b/src/BaseForm.latte index f4da843..6651b08 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -29,7 +29,7 @@ {include #$blockName, component => $el} {else} {if $el instanceof Nette\Forms\Container} - {foreach $el->getStructure() as $_el} + {foreach $el->getElements() as $_el} {include renderEl, el => $_el} {/foreach} {else} @@ -47,7 +47,7 @@ {var $blockName = $section->getOption('blockName')} {include #$blockName, section => $section} {else} - {foreach $section->getStructure() as $_el} + {foreach $section->getElements() as $_el} {include renderEl, el => $_el} {/foreach} {/ifset} @@ -65,7 +65,7 @@ {form form} {include errors} - {foreach $form->getStructure() as $_el} + {foreach $form->getElements() as $_el} {include renderEl el => $_el} {/foreach} {/form} diff --git a/src/ControlGroup.php b/src/ControlGroup.php index 616be5c..9f2d7aa 100644 --- a/src/ControlGroup.php +++ b/src/ControlGroup.php @@ -8,7 +8,7 @@ class ControlGroup extends \Nette\Forms\ControlGroup { - use StructureTrait; + use ElementsTrait; protected ?string $name = null; diff --git a/src/ElementsTrait.php b/src/ElementsTrait.php new file mode 100644 index 0000000..a2d9b99 --- /dev/null +++ b/src/ElementsTrait.php @@ -0,0 +1,60 @@ +elements === null) { + $this->elements = $this->buildElements(); + } + return $this->elements; + } + + private function buildElements(): array + { + $result = []; + + foreach ($this->getComponents() as $component) { + if ( + $component instanceof HiddenField + || + $component->getOption('redrawHandler') === true + ) { + continue; + } + + if ($component instanceof Container) { + $group = $component->getCurrentGroup(); + } else { + $group = $component->getOption('group'); + } + + if ($group === null || $group === $this) { + $result[] = $component; + } + } + + foreach ($this->groups as $group) { + if (!$group->getOption('insertAfter')) { + array_unshift($result, $group); + } else { + foreach ($result as $key => $el) { + if ($el === $group->getOption('insertAfter')) { + array_splice($result, $key + 1, 0, [$group]); + break; + } + } + } + } + + return $result; + } +} \ No newline at end of file diff --git a/src/Form.php b/src/Form.php index 5d896e4..1df7351 100644 --- a/src/Form.php +++ b/src/Form.php @@ -11,7 +11,7 @@ class Form extends Nette\Application\UI\Form use AnnotationsTrait; use GetComponentTrait; use SectionTrait; - use StructureTrait; + use ElementsTrait; private ?BootstrapFormRenderer $renderer = null; diff --git a/src/SectionTrait.php b/src/SectionTrait.php index 9790d76..e79d122 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -24,7 +24,6 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo $lastComponent = $this->getForm()->getComponents(); $lastComponent = end($lastComponent) ?: null; $insertAfter = $this->lastSection?->getOption('insertAfter') !== $lastComponent && ($lastComponent instanceof Container ? $lastComponent->getCurrentGroup() : $lastComponent->getOption('group')) === $this->getCurrentGroup() ? $lastComponent : $this->lastSection; - if ($this->getCurrentGroup()) { $group = $this->getCurrentGroup()->addGroup($this, $name); } else { @@ -77,7 +76,7 @@ public function getSectionName(string ...$path): string public function getSections(): array { $sections = []; - foreach ($this->getStructure() as $_el) { + foreach ($this->getElements() as $_el) { if ($_el instanceof ControlGroup) { $sections[$_el->getName()] = $_el; } diff --git a/src/StaticContainer.php b/src/StaticContainer.php index 69a091e..6c4ddcd 100644 --- a/src/StaticContainer.php +++ b/src/StaticContainer.php @@ -11,7 +11,7 @@ class StaticContainer extends BaseContainer { use GetComponentTrait; use SectionTrait; - use StructureTrait; + use ElementsTrait; private ?BaseControl $isFilledComponent = null; private bool $isTemplate = false; diff --git a/src/StructureTrait.php b/src/StructureTrait.php deleted file mode 100644 index 2f8538e..0000000 --- a/src/StructureTrait.php +++ /dev/null @@ -1,93 +0,0 @@ -structure === null) { - $this->structure = $this->buildStructure(); - } - return $this->structure; - } - - private function buildStructure(): array - { - $result = []; - $processedGroups = []; - $lastComponent = null; - - // Přidáme groups s insertAfter === null na začátek - foreach ($this->groups as $group) { - $insertAfter = $group->getOption('insertAfter'); - if ($insertAfter === null) { - $result[] = $group; - $processedGroups[spl_object_id($group)] = true; - } - } - - // Projdeme komponenty a přidáme je + jejich groups - foreach ($this->getComponents() as $component) { - // Přeskočíme hidden a redraw - if ($component instanceof HiddenField || - $component->getOption('redrawHandler') === true) { - continue; - } - - // Před zpracováním komponenty zkontroluj, jestli nemáme přidat groupu - foreach ($this->groups as $group) { - $groupId = spl_object_id($group); - if (isset($processedGroups[$groupId])) { - continue; - } - - $insertAfter = $group->getOption('insertAfter'); - if ($insertAfter !== null && $lastComponent === $insertAfter) { - $result[] = $group; - $processedGroups[$groupId] = true; - } - } - - // ZMĚNA: Zjisti groupu podle typu komponenty - if ($component instanceof Container) { - $group = $component->getCurrentGroup(); - } else { - $group = $component->getOption('group'); - } - - if ($group !== null) { - // Pro ControlGroup - pokud komponenta patří DO TÉTO groupy, vykresli ji - if ($this instanceof ControlGroup && $group === $this) { - $result[] = $component; - $lastComponent = $component; - continue; - } - - // Přeskočíme komponentu (patří do jiné groupy) - continue; - } - - // Komponenta bez groupy - $result[] = $component; - $lastComponent = $component; - } - - // Přidáme zbylé groupy - foreach ($this->groups as $group) { - $groupId = spl_object_id($group); - if (!isset($processedGroups[$groupId])) { - $result[] = $group; - } - } - - return $result; - } -} \ No newline at end of file From 790306b13f9f9d16881a8b5b51022d37ea3735f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Sun, 26 Oct 2025 10:39:56 +0100 Subject: [PATCH 32/54] Fixes section insertion after hidden fields Ensures that sections are correctly inserted after other form elements, even when hidden fields are present, by skipping hidden fields when determining the last component. This prevents sections from being incorrectly placed before hidden fields. --- src/SectionTrait.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/SectionTrait.php b/src/SectionTrait.php index e79d122..b60c1a9 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -4,6 +4,7 @@ use Exception; use Nette\Forms\Container; +use Nette\Forms\Controls\HiddenField; trait SectionTrait { @@ -21,8 +22,13 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo $name = $this->getCurrentGroup()->getName() . static::GROUP_LEVEL_SEPARATOR . $name; } - $lastComponent = $this->getForm()->getComponents(); - $lastComponent = end($lastComponent) ?: null; + $lastComponent = null; + foreach (array_reverse($this->getForm()->getComponents()) as $_component) { + if (!$_component instanceof HiddenField) { + $lastComponent = $_component; + break; + } + } $insertAfter = $this->lastSection?->getOption('insertAfter') !== $lastComponent && ($lastComponent instanceof Container ? $lastComponent->getCurrentGroup() : $lastComponent->getOption('group')) === $this->getCurrentGroup() ? $lastComponent : $this->lastSection; if ($this->getCurrentGroup()) { $group = $this->getCurrentGroup()->addGroup($this, $name); From 53c1fac464d2e0cc034799b3a731b11bc8424ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Mon, 27 Oct 2025 05:32:27 +0100 Subject: [PATCH 33/54] Uses component getter Uses the component getter method instead of accessing form's component. This ensures the code works correctly if the section is not directly associated with a form, but rather embedded within another component that manages its own set of components. --- src/SectionTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SectionTrait.php b/src/SectionTrait.php index b60c1a9..9fe2977 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -23,7 +23,7 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo } $lastComponent = null; - foreach (array_reverse($this->getForm()->getComponents()) as $_component) { + foreach (array_reverse($this->getComponents()) as $_component) { if (!$_component instanceof HiddenField) { $lastComponent = $_component; break; From e60cda015e9b7947a3615355f09f45e475190bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Mon, 27 Oct 2025 05:42:16 +0100 Subject: [PATCH 34/54] Refactors redraw handler logic Simplifies the redraw handler by removing the conditional check for callable onRedraw, as it is always callable due to the previous type hinting. This improves code readability and maintainability without altering the functionality. --- src/SectionTrait.php | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/SectionTrait.php b/src/SectionTrait.php index 9fe2977..3ff95d3 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -51,19 +51,16 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo $redrawHandler = $this->addSubmit('_redraw' . ucfirst($name)); $redrawHandler->setValidationScope($validationScope); $redrawHandler->setOption('redrawHandler', true); + $redrawHandler->onClick[] = function () use ($onRedraw, $prefixedName) { + $onRedraw && $onRedraw(); + $snippet = ''; + foreach (explode(self::GROUP_LEVEL_SEPARATOR, $prefixedName) as $_part) { + $snippet .= $_part; + $this->getForm()->getParent()->redrawControl($snippet); + $snippet .= self::GROUP_LEVEL_SEPARATOR; - if (is_callable($onRedraw)) { - $redrawHandler->onClick[] = function () use ($onRedraw, $prefixedName) { - $onRedraw(); - $snippet = ''; - foreach (explode(self::GROUP_LEVEL_SEPARATOR, $prefixedName) as $_part) { - $snippet .= $_part; - $this->getForm()->getParent()->redrawControl($snippet); - $snippet .= self::GROUP_LEVEL_SEPARATOR; - - } - }; - } + } + }; $group->setOption('redrawHandler', $redrawHandler); foreach ($watchForRedraw as $_control) { From ee2fa0c772c29914e5ccb66fb435e146a47c06d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Mon, 27 Oct 2025 11:26:24 +0100 Subject: [PATCH 35/54] Removes redundant check The check for HiddenField is no longer necessary as the redrawHandler option is sufficient to determine whether a component should be skipped. --- src/ElementsTrait.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/ElementsTrait.php b/src/ElementsTrait.php index a2d9b99..bb40201 100644 --- a/src/ElementsTrait.php +++ b/src/ElementsTrait.php @@ -3,7 +3,6 @@ namespace ADT\Forms; use Nette\Forms\Container; -use Nette\Forms\Controls\HiddenField; trait ElementsTrait { @@ -23,11 +22,7 @@ private function buildElements(): array $result = []; foreach ($this->getComponents() as $component) { - if ( - $component instanceof HiddenField - || - $component->getOption('redrawHandler') === true - ) { + if ($component->getOption('redrawHandler') === true) { continue; } From 8434ca478acfd4d92ec0f6b11c8f6850ff500e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Mon, 27 Oct 2025 16:44:03 +0100 Subject: [PATCH 36/54] Improves component name retrieval Updates component name retrieval to use lookupPath for better compatibility and consistency. Excludes redrawHandler components when determining the last component for section insertion, preventing incorrect ordering. --- src/BaseForm.latte | 2 +- src/SectionTrait.php | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/BaseForm.latte b/src/BaseForm.latte index 6651b08..ed5a989 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -24,7 +24,7 @@ {/if}
{else} - {var $blockName = 'component-' . $el->getName()} + {var $blockName = 'component-' . $el->lookupPath(Nette\Forms\Form::class)} {ifset #$blockName} {include #$blockName, component => $el} {else} diff --git a/src/SectionTrait.php b/src/SectionTrait.php index 3ff95d3..88d0248 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -24,10 +24,13 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo $lastComponent = null; foreach (array_reverse($this->getComponents()) as $_component) { - if (!$_component instanceof HiddenField) { - $lastComponent = $_component; - break; + // redrawHandlery jsou umele vlozene, takze nechceme, aby ovlivnovali poradi + if ($_component->getOption('redrawHandler') === true) { + continue; } + + $lastComponent = $_component; + break; } $insertAfter = $this->lastSection?->getOption('insertAfter') !== $lastComponent && ($lastComponent instanceof Container ? $lastComponent->getCurrentGroup() : $lastComponent->getOption('group')) === $this->getCurrentGroup() ? $lastComponent : $this->lastSection; if ($this->getCurrentGroup()) { From cbd2bb829abf51ae4006c1319006327929358893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Mon, 27 Oct 2025 19:57:05 +0100 Subject: [PATCH 37/54] Fixes redraw control group sections Ensures that nested control groups are correctly redrawn when a redraw handler is triggered. The fix addresses an issue where changes within nested control groups were not always reflected in the UI after a redraw. It achieves this by: - Ensuring the parent form knows of all control groups in the hierarchy - Checking control group invalidation status in the base form latte template, to trigger redraw when necessary - Marking all ancestor control groups as invalid when the onRedraw event is triggered This change provides a smoother user experience by ensuring that the UI accurately reflects the current state of the form. --- src/BaseForm.latte | 2 +- src/ControlGroup.php | 26 +++++++++++++++++++++----- src/Form.php | 1 + src/SectionTrait.php | 23 +++++++++-------------- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/BaseForm.latte b/src/BaseForm.latte index ed5a989..ca29a7a 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -12,7 +12,7 @@ {if $el instanceof ADT\Forms\ControlGroup}
getName()}id="{$el->getName()}"{/if}> {if $el->getOption('redrawHandler')} - {if (!$control->isControlInvalid() || $control->isControlInvalid($el->getOption('htmlId')))} + {if !$control->isControlInvalid() || $control->isControlInvalid($el->getOption('htmlId')) || $el->isControlInvalid()}
{include renderSection, section => $el}
diff --git a/src/ControlGroup.php b/src/ControlGroup.php index 9f2d7aa..cafcd1e 100644 --- a/src/ControlGroup.php +++ b/src/ControlGroup.php @@ -12,18 +12,19 @@ class ControlGroup extends \Nette\Forms\ControlGroup protected ?string $name = null; - protected $parent; + /** @var ControlGroup[] */ + protected array $ancestorGroups; - public function __construct($parent, ?string $name) + public function __construct(array $ancestorGroups, ?string $name) { parent::__construct(); - $this->parent = $parent; + $this->ancestorGroups = $ancestorGroups; $this->name = $name; } - public function addGroup($parent, ?string $name): ControlGroup + public function addGroup(array $ancestorGroups, ?string $name): ControlGroup { - $this->groups[] = $group = new ControlGroup($parent, $name); + $this->groups[] = $group = new ControlGroup($ancestorGroups, $name); return $group; } @@ -58,4 +59,19 @@ public function getComponents(): array { return $this->getControls(); } + + public function isControlInvalid(): bool + { + foreach (array_merge([$this], $this->ancestorGroups) as $_group) { + if ($_group->getOption('isControlInvalid')) { + return true; + } + } + return false; + } + + public function getAncestorGroups(): array + { + return $this->ancestorGroups; + } } \ No newline at end of file diff --git a/src/Form.php b/src/Form.php index 1df7351..d36e252 100644 --- a/src/Form.php +++ b/src/Form.php @@ -14,6 +14,7 @@ class Form extends Nette\Application\UI\Form use ElementsTrait; private ?BootstrapFormRenderer $renderer = null; + public array $ancestorGroups = []; public function __construct(?Nette\ComponentModel\IContainer $parent = null, ?string $name = null) { diff --git a/src/SectionTrait.php b/src/SectionTrait.php index 88d0248..123d5a2 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -4,13 +4,11 @@ use Exception; use Nette\Forms\Container; -use Nette\Forms\Controls\HiddenField; trait SectionTrait { const string GROUP_LEVEL_SEPARATOR = '_'; - protected array $nestedGroups = []; protected ?ControlGroup $lastSection = null; /** @@ -34,34 +32,31 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo } $insertAfter = $this->lastSection?->getOption('insertAfter') !== $lastComponent && ($lastComponent instanceof Container ? $lastComponent->getCurrentGroup() : $lastComponent->getOption('group')) === $this->getCurrentGroup() ? $lastComponent : $this->lastSection; if ($this->getCurrentGroup()) { - $group = $this->getCurrentGroup()->addGroup($this, $name); + $group = $this->getCurrentGroup()->addGroup($this->getForm()->ancestorGroups, $name); } else { - $group = new ControlGroup($this, $name); + $group = new ControlGroup($this->getForm()->ancestorGroups, $name); $this->groups[] = $group; } $this->setCurrentGroup($group); - $this->nestedGroups[] = $group; + $this->getForm()->ancestorGroups[] = $group; $group->setOption('insertAfter', $insertAfter); $prefixedName = $this instanceof Form ? $name : $this->getName() .'-' . $name; $group->setOption('blockName', $blockName?->getName()); $group->setOption('htmlId', $prefixedName); $factory && $factory(); $this->lastSection = $group; - array_pop($this->nestedGroups); - $this->setCurrentGroup($this->nestedGroups ? end($this->nestedGroups) : null); + array_pop($this->getForm()->ancestorGroups); + $this->setCurrentGroup($this->getForm()->ancestorGroups ? end($this->getForm()->ancestorGroups) : null); if ($watchForRedraw) { $redrawHandler = $this->addSubmit('_redraw' . ucfirst($name)); $redrawHandler->setValidationScope($validationScope); $redrawHandler->setOption('redrawHandler', true); - $redrawHandler->onClick[] = function () use ($onRedraw, $prefixedName) { + $redrawHandler->onClick[] = function () use ($onRedraw, $prefixedName, $group) { $onRedraw && $onRedraw(); - $snippet = ''; - foreach (explode(self::GROUP_LEVEL_SEPARATOR, $prefixedName) as $_part) { - $snippet .= $_part; - $this->getForm()->getParent()->redrawControl($snippet); - $snippet .= self::GROUP_LEVEL_SEPARATOR; - + $group->setOption('isControlInvalid', true); + foreach (array_merge([$group], $group->getAncestorGroups()) as $_group) { + $this->getForm()->getParent()->redrawControl($_group->getName()); } }; From 42d5dfa0e7de5de6ddcf5f9211988d74031698c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Tue, 28 Oct 2025 12:37:12 +0100 Subject: [PATCH 38/54] Enhances ControlGroup identification Improves ControlGroup identification by using a dedicated method to generate HTML IDs. This change ensures that ControlGroup elements are correctly identified within forms, especially when nested, by constructing unique HTML IDs based on their parentage. Removes the duplicated constant and instead defines it in the Form class. --- src/BaseForm.latte | 4 ++-- src/ControlGroup.php | 26 ++++++++++++++++++++++---- src/Form.php | 3 +++ src/SectionTrait.php | 43 ++++++++++++++++++++----------------------- 4 files changed, 47 insertions(+), 29 deletions(-) diff --git a/src/BaseForm.latte b/src/BaseForm.latte index ca29a7a..a8cfd28 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -10,10 +10,10 @@ {define renderEl} {if $el instanceof ADT\Forms\ControlGroup} -
getName()}id="{$el->getName()}"{/if}> +
getName()}id="{$el->getHtmlId()}"{/if}> {if $el->getOption('redrawHandler')} {if !$control->isControlInvalid() || $control->isControlInvalid($el->getOption('htmlId')) || $el->isControlInvalid()} -
+
{include renderSection, section => $el}
diff --git a/src/ControlGroup.php b/src/ControlGroup.php index cafcd1e..977f338 100644 --- a/src/ControlGroup.php +++ b/src/ControlGroup.php @@ -2,6 +2,7 @@ namespace ADT\Forms; +use Exception; use Nette\Forms\Container; use Nette\Forms\Control; use Nette\InvalidArgumentException; @@ -11,20 +12,21 @@ class ControlGroup extends \Nette\Forms\ControlGroup use ElementsTrait; protected ?string $name = null; - /** @var ControlGroup[] */ protected array $ancestorGroups; + protected Container $parent; - public function __construct(array $ancestorGroups, ?string $name) + public function __construct(Container $parent, array $ancestorGroups, ?string $name) { parent::__construct(); + $this->parent = $parent; $this->ancestorGroups = $ancestorGroups; $this->name = $name; } - public function addGroup(array $ancestorGroups, ?string $name): ControlGroup + public function addGroup(Container $parent, array $ancestorGroups, ?string $name): ControlGroup { - $this->groups[] = $group = new ControlGroup($ancestorGroups, $name); + $this->groups[] = $group = new ControlGroup($parent, $ancestorGroups, $name); return $group; } @@ -33,6 +35,22 @@ public function getName(): ?string return $this->name; } + /** + * @throws Exception + */ + public function getHtmlId(): string + { + if ($this->name === null) { + throw new Exception('Control group name is not set.'); + } + + if ($this->parent instanceof Form) { + return $this->name; + } + + return $this->parent->lookupPath(Form::class) . Form::GROUP_LEVEL_SEPARATOR . $this->name; + } + public function add(...$items): static { foreach ($items as $item) { diff --git a/src/Form.php b/src/Form.php index d36e252..dca5eb1 100644 --- a/src/Form.php +++ b/src/Form.php @@ -13,7 +13,10 @@ class Form extends Nette\Application\UI\Form use SectionTrait; use ElementsTrait; + const string GROUP_LEVEL_SEPARATOR = '-'; + private ?BootstrapFormRenderer $renderer = null; + /** @var ControlGroup[] */ public array $ancestorGroups = []; public function __construct(?Nette\ComponentModel\IContainer $parent = null, ?string $name = null) diff --git a/src/SectionTrait.php b/src/SectionTrait.php index 123d5a2..60183fd 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -4,22 +4,20 @@ use Exception; use Nette\Forms\Container; +use Nette\InvalidArgumentException; trait SectionTrait { - const string GROUP_LEVEL_SEPARATOR = '_'; - protected ?ControlGroup $lastSection = null; + /** @var ControlGroup[] */ + protected array $allGroups = []; + private const string NameRegexp = '#^[a-zA-Z0-9_]+$#D'; /** * @throws Exception */ public function addSection(?callable $factory = null, ?string $name = null, ?BlockName $blockName = null, array $watchForRedraw = [], ?callable $onRedraw = null, array $validationScope = []): ControlGroup { - if ($this->getCurrentGroup() !== null) { - $name = $this->getCurrentGroup()->getName() . static::GROUP_LEVEL_SEPARATOR . $name; - } - $lastComponent = null; foreach (array_reverse($this->getComponents()) as $_component) { // redrawHandlery jsou umele vlozene, takze nechceme, aby ovlivnovali poradi @@ -32,17 +30,24 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo } $insertAfter = $this->lastSection?->getOption('insertAfter') !== $lastComponent && ($lastComponent instanceof Container ? $lastComponent->getCurrentGroup() : $lastComponent->getOption('group')) === $this->getCurrentGroup() ? $lastComponent : $this->lastSection; if ($this->getCurrentGroup()) { - $group = $this->getCurrentGroup()->addGroup($this->getForm()->ancestorGroups, $name); + $group = $this->getCurrentGroup()->addGroup($this, $this->getForm()->ancestorGroups, $name); } else { - $group = new ControlGroup($this->getForm()->ancestorGroups, $name); + $group = new ControlGroup($this, $this->getForm()->ancestorGroups, $name); $this->groups[] = $group; } + if ($name) { + if (!preg_match(self::NameRegexp, $name)) { + throw new InvalidArgumentException("Component name must be non-empty alphanumeric string, '$name' given."); + } + if (isset($this->allGroups[$name])) { + throw new Exception("Section $name already exists."); + } + $this->allGroups[$name] = $group; + } $this->setCurrentGroup($group); $this->getForm()->ancestorGroups[] = $group; $group->setOption('insertAfter', $insertAfter); - $prefixedName = $this instanceof Form ? $name : $this->getName() .'-' . $name; $group->setOption('blockName', $blockName?->getName()); - $group->setOption('htmlId', $prefixedName); $factory && $factory(); $this->lastSection = $group; array_pop($this->getForm()->ancestorGroups); @@ -52,7 +57,7 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo $redrawHandler = $this->addSubmit('_redraw' . ucfirst($name)); $redrawHandler->setValidationScope($validationScope); $redrawHandler->setOption('redrawHandler', true); - $redrawHandler->onClick[] = function () use ($onRedraw, $prefixedName, $group) { + $redrawHandler->onClick[] = function () use ($onRedraw, $group) { $onRedraw && $onRedraw(); $group->setOption('isControlInvalid', true); foreach (array_merge([$group], $group->getAncestorGroups()) as $_group) { @@ -69,19 +74,11 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo return $group; } - public function getSectionName(string ...$path): string - { - return implode(static::GROUP_LEVEL_SEPARATOR, $path); - } - + /** + * @return ControlGroup[] + */ public function getSections(): array { - $sections = []; - foreach ($this->getElements() as $_el) { - if ($_el instanceof ControlGroup) { - $sections[$_el->getName()] = $_el; - } - } - return $sections; + return $this->allGroups; } } \ No newline at end of file From d3a6f6dcaa89149bb1f6122cabd7c0a16300c231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Tue, 28 Oct 2025 15:29:50 +0100 Subject: [PATCH 39/54] Refactors ControlGroup to Section Renames `ControlGroup` to `Section` to better reflect its purpose in organizing form elements. Updates related code and templates to reflect this change. This change provides a more semantic naming convention. Updates toggle processing to correctly handle sections and ensures that only BaseControl instances are processed. --- src/BaseForm.latte | 8 +++-- src/BaseForm.php | 52 ++++++++++++++++++++------- src/ElementsTrait.php | 18 +++++----- src/{ControlGroup.php => Section.php} | 33 +++++++++-------- src/SectionTrait.php | 46 ++++++++++++------------ 5 files changed, 96 insertions(+), 61 deletions(-) rename src/{ControlGroup.php => Section.php} (61%) diff --git a/src/BaseForm.latte b/src/BaseForm.latte index a8cfd28..dac4637 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -9,7 +9,8 @@ {/define} {define renderEl} - {if $el instanceof ADT\Forms\ControlGroup} + {if $el instanceof ADT\Forms\Section} + {varType ADT\Forms\Section $el}
getName()}id="{$el->getHtmlId()}"{/if}> {if $el->getOption('redrawHandler')} {if !$control->isControlInvalid() || $control->isControlInvalid($el->getOption('htmlId')) || $el->isControlInvalid()} @@ -24,11 +25,13 @@ {/if}
{else} + {varType Nette\ComponentModel\Component $el} {var $blockName = 'component-' . $el->lookupPath(Nette\Forms\Form::class)} {ifset #$blockName} {include #$blockName, component => $el} {else} - {if $el instanceof Nette\Forms\Container} + {if $el instanceof ADT\Forms\StaticContainer} + {varType ADT\Forms\StaticContainer $el} {foreach $el->getElements() as $_el} {include renderEl, el => $_el} {/foreach} @@ -40,6 +43,7 @@ {/define} {define renderSection} + {varType ADT\Forms\Section $section} {var $blockName = 'section-' . $section->getName()} {ifset #$blockName} {include #$blockName, section => $section} diff --git a/src/BaseForm.php b/src/BaseForm.php index 8231ec0..cc46b82 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -5,10 +5,12 @@ use Exception; use Nette\Application\UI\Control; use Nette\Application\UI\Presenter; +use Nette\Forms\Controls\BaseControl; use Nette\Forms\SubmitterControl; use Nette\Utils\ArrayHash; use Nette\Utils\Callback; use Nette\Utils\Type; +use ReflectionClass; use ReflectionException; use ReflectionParameter; @@ -255,23 +257,26 @@ protected function invokeHandler(callable $handler, object|array|null $formValue return $handler(...$params); } + /** + * @throws Exception + */ protected function processToggles(Form $form, bool $emptyValue): void { if ($this->emptyHiddenToggleControls) { $toggles = $form->getToggles(); - foreach ($form->getGroups() as $_group) { - $toggleName = ''; - foreach (explode(Form::GROUP_LEVEL_SEPARATOR, (string)$_group->getOption('label')) as $_togglePart) { - $toggleName = trim($toggleName . Form::GROUP_LEVEL_SEPARATOR . $_togglePart, Form::GROUP_LEVEL_SEPARATOR); - if (isset($toggles[$toggleName]) && $toggles[$toggleName] === false) { - foreach ($_group->getControls() as $_control) { - $_control->setOption('hidden', true); - if ($emptyValue) { - if (method_exists($_control, 'setNullable')) { - $_control->setNullable(true); - } - $_control->setValue(null); + foreach ($this->getSections() as $_section) { + if (isset($toggles[$_section->getHtmlId()]) && $toggles[$_section->getHtmlId()] === false) { + foreach ($_section->getControls() as $_control) { + if (!$_control instanceof BaseControl) { + continue; + } + + $_control->setOption('hidden', true); + if ($emptyValue) { + if (method_exists($_control, 'setNullable')) { + $_control->setNullable(); } + $_control->setValue(null); } } } @@ -279,9 +284,30 @@ protected function processToggles(Form $form, bool $emptyValue): void } } + /** + * @return Section[] + */ + protected function getSections(): array + { + $sections = []; + foreach ($this->form->getSections() as $_section) { + $sections[] = $_section; + } + foreach ($this->form->getComponentTree() as $_component) { + if (!$_component instanceof StaticContainer) { + continue; + } + foreach ($_component->getSections() as $_section) { + $sections[] = $_section; + } + + } + return $sections; + } + protected function getTemplateFile(): ?string { - $reflectionClass = new \ReflectionClass($this); + $reflectionClass = new ReflectionClass($this); $templateName = $reflectionClass->getShortName() .'.latte'; $templateFile = dirname($reflectionClass->getFileName()) . '/' . $templateName; diff --git a/src/ElementsTrait.php b/src/ElementsTrait.php index bb40201..121145c 100644 --- a/src/ElementsTrait.php +++ b/src/ElementsTrait.php @@ -6,7 +6,7 @@ trait ElementsTrait { - protected array $groups = []; + protected array $sections = []; protected ?array $elements = null; public function getElements(): array @@ -27,23 +27,23 @@ private function buildElements(): array } if ($component instanceof Container) { - $group = $component->getCurrentGroup(); + $section = $component->getCurrentGroup(); } else { - $group = $component->getOption('group'); + $section = $component->getOption('section'); } - if ($group === null || $group === $this) { + if ($section === null || $section === $this) { $result[] = $component; } } - foreach ($this->groups as $group) { - if (!$group->getOption('insertAfter')) { - array_unshift($result, $group); + foreach ($this->sections as $_section) { + if (!$_section->getOption('insertAfter')) { + array_unshift($result, $_section); } else { foreach ($result as $key => $el) { - if ($el === $group->getOption('insertAfter')) { - array_splice($result, $key + 1, 0, [$group]); + if ($el === $_section->getOption('insertAfter')) { + array_splice($result, $key + 1, 0, [$_section]); break; } } diff --git a/src/ControlGroup.php b/src/Section.php similarity index 61% rename from src/ControlGroup.php rename to src/Section.php index 977f338..a9697d8 100644 --- a/src/ControlGroup.php +++ b/src/Section.php @@ -7,27 +7,27 @@ use Nette\Forms\Control; use Nette\InvalidArgumentException; -class ControlGroup extends \Nette\Forms\ControlGroup +class Section extends \Nette\Forms\ControlGroup { use ElementsTrait; protected ?string $name = null; - /** @var ControlGroup[] */ - protected array $ancestorGroups; + /** @var Section[] */ + protected array $ancestorSections; protected Container $parent; - public function __construct(Container $parent, array $ancestorGroups, ?string $name) + public function __construct(Container $parent, array $ancestorSections, ?string $name) { parent::__construct(); $this->parent = $parent; - $this->ancestorGroups = $ancestorGroups; + $this->ancestorSections = $ancestorSections; $this->name = $name; } - public function addGroup(Container $parent, array $ancestorGroups, ?string $name): ControlGroup + public function addSection(Container $parent, array $ancestorSections, ?string $name): Section { - $this->groups[] = $group = new ControlGroup($parent, $ancestorGroups, $name); - return $group; + $this->sections[] = $section = new Section($parent, $ancestorSections, $name); + return $section; } public function getName(): ?string @@ -35,13 +35,18 @@ public function getName(): ?string return $this->name; } + public function getParent(): Container + { + return $this->parent; + } + /** * @throws Exception */ public function getHtmlId(): string { if ($this->name === null) { - throw new Exception('Control group name is not set.'); + throw new Exception('Section name is not set.'); } if ($this->parent instanceof Form) { @@ -55,7 +60,7 @@ public function add(...$items): static { foreach ($items as $item) { if ($item instanceof Control) { - $item->setOption('group', $this); + $item->setOption('section', $this); $this->controls[$item] = null; } elseif ($item instanceof Container) { @@ -80,16 +85,16 @@ public function getComponents(): array public function isControlInvalid(): bool { - foreach (array_merge([$this], $this->ancestorGroups) as $_group) { - if ($_group->getOption('isControlInvalid')) { + foreach (array_merge([$this], $this->ancestorSections) as $_section) { + if ($_section->getOption('isControlInvalid')) { return true; } } return false; } - public function getAncestorGroups(): array + public function getAncestorSections(): array { - return $this->ancestorGroups; + return $this->ancestorSections; } } \ No newline at end of file diff --git a/src/SectionTrait.php b/src/SectionTrait.php index 60183fd..7b73f8c 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -8,15 +8,15 @@ trait SectionTrait { - protected ?ControlGroup $lastSection = null; - /** @var ControlGroup[] */ - protected array $allGroups = []; + protected ?Section $lastSection = null; + /** @var Section[] */ + protected array $allSections = []; private const string NameRegexp = '#^[a-zA-Z0-9_]+$#D'; /** * @throws Exception */ - public function addSection(?callable $factory = null, ?string $name = null, ?BlockName $blockName = null, array $watchForRedraw = [], ?callable $onRedraw = null, array $validationScope = []): ControlGroup + public function addSection(?callable $factory = null, ?string $name = null, ?BlockName $blockName = null, array $watchForRedraw = [], ?callable $onRedraw = null, array $validationScope = []): Section { $lastComponent = null; foreach (array_reverse($this->getComponents()) as $_component) { @@ -28,28 +28,28 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo $lastComponent = $_component; break; } - $insertAfter = $this->lastSection?->getOption('insertAfter') !== $lastComponent && ($lastComponent instanceof Container ? $lastComponent->getCurrentGroup() : $lastComponent->getOption('group')) === $this->getCurrentGroup() ? $lastComponent : $this->lastSection; + $insertAfter = $this->lastSection?->getOption('insertAfter') !== $lastComponent && ($lastComponent instanceof Container ? $lastComponent->getCurrentGroup() : $lastComponent->getOption('section')) === $this->getCurrentGroup() ? $lastComponent : $this->lastSection; if ($this->getCurrentGroup()) { - $group = $this->getCurrentGroup()->addGroup($this, $this->getForm()->ancestorGroups, $name); + $section = $this->getCurrentGroup()->addSection($this, $this->getForm()->ancestorGroups, $name); } else { - $group = new ControlGroup($this, $this->getForm()->ancestorGroups, $name); - $this->groups[] = $group; + $section = new Section($this, $this->getForm()->ancestorGroups, $name); + $this->sections[] = $section; } if ($name) { if (!preg_match(self::NameRegexp, $name)) { throw new InvalidArgumentException("Component name must be non-empty alphanumeric string, '$name' given."); } - if (isset($this->allGroups[$name])) { + if (isset($this->allSections[$name])) { throw new Exception("Section $name already exists."); } - $this->allGroups[$name] = $group; + $this->allSections[$name] = $section; } - $this->setCurrentGroup($group); - $this->getForm()->ancestorGroups[] = $group; - $group->setOption('insertAfter', $insertAfter); - $group->setOption('blockName', $blockName?->getName()); + $this->setCurrentGroup($section); + $this->getForm()->ancestorGroups[] = $section; + $section->setOption('insertAfter', $insertAfter); + $section->setOption('blockName', $blockName?->getName()); $factory && $factory(); - $this->lastSection = $group; + $this->lastSection = $section; array_pop($this->getForm()->ancestorGroups); $this->setCurrentGroup($this->getForm()->ancestorGroups ? end($this->getForm()->ancestorGroups) : null); @@ -57,28 +57,28 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo $redrawHandler = $this->addSubmit('_redraw' . ucfirst($name)); $redrawHandler->setValidationScope($validationScope); $redrawHandler->setOption('redrawHandler', true); - $redrawHandler->onClick[] = function () use ($onRedraw, $group) { + $redrawHandler->onClick[] = function () use ($onRedraw, $section) { $onRedraw && $onRedraw(); - $group->setOption('isControlInvalid', true); - foreach (array_merge([$group], $group->getAncestorGroups()) as $_group) { - $this->getForm()->getParent()->redrawControl($_group->getName()); + $section->setOption('isControlInvalid', true); + foreach (array_merge([$section], $section->getAncestorSections()) as $_section) { + $this->getForm()->getParent()->redrawControl($_section->getHtmlId()); } }; - $group->setOption('redrawHandler', $redrawHandler); + $section->setOption('redrawHandler', $redrawHandler); foreach ($watchForRedraw as $_control) { $_control->setHtmlAttribute('data-adt-redraw-snippet', $redrawHandler->getHtmlName()); } } - return $group; + return $section; } /** - * @return ControlGroup[] + * @return Section[] */ public function getSections(): array { - return $this->allGroups; + return $this->allSections; } } \ No newline at end of file From 259fa887d3af709e7891b7bb6ecef2d61ffcb654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Thu, 30 Oct 2025 07:24:27 +0100 Subject: [PATCH 40/54] Fixes section redraw logic Updates section redraw logic to properly handle multiple redraw handlers and ensure that the `isControlInvalid` option is set correctly for ancestor sections. Removes redundant check for control invalidity in the latte template, relying instead on the section's own invalidation status. This streamlines the redraw process and avoids unnecessary re-renders. --- src/BaseForm.latte | 2 +- src/SectionTrait.php | 37 +++++++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/BaseForm.latte b/src/BaseForm.latte index dac4637..692ecf7 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -13,7 +13,7 @@ {varType ADT\Forms\Section $el}
getName()}id="{$el->getHtmlId()}"{/if}> {if $el->getOption('redrawHandler')} - {if !$control->isControlInvalid() || $control->isControlInvalid($el->getOption('htmlId')) || $el->isControlInvalid()} + {if !$control->isControlInvalid() || $el->isControlInvalid()}
{include renderSection, section => $el}
diff --git a/src/SectionTrait.php b/src/SectionTrait.php index 7b73f8c..9acb86c 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -4,6 +4,7 @@ use Exception; use Nette\Forms\Container; +use Nette\Forms\Controls\SubmitButton; use Nette\InvalidArgumentException; trait SectionTrait @@ -12,6 +13,8 @@ trait SectionTrait /** @var Section[] */ protected array $allSections = []; private const string NameRegexp = '#^[a-zA-Z0-9_]+$#D'; + /** @var SubmitButton[] */ + protected array $redrawHandlers = []; /** * @throws Exception @@ -54,20 +57,30 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo $this->setCurrentGroup($this->getForm()->ancestorGroups ? end($this->getForm()->ancestorGroups) : null); if ($watchForRedraw) { - $redrawHandler = $this->addSubmit('_redraw' . ucfirst($name)); - $redrawHandler->setValidationScope($validationScope); - $redrawHandler->setOption('redrawHandler', true); - $redrawHandler->onClick[] = function () use ($onRedraw, $section) { - $onRedraw && $onRedraw(); - $section->setOption('isControlInvalid', true); - foreach (array_merge([$section], $section->getAncestorSections()) as $_section) { - $this->getForm()->getParent()->redrawControl($_section->getHtmlId()); + foreach ($watchForRedraw as $_control) { + $_controlName = $_control->name; + + if (!isset($this->redrawHandlers[$_controlName])) { + $this->redrawHandlers[$_controlName] = $this->addSubmit('_redraw' . ucfirst($_controlName)); } - }; + if ($validationScope !== null) { + if ($this->redrawHandlers[$_controlName]->getValidationScope() !== null) { + $this->redrawHandlers[$_controlName]->setValidationScope(array_merge($this->redrawHandlers[$_controlName]->getValidationScope(), $validationScope)); + } else { + $this->redrawHandlers[$_controlName]->setValidationScope($validationScope); + } + } + $this->redrawHandlers[$_controlName]->setOption('redrawHandler', true); + $this->redrawHandlers[$_controlName]->onClick[] = function () use ($onRedraw, $section) { + $onRedraw && $onRedraw(); + foreach (array_merge([$section], $section->getAncestorSections()) as $_section) { + $_section->setOption('isControlInvalid', true); + $this->getForm()->getParent()->redrawControl($_section->getHtmlId()); + } + }; + $section->setOption('redrawHandler', $this->redrawHandlers[$_controlName]); - $section->setOption('redrawHandler', $redrawHandler); - foreach ($watchForRedraw as $_control) { - $_control->setHtmlAttribute('data-adt-redraw-snippet', $redrawHandler->getHtmlName()); + $_control->setHtmlAttribute('data-adt-redraw-snippet', $this->redrawHandlers[$_controlName]->getHtmlName()); } } From 5772f41c8724c61621b58d547a3be6ba4688c21a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Thu, 30 Oct 2025 12:03:41 +0100 Subject: [PATCH 41/54] Enables multiple redraw handlers for sections Allows sections to have multiple redraw handlers instead of just one. This enables more flexible and granular control over which parts of the form are re-rendered when certain events occur. Also prevents controls from being rendered multiple times. --- src/BaseForm.latte | 16 ++++++++++------ src/SectionTrait.php | 6 +++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/BaseForm.latte b/src/BaseForm.latte index 692ecf7..f2181df 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -12,13 +12,15 @@ {if $el instanceof ADT\Forms\Section} {varType ADT\Forms\Section $el}
getName()}id="{$el->getHtmlId()}"{/if}> - {if $el->getOption('redrawHandler')} + {if $el->getOption('redrawHandlers')} {if !$control->isControlInvalid() || $el->isControlInvalid()}
{include renderSection, section => $el}
- {include renderControl control => $el->getOption('redrawHandler')} + {foreach $el->getOption('redrawHandlers') as $_el} + {include renderControl control => $_el} + {/foreach} {/if} {else} {include renderSection, section => $el} @@ -58,10 +60,12 @@ {/define} {define renderControl} - {if $control instanceof \Nette\Forms\Controls\SubmitButton && !$control->getCaption() || $control instanceof Nette\Forms\Controls\HiddenField} - {input $control hidden => true} - {else} - {formPair $control} + {if !$control->getOption('rendered')} + {if $control instanceof \Nette\Forms\Controls\SubmitButton && !$control->getCaption() || $control instanceof Nette\Forms\Controls\HiddenField} + {input $control hidden => true} + {else} + {formPair $control} + {/if} {/if} {/define} diff --git a/src/SectionTrait.php b/src/SectionTrait.php index 9acb86c..6489e32 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -57,6 +57,7 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo $this->setCurrentGroup($this->getForm()->ancestorGroups ? end($this->getForm()->ancestorGroups) : null); if ($watchForRedraw) { + $redrawHandlers = []; foreach ($watchForRedraw as $_control) { $_controlName = $_control->name; @@ -78,10 +79,13 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo $this->getForm()->getParent()->redrawControl($_section->getHtmlId()); } }; - $section->setOption('redrawHandler', $this->redrawHandlers[$_controlName]); $_control->setHtmlAttribute('data-adt-redraw-snippet', $this->redrawHandlers[$_controlName]->getHtmlName()); + + $redrawHandlers[] = $this->redrawHandlers[$_controlName]; } + + $section->setOption('redrawHandlers', $redrawHandlers); } return $section; From 4d80718d686660fd6adb0c9454f1bb610ead356f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Tue, 4 Nov 2025 08:00:35 +0100 Subject: [PATCH 42/54] Improves section redraw and validation handling Refactors section rendering to use snippets for improved redraw handling. Ensures that sections are only redrawn when necessary. Introduces validation scope configuration for sections and integrates it with redraw handlers. This allows more precise control over which parts of the form are re-rendered and validated, improving performance and user experience. --- src/BaseForm.latte | 24 +++++++++++++++--------- src/BaseForm.php | 1 - src/Section.php | 35 +++++++++++++++++++++++++---------- src/SectionTrait.php | 23 +++++++++++------------ 4 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/BaseForm.latte b/src/BaseForm.latte index f2181df..aa4e395 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -13,15 +13,19 @@ {varType ADT\Forms\Section $el}
getName()}id="{$el->getHtmlId()}"{/if}> {if $el->getOption('redrawHandlers')} - {if !$control->isControlInvalid() || $el->isControlInvalid()} + {if $control->isControlInvalid($el->getHtmlId())}
{include renderSection, section => $el}
- - {foreach $el->getOption('redrawHandlers') as $_el} - {include renderControl control => $_el} - {/foreach} + {else} +
+ {include renderSection, section => $el} +
{/if} + + {foreach $el->getOption('redrawHandlers') as $_el} + {include renderControl control => $_el} + {/foreach} {else} {include renderSection, section => $el} {/if} @@ -71,11 +75,13 @@ {define renderForm} {form form} - {include errors} +
+ {include errors} - {foreach $form->getElements() as $_el} - {include renderEl el => $_el} - {/foreach} + {foreach $form->getElements() as $_el} + {include renderEl el => $_el} + {/foreach} +
{/form} {/define} diff --git a/src/BaseForm.php b/src/BaseForm.php index cc46b82..f46e82e 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -270,7 +270,6 @@ protected function processToggles(Form $form, bool $emptyValue): void if (!$_control instanceof BaseControl) { continue; } - $_control->setOption('hidden', true); if ($emptyValue) { if (method_exists($_control, 'setNullable')) { diff --git a/src/Section.php b/src/Section.php index a9697d8..59ac476 100644 --- a/src/Section.php +++ b/src/Section.php @@ -5,9 +5,10 @@ use Exception; use Nette\Forms\Container; use Nette\Forms\Control; +use Nette\Forms\ControlGroup; use Nette\InvalidArgumentException; -class Section extends \Nette\Forms\ControlGroup +class Section extends ControlGroup { use ElementsTrait; @@ -15,6 +16,8 @@ class Section extends \Nette\Forms\ControlGroup /** @var Section[] */ protected array $ancestorSections; protected Container $parent; + protected array $watchForRedraw = []; + protected array $validationScope = []; public function __construct(Container $parent, array $ancestorSections, ?string $name) { @@ -83,18 +86,30 @@ public function getComponents(): array return $this->getControls(); } - public function isControlInvalid(): bool + public function getAncestorSections(): array { - foreach (array_merge([$this], $this->ancestorSections) as $_section) { - if ($_section->getOption('isControlInvalid')) { - return true; - } - } - return false; + return $this->ancestorSections; } - public function getAncestorSections(): array + public function getWatchForRedraw(): ?array { - return $this->ancestorSections; + return $this->watchForRedraw; + } + + public function setWatchForRedraw(array $watchForRedraw): static + { + $this->watchForRedraw = $watchForRedraw; + return $this; + } + + public function getValidationScope(): array + { + return $this->validationScope; + } + + public function setValidationScope(array $validationScope): static + { + $this->validationScope = $validationScope; + return $this; } } \ No newline at end of file diff --git a/src/SectionTrait.php b/src/SectionTrait.php index 6489e32..60c9b52 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -51,32 +51,31 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo $this->getForm()->ancestorGroups[] = $section; $section->setOption('insertAfter', $insertAfter); $section->setOption('blockName', $blockName?->getName()); - $factory && $factory(); + $section->setWatchForRedraw($watchForRedraw); + $section->setValidationScope($validationScope); + $factory && $factory($section); $this->lastSection = $section; array_pop($this->getForm()->ancestorGroups); $this->setCurrentGroup($this->getForm()->ancestorGroups ? end($this->getForm()->ancestorGroups) : null); - if ($watchForRedraw) { + if ($section->getWatchForRedraw()) { $redrawHandlers = []; - foreach ($watchForRedraw as $_control) { + foreach ($section->getWatchForRedraw() as $_control) { $_controlName = $_control->name; if (!isset($this->redrawHandlers[$_controlName])) { $this->redrawHandlers[$_controlName] = $this->addSubmit('_redraw' . ucfirst($_controlName)); } - if ($validationScope !== null) { - if ($this->redrawHandlers[$_controlName]->getValidationScope() !== null) { - $this->redrawHandlers[$_controlName]->setValidationScope(array_merge($this->redrawHandlers[$_controlName]->getValidationScope(), $validationScope)); - } else { - $this->redrawHandlers[$_controlName]->setValidationScope($validationScope); - } + if ($this->redrawHandlers[$_controlName]->getValidationScope() !== null) { + $this->redrawHandlers[$_controlName]->setValidationScope(array_merge($this->redrawHandlers[$_controlName]->getValidationScope(), $section->getValidationScope())); + } else { + $this->redrawHandlers[$_controlName]->setValidationScope($section->getValidationScope()); } $this->redrawHandlers[$_controlName]->setOption('redrawHandler', true); $this->redrawHandlers[$_controlName]->onClick[] = function () use ($onRedraw, $section) { $onRedraw && $onRedraw(); - foreach (array_merge([$section], $section->getAncestorSections()) as $_section) { - $_section->setOption('isControlInvalid', true); - $this->getForm()->getParent()->redrawControl($_section->getHtmlId()); + if (!$this->getForm()->getParent()->isControlInvalid()) { + $this->getForm()->getParent()->redrawControl($section->getHtmlId()); } }; From feb2f7e7b996cbc2543e825889181124e075ddb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Wed, 5 Nov 2025 10:19:17 +0100 Subject: [PATCH 43/54] Allows SubmitterControl as watchForRedraw Extends the watchForRedraw functionality to accept a SubmitterControl instance directly, simplifying the redraw handling setup when a submitter control triggers the redraw. This change avoids creating additional submit buttons, improving efficiency and code clarity. --- src/Section.php | 7 ++++--- src/SectionTrait.php | 44 ++++++++++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/Section.php b/src/Section.php index 59ac476..71f6a7a 100644 --- a/src/Section.php +++ b/src/Section.php @@ -6,6 +6,7 @@ use Nette\Forms\Container; use Nette\Forms\Control; use Nette\Forms\ControlGroup; +use Nette\Forms\SubmitterControl; use Nette\InvalidArgumentException; class Section extends ControlGroup @@ -16,7 +17,7 @@ class Section extends ControlGroup /** @var Section[] */ protected array $ancestorSections; protected Container $parent; - protected array $watchForRedraw = []; + protected array|SubmitterControl $watchForRedraw; protected array $validationScope = []; public function __construct(Container $parent, array $ancestorSections, ?string $name) @@ -91,12 +92,12 @@ public function getAncestorSections(): array return $this->ancestorSections; } - public function getWatchForRedraw(): ?array + public function getWatchForRedraw(): array|SubmitterControl { return $this->watchForRedraw; } - public function setWatchForRedraw(array $watchForRedraw): static + public function setWatchForRedraw(array|SubmitterControl $watchForRedraw): static { $this->watchForRedraw = $watchForRedraw; return $this; diff --git a/src/SectionTrait.php b/src/SectionTrait.php index 60c9b52..ac2a1ac 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -5,6 +5,7 @@ use Exception; use Nette\Forms\Container; use Nette\Forms\Controls\SubmitButton; +use Nette\Forms\SubmitterControl; use Nette\InvalidArgumentException; trait SectionTrait @@ -60,28 +61,35 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo if ($section->getWatchForRedraw()) { $redrawHandlers = []; - foreach ($section->getWatchForRedraw() as $_control) { - $_controlName = $_control->name; - if (!isset($this->redrawHandlers[$_controlName])) { - $this->redrawHandlers[$_controlName] = $this->addSubmit('_redraw' . ucfirst($_controlName)); - } - if ($this->redrawHandlers[$_controlName]->getValidationScope() !== null) { - $this->redrawHandlers[$_controlName]->setValidationScope(array_merge($this->redrawHandlers[$_controlName]->getValidationScope(), $section->getValidationScope())); - } else { - $this->redrawHandlers[$_controlName]->setValidationScope($section->getValidationScope()); - } - $this->redrawHandlers[$_controlName]->setOption('redrawHandler', true); - $this->redrawHandlers[$_controlName]->onClick[] = function () use ($onRedraw, $section) { - $onRedraw && $onRedraw(); - if (!$this->getForm()->getParent()->isControlInvalid()) { - $this->getForm()->getParent()->redrawControl($section->getHtmlId()); + if ($section->getWatchForRedraw() instanceof SubmitterControl) { + $_controlName = $section->getWatchForRedraw()->getName(); + $this->redrawHandlers[$_controlName] = $section->getWatchForRedraw(); + $redrawHandlers[] = $this->redrawHandlers[$_controlName]; + } else { + foreach ($section->getWatchForRedraw() as $_control) { + $_controlName = $_control->name; + + if (!isset($this->redrawHandlers[$_controlName])) { + $this->redrawHandlers[$_controlName] = $this->addSubmit('_redraw' . ucfirst($_controlName)); } - }; + if ($this->redrawHandlers[$_controlName]->getValidationScope() !== null) { + $this->redrawHandlers[$_controlName]->setValidationScope(array_merge($this->redrawHandlers[$_controlName]->getValidationScope(), $section->getValidationScope())); + } else { + $this->redrawHandlers[$_controlName]->setValidationScope($section->getValidationScope()); + } + $this->redrawHandlers[$_controlName]->setOption('redrawHandler', true); + $this->redrawHandlers[$_controlName]->onClick[] = function () use ($onRedraw, $section) { + $onRedraw && $onRedraw(); + if (!$this->getForm()->getParent()->isControlInvalid()) { + $this->getForm()->getParent()->redrawControl($section->getHtmlId()); + } + }; - $_control->setHtmlAttribute('data-adt-redraw-snippet', $this->redrawHandlers[$_controlName]->getHtmlName()); + $_control->setHtmlAttribute('data-adt-redraw-snippet', $this->redrawHandlers[$_controlName]->getHtmlName()); - $redrawHandlers[] = $this->redrawHandlers[$_controlName]; + $redrawHandlers[] = $this->redrawHandlers[$_controlName]; + } } $section->setOption('redrawHandlers', $redrawHandlers); From ecc87f5ace136a231fa4f420b86fd929c3881ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Thu, 6 Nov 2025 18:46:46 +0100 Subject: [PATCH 44/54] Redraws section unconditionally Removes the conditional check for form validity before redrawing a section. This ensures the section is always redrawn when the associated event is triggered, regardless of the form's validation state. --- src/SectionTrait.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/SectionTrait.php b/src/SectionTrait.php index ac2a1ac..c1773fe 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -81,9 +81,7 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo $this->redrawHandlers[$_controlName]->setOption('redrawHandler', true); $this->redrawHandlers[$_controlName]->onClick[] = function () use ($onRedraw, $section) { $onRedraw && $onRedraw(); - if (!$this->getForm()->getParent()->isControlInvalid()) { - $this->getForm()->getParent()->redrawControl($section->getHtmlId()); - } + $this->getForm()->getParent()->redrawControl($section->getHtmlId()); }; $_control->setHtmlAttribute('data-adt-redraw-snippet', $this->redrawHandlers[$_controlName]->getHtmlName()); From 2b06c46b16cb93cdd3f1e8311ea60e3b306b1e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Mon, 10 Nov 2025 07:02:22 +0100 Subject: [PATCH 45/54] Moves form snippet outside the form tag Encapsulates the form within a snippet for better control and reusability. This allows to refresh the form independently. --- src/BaseForm.latte | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/BaseForm.latte b/src/BaseForm.latte index aa4e395..484cef9 100644 --- a/src/BaseForm.latte +++ b/src/BaseForm.latte @@ -74,15 +74,15 @@ {/define} {define renderForm} - {form form} -
+
+ {form form} {include errors} {foreach $form->getElements() as $_el} {include renderEl el => $_el} {/foreach} -
- {/form} + {/form} +
{/define} {snippetArea formArea} From ae224fca84c88f2679c7c9b9baa80f4cef7657c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Konvi=C4=8Dka?= Date: Fri, 12 Dec 2025 11:28:36 +0100 Subject: [PATCH 46/54] Add form switcher --- src/BootstrapFormRenderer.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/BootstrapFormRenderer.php b/src/BootstrapFormRenderer.php index 5005fde..2383ca5 100644 --- a/src/BootstrapFormRenderer.php +++ b/src/BootstrapFormRenderer.php @@ -287,7 +287,7 @@ protected static function bootstrap4(Nette\Forms\Container $container): void $type = $control->getOption('type'); if ($control instanceof Nette\Forms\Controls\Button) { $control->renderAsButton(); - + if ($control->getValidationScope() !== null) { $control->getControlPrototype()->addClass('btn btn-outline-secondary'); } else { @@ -296,7 +296,16 @@ protected static function bootstrap4(Nette\Forms\Container $container): void } } elseif (in_array($type, ['checkbox', 'radio'], true)) { - if ($control instanceof Nette\Forms\Controls\Checkbox) { + + if ($control->getOption('switch') === true) { + $control->getContainerPrototype() + ->setName('div') + ->addClass('form-check form-switch'); + + $control->getControlPrototype() + ->addClass('form-check-input'); + } + elseif ($control instanceof Nette\Forms\Controls\Checkbox) { $control->getLabelPrototype()->addClass('form-check-label'); } else { $control->getItemLabelPrototype()->addClass('form-check-label'); From 49c2cbaaa8fb866e67b337c72bf7a6be7a0e072c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Sat, 10 Jan 2026 14:08:59 +0100 Subject: [PATCH 47/54] Improves dynamic container functionality Enhances the dynamic container component by allowing for easier creation and management of static containers. This includes: - Using exceptions for clarity and error handling. - Improving default value setting by skipping when the form is not yet submitted. - Improving type safety. --- src/DynamicContainer.php | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/DynamicContainer.php b/src/DynamicContainer.php index 91b03e7..cadde7c 100644 --- a/src/DynamicContainer.php +++ b/src/DynamicContainer.php @@ -2,15 +2,15 @@ namespace ADT\Forms; +use Exception; use Nette; use Nette\Application\UI; use Nette\Application\UI\Presenter; -use Nette\Forms\Form; use Traversable; class DynamicContainer extends BaseContainer { - const NEW_PREFIX = '_new_'; + const string NEW_PREFIX = '_new_'; private StaticContainerFactory $staticContainerFactory; private bool $allowAdding = true; @@ -63,23 +63,26 @@ public function validate(?array $controls = NULL): void * @param string $name * @return Nette\ComponentModel\IComponent|null */ - protected function createComponent($name): ?Nette\ComponentModel\IComponent + protected function createComponent(string $name): ?Nette\ComponentModel\IComponent { return $this[$name] = $this->staticContainerFactory->create(); } - public function setStaticContainerFactory($staticContainerFactory) + public function setStaticContainerFactory($staticContainerFactory): static { $this->staticContainerFactory = $staticContainerFactory; return $this; } + /** + * @throws Exception + */ public function getTemplate(): StaticContainer { if (!$this->isAllowAdding()) { - throw new \Exception('Adding is not allowed.'); + throw new Exception('Adding is not allowed.'); } if (!$this->template) { @@ -94,12 +97,15 @@ public function createNew(): StaticContainer return $this[static::NEW_PREFIX . $this->newCount++]; } - /** - * Fill-in with values. - * @param array|object $data - * @return static - * @internal - */ + public function setDefaults(object|array $data, bool $erase = false): static + { + $form = $this->getForm(throw: false); + if (!$form || !$form->isAnchored() || !$form->isSubmitted()) { + return parent::setDefaults($data, $erase); + } + return $this; + } + public function setValues(object|array $values, bool $erase = false, bool $onlyDisabled = false): static { foreach ($values as $name => $value) { @@ -127,7 +133,7 @@ public function setAllowAdding(bool $allowAdding): self private function getHttpData(): ?array { - $path = explode(self::NAME_SEPARATOR, $this->lookupPath('Nette\Application\UI\Form')); + $path = explode(self::NameSeparator, $this->lookupPath('Nette\Application\UI\Form')); $allData = $this->getForm()->getHttpData(); return Nette\Utils\Arrays::get($allData, $path, NULL); } @@ -136,7 +142,7 @@ private function getHttpData(): ?array /** * @return StaticContainer[] */ - public function getContainers() + public function getContainers(): array { return array_filter( array_filter($this->getComponents(), fn($item) => $item instanceof StaticContainer), From f1f0a3d098259056772af1922b1849e32f6bfe43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Fri, 30 Jan 2026 14:00:48 +0100 Subject: [PATCH 48/54] Returns named components from section Ensures that components are returned as an associative array where keys are the component names and values are the components themselves. This provides more convenient access to the components. --- src/Section.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Section.php b/src/Section.php index 71f6a7a..9817b71 100644 --- a/src/Section.php +++ b/src/Section.php @@ -84,7 +84,11 @@ public function add(...$items): static public function getComponents(): array { - return $this->getControls(); + $components = []; + foreach ($this->getControls() as $control) { + $components[$control->getName()] = $control; + } + return $components; } public function getAncestorSections(): array From 9a751681140ecbc2063504aaa8b269d2b619818f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Sat, 31 Jan 2026 08:29:39 +0100 Subject: [PATCH 49/54] Adds form validation and data filtering Adds functionality to filter validated values from a form, excluding those with validation errors. Improves form data handling by providing a way to retrieve only trusted, validated data. The validation process checks for disabled controls, empty optional fields, and individual rule success, ensuring data integrity and reliability. --- src/Form.php | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/src/Form.php b/src/Form.php index dca5eb1..4026a92 100644 --- a/src/Form.php +++ b/src/Form.php @@ -5,6 +5,7 @@ use Nette; use Nette\Application\UI\Presenter; use Nette\Forms\Controls\BaseControl; +use stdClass; class Form extends Nette\Application\UI\Form { @@ -16,7 +17,7 @@ class Form extends Nette\Application\UI\Form const string GROUP_LEVEL_SEPARATOR = '-'; private ?BootstrapFormRenderer $renderer = null; - /** @var ControlGroup[] */ + /** @var Nette\Forms\ControlGroup[] */ public array $ancestorGroups = []; public function __construct(?Nette\ComponentModel\IContainer $parent = null, ?string $name = null) @@ -53,4 +54,92 @@ public function watchForSubmit(BaseControl $control): void { $control->setHtmlAttribute('data-adt-redraw-snippet', $this->addSubmit('submit')->getHtmlName()); } + + /** + * Returns only validated values from the form. + * Values from controls that have validation errors are excluded. + */ + public function getValidatedValues(string|object|bool|null $returnType = null): object|array + { + $allValues = $this->getUntrustedValues($returnType); + return $this->filterValidValues($this, $allValues); + } + + private function filterValidValues(Nette\Forms\Container $container, $values): array|stdClass + { + $result = is_array($values) ? [] : new stdClass; + $isArray = is_array($values); + + foreach ($container->getComponents() as $name => $component) { + $name = (string) $name; + + // Zkontroluj, jestli hodnota existuje + if ($isArray) { + if (!array_key_exists($name, $values)) { + continue; + } + $value = $values[$name]; + } else { + if (!property_exists($values, $name)) { + continue; + } + $value = $values->$name; + } + + if ($component instanceof Nette\Forms\Container) { + // Rekurzivně filtruj vnořený kontejner + $filtered = $this->filterValidValues($component, $value); + if (is_array($result)) { + $result[$name] = $filtered; + } else { + $result->$name = $filtered; + } + } elseif ($component instanceof BaseControl) { + // Zkontroluj, jestli je control validní + if ($this->isControlValid($component)) { + if (is_array($result)) { + $result[$name] = $value; + } else { + $result->$name = $value; + } + } + } + } + + return $result; + } + + private function isControlValid(BaseControl $control): bool + { + if ($control->isDisabled()) { + return false; + } + + $rules = $control->getRules(); + $emptyOptional = !$rules->isRequired() && !$control->isFilled(); + + return $this->validateBranch($rules, $emptyOptional); + } + + private function validateBranch(Nette\Forms\Rules $branch, bool $emptyOptional): bool + { + foreach ($branch as $rule) { + if (!$rule->branch && $emptyOptional && $rule->validator !== Nette\Forms\Form::Filled) { + continue; + } + + $success = $branch::validateRule($rule); + if (!$success && !$rule->branch) { + return false; + } + + if ($success && $rule->branch) { + if (!$this->validateBranch($rule->branch, $rule->validator === Nette\Forms\Form::Blank ? false : $emptyOptional)) { + return false; + } + } + } + + return true; + } } From e02508256caef4d92a2664338541ce6d083ce0bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Thu, 5 Feb 2026 07:49:55 +0100 Subject: [PATCH 50/54] Increments new component count if prefixed Increments the counter for dynamically created components only when the component's name includes the designated prefix. This ensures accurate tracking of newly instantiated components. --- src/DynamicContainer.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/DynamicContainer.php b/src/DynamicContainer.php index cadde7c..df88c32 100644 --- a/src/DynamicContainer.php +++ b/src/DynamicContainer.php @@ -65,6 +65,9 @@ public function validate(?array $controls = NULL): void */ protected function createComponent(string $name): ?Nette\ComponentModel\IComponent { + if (str_contains($name, static::NEW_PREFIX)) { + $this->newCount++; + } return $this[$name] = $this->staticContainerFactory->create(); } From 4a31ef2e889426508639da73e0213adddcba5aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kud=C4=9Blka?= Date: Wed, 24 Jun 2026 17:20:12 +0200 Subject: [PATCH 51/54] Use getter for control name --- src/SectionTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SectionTrait.php b/src/SectionTrait.php index c1773fe..6cf2d8e 100644 --- a/src/SectionTrait.php +++ b/src/SectionTrait.php @@ -68,7 +68,7 @@ public function addSection(?callable $factory = null, ?string $name = null, ?Blo $redrawHandlers[] = $this->redrawHandlers[$_controlName]; } else { foreach ($section->getWatchForRedraw() as $_control) { - $_controlName = $_control->name; + $_controlName = $_control->getName(); if (!isset($this->redrawHandlers[$_controlName])) { $this->redrawHandlers[$_controlName] = $this->addSubmit('_redraw' . ucfirst($_controlName)); From bce30e9d044729bfa9d9b1156522ced0fa662d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Konvi=C4=8Dka?= Date: Mon, 29 Jun 2026 10:48:32 +0200 Subject: [PATCH 52/54] =?UTF-8?q?feat:=20form=20prvek=20addPasswordReveal?= =?UTF-8?q?=20pro=20maskovan=C3=BD=20text=20s=20odkryt=C3=ADm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Controls/PasswordRevealInput.php | 118 +++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/Controls/PasswordRevealInput.php diff --git a/src/Controls/PasswordRevealInput.php b/src/Controls/PasswordRevealInput.php new file mode 100644 index 0000000..9e79d36 --- /dev/null +++ b/src/Controls/PasswordRevealInput.php @@ -0,0 +1,118 @@ +setHtmlType('password'); + $this->getControlPrototype()->appendAttribute('class', self::INPUT_CLASS); + } + + public function getControl(): Html + { + $input = parent::getControl(); + + $button = Html::el('button') + ->setAttribute('type', 'button') + ->setAttribute('tabindex', '-1') + ->appendAttribute('class', 'btn btn-outline-secondary ' . self::TOGGLE_CLASS) + ->addHtml(Html::el('i')->setAttribute('class', 'fas fa-eye')); + + $group = Html::el('div') + ->setAttribute('class', 'input-group') + ->addHtml($input) + ->addHtml($button); + + if (!self::$scriptRendered) { + self::$scriptRendered = true; + $group->addHtml(self::getScript()); + } + + return $group; + } + + public static function addPasswordReveal(Container $container, string $name, $label = null): self + { + $component = new self($label); + $container->addComponent($component, $name); + return $component; + } + + public static function register(): void + { + Form::extensionMethod('addPasswordReveal', [self::class, 'addPasswordReveal']); + Container::extensionMethod('addPasswordReveal', [self::class, 'addPasswordReveal']); + } + + private static function getScript(): Html + { + $js = << self::INPUT_CLASS, + '{TOGGLE_CLASS}' => self::TOGGLE_CLASS, + ]); + + return Html::el('script')->setHtml($js); + } +} From a055e665614f10562c9b6a48764fe4c8ef502e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Konvi=C4=8Dka?= Date: Mon, 29 Jun 2026 21:02:16 +0200 Subject: [PATCH 53/54] =?UTF-8?q?fix:=20vykresluj=20odkr=C3=BDvac=C3=AD=20?= =?UTF-8?q?script=20u=20ka=C5=BEd=C3=A9ho=20controlu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Controls/PasswordRevealInput.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Controls/PasswordRevealInput.php b/src/Controls/PasswordRevealInput.php index 9e79d36..9d01666 100644 --- a/src/Controls/PasswordRevealInput.php +++ b/src/Controls/PasswordRevealInput.php @@ -14,8 +14,6 @@ class PasswordRevealInput extends TextInput public const string INPUT_CLASS = 'toggle-password-input'; public const string TOGGLE_CLASS = 'toggle-password-reveal'; - private static bool $scriptRendered = false; - public function __construct($label = null, ?int $maxLength = null) { parent::__construct($label, $maxLength); @@ -37,12 +35,8 @@ public function getControl(): Html $group = Html::el('div') ->setAttribute('class', 'input-group') ->addHtml($input) - ->addHtml($button); - - if (!self::$scriptRendered) { - self::$scriptRendered = true; - $group->addHtml(self::getScript()); - } + ->addHtml($button) + ->addHtml(self::getScript()); return $group; } From 9159cd4f2d87c1a38476ea88653437150f33e520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Konvi=C4=8Dka?= Date: Mon, 29 Jun 2026 21:44:44 +0200 Subject: [PATCH 54/54] fix: vypisuj ulozenou hodnotu i pro type=password --- src/Controls/PasswordRevealInput.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controls/PasswordRevealInput.php b/src/Controls/PasswordRevealInput.php index 9d01666..3714ff8 100644 --- a/src/Controls/PasswordRevealInput.php +++ b/src/Controls/PasswordRevealInput.php @@ -25,6 +25,7 @@ public function __construct($label = null, ?int $maxLength = null) public function getControl(): Html { $input = parent::getControl(); + $input->value = $this->getRenderedValue(); $button = Html::el('button') ->setAttribute('type', 'button')