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..484cef9 --- /dev/null +++ b/src/BaseForm.latte @@ -0,0 +1,94 @@ +{if $templateFile} + {import $templateFile} +{/if} + +{varType ADT\Forms\Form $form} + +{define errors} + {$control['form']->getRenderer()->renderErrors()|noescape} +{/define} + +{define renderEl} + {if $el instanceof ADT\Forms\Section} + {varType ADT\Forms\Section $el} +
getName()}id="{$el->getHtmlId()}"{/if}> + {if $el->getOption('redrawHandlers')} + {if $control->isControlInvalid($el->getHtmlId())} +
+ {include renderSection, section => $el} +
+ {else} +
+ {include renderSection, section => $el} +
+ {/if} + + {foreach $el->getOption('redrawHandlers') as $_el} + {include renderControl control => $_el} + {/foreach} + {else} + {include renderSection, section => $el} + {/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 ADT\Forms\StaticContainer} + {varType ADT\Forms\StaticContainer $el} + {foreach $el->getElements() as $_el} + {include renderEl, el => $_el} + {/foreach} + {else} + {include renderControl, control => $el} + {/if} + {/ifset} + {/if} +{/define} + +{define renderSection} + {varType ADT\Forms\Section $section} + {var $blockName = 'section-' . $section->getName()} + {ifset #$blockName} + {include #$blockName, section => $section} + {elseif $section->getOption('blockName')} + {var $blockName = $section->getOption('blockName')} + {include #$blockName, section => $section} + {else} + {foreach $section->getElements() as $_el} + {include renderEl, el => $_el} + {/foreach} + {/ifset} +{/define} + +{define renderControl} + {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} + +{define renderForm} +
+ {form form} + {include errors} + + {foreach $form->getElements() as $_el} + {include renderEl el => $_el} + {/foreach} + {/form} +
+{/define} + +{snippetArea formArea} + {ifset #form} + {include #form} + {else} + {include renderForm} + {/ifset} +{/snippetArea} diff --git a/src/BaseForm.php b/src/BaseForm.php index e1d38a2..f46e82e 100644 --- a/src/BaseForm.php +++ b/src/BaseForm.php @@ -5,14 +5,16 @@ 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\Reflection; use Nette\Utils\Type; +use ReflectionClass; use ReflectionException; +use ReflectionParameter; /** - * @property-read Form $form * @method onBeforeInitForm($form) * @method onAfterInitForm($form) * @method onBeforeValidateForm($form) @@ -21,17 +23,9 @@ */ abstract class BaseForm extends Control { - /** @var Form */ - protected $form; - - /** @var string|null */ - public ?string $templateFilename = null; - - /** @var bool */ - public bool $isAjax = true; - - /** @var bool */ - public bool $emptyHiddenToggleControls = false; + protected Form $form; + protected bool $isAjax = true; + protected bool $emptyHiddenToggleControls = true; /** @var callable[] */ protected array $paramResolvers = []; @@ -68,7 +62,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)) { @@ -77,7 +71,7 @@ public function __construct() if ($type === ArrayHash::class) { return $values; } elseif ($type === 'array') { - return (array) $values; + return $this->convertArrayHashToArray($values); } } @@ -85,7 +79,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']; @@ -114,7 +108,7 @@ public function __construct() if ($form->isSubmitted()) { if (is_bool($form->isSubmitted()) || $form->isSubmitted()->isDisabled()) { - $form->setSubmittedBy(null); + $form->addError('The form must be submitted using the specific submit button.'); } elseif ($form->isSubmitted()->getValidationScope() !== null) { $form->onValidate = []; @@ -128,10 +122,22 @@ 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 + // 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; + } + $this->onBeforeValidateForm($form); if ($form->isValid() && method_exists($this, 'validateForm')) { - $this->invokeHandler([$this, 'validateForm'], $form->getUnsafeValues(null)); + $this->invokeHandler([$this, 'validateForm'], $form->getUntrustedValues()); } } @@ -166,34 +172,20 @@ final public function processFormCallback(Form $form): void */ public function render(): void { - $this->template->setFile(__DIR__ . DIRECTORY_SEPARATOR . 'form.latte'); - - $customTemplatePath = ( - (!empty($this->templateFilename)) - ? $this->templateFilename - : str_replace('.php', '.latte', $this->getReflection()->getFileName()) - ); - - if (file_exists($customTemplatePath)) { - $this->template->customTemplatePath = $customTemplatePath; - } - if ($this->isAjax) { $this->form->getElementPrototype()->class[] = 'ajax'; } - if ($this->presenter->isAjax()) { - $this->redrawControl('formArea'); - } - if (method_exists($this, 'renderForm')) { $this->invokeHandler([$this, 'renderForm']); } - $this->template->render(); + $this->getTemplate()->templateFile = $this->getTemplateFile(); + $this->getTemplate()->setFile(static::getDefaultTemplateFile()); + $this->getTemplate()->render(); } - protected function createComponentForm(): Form + protected function createComponentForm() { return new Form(); } @@ -203,31 +195,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 +229,9 @@ public function setOnSuccess(callable $onSuccess) * @throws ReflectionException * @throws Exception */ - private function invokeHandler($handler, $formValues = null) + protected function invokeHandler(callable $handler, object|array|null $formValues = null) { - $types = array_map(function(\ReflectionParameter $param) { + $types = array_map(function(ReflectionParameter $param) { return Type::resolve($param->getType()->getName(), $param); }, Callback::toReflection($handler)->getParameters()); @@ -262,36 +254,92 @@ private function invokeHandler($handler, $formValues = null) } } - $handler(...$params); + return $handler(...$params); } /** - * @deprecated Use $this->form instead - * @return Form + * @throws Exception */ - 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(); - foreach ($form->getGroups() as $_group) { - $toggleName = ''; - foreach (explode('_', (string)$_group->getOption('label')) as $_togglePart) { - $toggleName = trim($toggleName . '_' . $_togglePart, '_'); - if (isset($toggles[$toggleName]) && $toggles[$toggleName] === false) { - foreach ($_group->getControls() as $_control) { - $_control->setOption('hidden', true); - if ($emptyValue) { - $_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); } } } } } } -} + + /** + * @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); + $templateName = $reflectionClass->getShortName() .'.latte'; + + $templateFile = dirname($reflectionClass->getFileName()) . '/' . $templateName; + if (file_exists($templateFile)) { + return $templateFile; + } + + 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; + } + + 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/BlockName.php b/src/BlockName.php new file mode 100644 index 0000000..2c5cc80 --- /dev/null +++ b/src/BlockName.php @@ -0,0 +1,8 @@ +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'); diff --git a/src/Controls/PasswordRevealInput.php b/src/Controls/PasswordRevealInput.php new file mode 100644 index 0000000..3714ff8 --- /dev/null +++ b/src/Controls/PasswordRevealInput.php @@ -0,0 +1,113 @@ +setHtmlType('password'); + $this->getControlPrototype()->appendAttribute('class', self::INPUT_CLASS); + } + + public function getControl(): Html + { + $input = parent::getControl(); + $input->value = $this->getRenderedValue(); + + $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) + ->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); + } +} diff --git a/src/DynamicContainer.php b/src/DynamicContainer.php index 91b03e7..df88c32 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,29 @@ 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 { + if (str_contains($name, static::NEW_PREFIX)) { + $this->newCount++; + } 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 +100,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 +136,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 +145,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), diff --git a/src/ElementsTrait.php b/src/ElementsTrait.php new file mode 100644 index 0000000..121145c --- /dev/null +++ b/src/ElementsTrait.php @@ -0,0 +1,55 @@ +elements === null) { + $this->elements = $this->buildElements(); + } + return $this->elements; + } + + private function buildElements(): array + { + $result = []; + + foreach ($this->getComponents() as $component) { + if ($component->getOption('redrawHandler') === true) { + continue; + } + + if ($component instanceof Container) { + $section = $component->getCurrentGroup(); + } else { + $section = $component->getOption('section'); + } + + if ($section === null || $section === $this) { + $result[] = $component; + } + } + + foreach ($this->sections as $_section) { + if (!$_section->getOption('insertAfter')) { + array_unshift($result, $_section); + } else { + foreach ($result as $key => $el) { + if ($el === $_section->getOption('insertAfter')) { + array_splice($result, $key + 1, 0, [$_section]); + break; + } + } + } + } + + return $result; + } +} \ No newline at end of file diff --git a/src/Form.php b/src/Form.php index 49c31de..4026a92 100644 --- a/src/Form.php +++ b/src/Form.php @@ -4,19 +4,27 @@ use Nette; use Nette\Application\UI\Presenter; +use Nette\Forms\Controls\BaseControl; +use stdClass; -class Form extends \Nette\Application\UI\Form +class Form extends Nette\Application\UI\Form { use AnnotationsTrait; use GetComponentTrait; + use SectionTrait; + use ElementsTrait; + + const string GROUP_LEVEL_SEPARATOR = '-'; private ?BootstrapFormRenderer $renderer = null; + /** @var Nette\Forms\ControlGroup[] */ + public array $ancestorGroups = []; public function __construct(?Nette\ComponentModel\IContainer $parent = null, ?string $name = null) { 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(); }); @@ -41,4 +49,97 @@ 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()); + } + + /** + * 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; + } } diff --git a/src/Section.php b/src/Section.php new file mode 100644 index 0000000..9817b71 --- /dev/null +++ b/src/Section.php @@ -0,0 +1,120 @@ +parent = $parent; + $this->ancestorSections = $ancestorSections; + $this->name = $name; + } + + public function addSection(Container $parent, array $ancestorSections, ?string $name): Section + { + $this->sections[] = $section = new Section($parent, $ancestorSections, $name); + return $section; + } + + 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('Section 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) { + if ($item instanceof Control) { + $item->setOption('section', $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 + { + $components = []; + foreach ($this->getControls() as $control) { + $components[$control->getName()] = $control; + } + return $components; + } + + public function getAncestorSections(): array + { + return $this->ancestorSections; + } + + public function getWatchForRedraw(): array|SubmitterControl + { + return $this->watchForRedraw; + } + + public function setWatchForRedraw(array|SubmitterControl $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 new file mode 100644 index 0000000..6cf2d8e --- /dev/null +++ b/src/SectionTrait.php @@ -0,0 +1,106 @@ +getComponents()) as $_component) { + // 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('section')) === $this->getCurrentGroup() ? $lastComponent : $this->lastSection; + if ($this->getCurrentGroup()) { + $section = $this->getCurrentGroup()->addSection($this, $this->getForm()->ancestorGroups, $name); + } else { + $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->allSections[$name])) { + throw new Exception("Section $name already exists."); + } + $this->allSections[$name] = $section; + } + $this->setCurrentGroup($section); + $this->getForm()->ancestorGroups[] = $section; + $section->setOption('insertAfter', $insertAfter); + $section->setOption('blockName', $blockName?->getName()); + $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 ($section->getWatchForRedraw()) { + $redrawHandlers = []; + + 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->getName(); + + 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(); + $this->getForm()->getParent()->redrawControl($section->getHtmlId()); + }; + + $_control->setHtmlAttribute('data-adt-redraw-snippet', $this->redrawHandlers[$_controlName]->getHtmlName()); + + $redrawHandlers[] = $this->redrawHandlers[$_controlName]; + } + } + + $section->setOption('redrawHandlers', $redrawHandlers); + } + + return $section; + } + + /** + * @return Section[] + */ + public function getSections(): array + { + return $this->allSections; + } +} \ No newline at end of file diff --git a/src/StaticContainer.php b/src/StaticContainer.php index 2203256..6c4ddcd 100644 --- a/src/StaticContainer.php +++ b/src/StaticContainer.php @@ -5,10 +5,13 @@ use Closure; use Nette\Application\UI\Presenter; use Nette\Forms\Controls\BaseControl; +use Nette\Http\FileUpload; class StaticContainer extends BaseContainer { use GetComponentTrait; + use SectionTrait; + use ElementsTrait; private ?BaseControl $isFilledComponent = null; private bool $isTemplate = false; @@ -53,16 +56,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 597e2fd..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}