diff --git a/.gitignore b/.gitignore
old mode 100755
new mode 100644
diff --git a/.travis.yml b/.travis.yml
index 3560e33..24cc2d8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,47 +1,50 @@
language: php
-
sudo: false
+php:
+ - 7.1
+ - 7.2
+ - 7.3
cache:
directories:
- $HOME/.composer/cache
-env:
- - NETTE=nette-2.4-dev
- - NETTE=nette-2.4
-
-php:
- - 5.6
- - 7.0
-
-matrix:
- include:
- - php: 5.6
- env: NETTE=nette-2.4 COMPOSER_EXTRA_ARGS="--prefer-lowest --prefer-stable"
- - php: 7.0
- env: NETTE=nette-2.4 COVERAGE="--coverage ./coverage.xml --coverage-src ./src" TESTER_RUNTIME="phpdbg"
- allow_failures:
- - php: 7.0
- env: NETTE=nette-2.4 COVERAGE="--coverage ./coverage.xml --coverage-src ./src" TESTER_RUNTIME="phpdbg"
-
before_install:
- - travis_retry composer self-update
- - wget -O /tmp/composer-nette https://raw.githubusercontent.com/Kdyby/TesterExtras/master/bin/composer-nette.php
- - php /tmp/composer-nette
+ - phpenv config-rm xdebug.ini || return 0
install:
- - travis_retry composer update --no-interaction --prefer-dist $COMPOSER_EXTRA_ARGS
+ - travis_retry composer install --no-progress --prefer-dist
- travis_retry composer create-project --no-interaction jakub-onderka/php-parallel-lint /tmp/php-parallel-lint
- - travis_retry composer create-project --no-interaction kdyby/code-checker /tmp/code-checker
- - travis_retry wget -O /tmp/coveralls.phar https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar
script:
- - vendor/bin/tester $COVERAGE -s -p ${TESTER_RUNTIME:-php} -c ./tests/php.ini-unix ./tests/KdybyTests/
- php /tmp/php-parallel-lint/parallel-lint.php -e php,phpt --exclude vendor .
- - php /tmp/code-checker/src/code-checker.php --short-arrays
-
-after_script:
- - if [ "$COVERAGE" != "" ]; then php /tmp/coveralls.phar --verbose --config tests/.coveralls.yml || true; fi
+ - ./vendor/bin/tester -s -p php --colors 1 -C ./tests/Replicator
after_failure:
- - 'for i in $(find ./tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done'
+ - for i in $(find tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done
+
+jobs:
+ include:
+ - env: title="Lowest Dependencies 7.1"
+ php: 7.1
+ install:
+ - travis_retry composer update --no-progress --prefer-dist --prefer-lowest
+ script:
+ - ./vendor/bin/tester -s -p php --colors 1 -C ./tests/Replicator
+
+ - stage: Test Coverage
+ php: 7.1
+ script:
+ - ./vendor/bin/tester -s -p phpdbg --colors 1 -C --coverage ./coverage.xml --coverage-src ./src ./tests/Replicator
+ after_script:
+ - travis_retry wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.1.0/php-coveralls.phar
+ - php php-coveralls.phar --verbose --config tests/.coveralls.yml
+
+ - stage: Outdated Dependencies
+ php: 7.1
+ script:
+ - composer outdated --direct --strict
+
+ allow_failures:
+ - stage: Test Coverage
+ - stage: Outdated Dependencies
diff --git a/README.md b/README.md
old mode 100755
new mode 100644
index fc2ba0b..413ad01
--- a/README.md
+++ b/README.md
@@ -1,47 +1,55 @@
-Kdyby/FormsReplicator
-======
+# Kdyby/FormsReplicator
-[![Build Status](https://travis-ci.org/Kdyby/FormsReplicator.svg?branch=master)](https://travis-ci.org/Kdyby/FormsReplicator)
-[![Downloads this Month](https://img.shields.io/packagist/dm/kdyby/forms-replicator.svg)](https://packagist.org/packages/kdyby/forms-replicator)
-[![Latest stable](https://img.shields.io/packagist/v/kdyby/forms-replicator.svg)](https://packagist.org/packages/kdyby/forms-replicator)
-[![Coverage Status](https://coveralls.io/repos/github/Kdyby/FormsReplicator/badge.svg?branch=master)](https://coveralls.io/github/Kdyby/FormsReplicator?branch=master)
-[![Join the chat at https://gitter.im/Kdyby/Help](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Kdyby/Help)
-
-
-Save me please!
----------------
-
-The maintainer of this project has no more time to maintain it. It may even contain unfixed bugs :(
-
-If you need something like this and you're willing to join in, you're welcome to take over this project.
+-----
-![help](https://cdn.kdyby.org/keyboard-help.png)
+[![Build Status](https://img.shields.io/travis/Kdyby/FormsReplicator.svg?style=flat-square)](https://travis-ci.org/Kdyby/FormsReplicator)
+[![Code coverage](https://img.shields.io/coveralls/Kdyby/FormsReplicator.svg?style=flat-square)](https://coveralls.io/github/Kdyby/FormsReplicator)
+[![Licence](https://img.shields.io/packagist/l/Kdyby/FormsReplicator.svg?style=flat-square)](https://packagist.org/packages/kdyby/forms-replicator)
+[![Downloads this Month](https://img.shields.io/packagist/dm/Kdyby/FormsReplicator.svg?style=flat-square)](https://packagist.org/packages/kdyby/forms-replicator)
+[![Downloads total](https://img.shields.io/packagist/dt/Kdyby/FormsReplicator.svg?style=flat-square)](https://packagist.org/packages/kdyby/forms-replicator)
+[![Latest stable](https://img.shields.io/packagist/v/Kdyby/FormsReplicator.svg?style=flat-square)](https://packagist.org/packages/kdyby/forms-replicator)
+## Discussion / Help
-Requirements
-------------
+[![Join the chat](https://img.shields.io/gitter/room/Kdyby/Help.svg?style=flat-square)](https://gitter.im/Kdyby/Help)
-Kdyby/FormsReplicator requires PHP 5.5 or higher.
+## Install
-- [Nette Framework](https://github.com/nette/nette)
+```sh
+composer require kdyby/forms-replicator
+```
+## Versions
-Installation
-------------
+| State | Version | Branch | PHP |
+|-------------|--------------|----------|----------|
+| rc | `^2.0.0@rc` | `master` | `>= 7.1` |
+| stable | `^1.3.0` | `master` | `>= 5.6` |
-The best way to install Kdyby/FormsReplicator is using [Composer](http://getcomposer.org/):
+## Overview
-```sh
-$ composer require kdyby/forms-replicator:~1.1
-```
+- [Learn more in the documentation](https://github.com/Kdyby/FormsReplicator/blob/master/docs/en/index.md)
+## Maintainers
-Documentation
-------------
+
-Learn more in the [documentation](https://github.com/Kdyby/FormsReplicator/blob/master/docs/en/index.md).
+-----
+Thank you for testing, reporting and contributing.
-----
-Homepage [http://www.kdyby.org](http://www.kdyby.org) and repository [http://github.com/kdyby/FormsReplicator](http://github.com/kdyby/FormsReplicator).
+Homepage [https://www.kdyby.org](https://www.kdyby.org) and repository [https://github.com/Kdyby/FormsReplicator](https://github.com/Kdyby/FormsReplicator).
diff --git a/composer.json b/composer.json
index 819bbea..85551a8 100644
--- a/composer.json
+++ b/composer.json
@@ -10,49 +10,44 @@
"name": "Filip Procházka",
"homepage": "http://filip-prochazka.com",
"email": "filip@prochazka.su"
+ },
+ {
+ "name": "David Šolc",
+ "homepage": "https://solc.dev",
+ "email": "solcik@gmail.com"
}
],
"support": {
- "email": "filip@prochazka.su",
- "issues": "https://github.com/kdyby/replicator/issues"
+ "issues": "https://github.com/Kdyby/FormsReplicator/issues"
},
"require": {
- "nette/forms": "~2.4@dev"
+ "php": ">=7.1",
+ "nette/forms": "^3.0",
+ "nette/utils": "^3.0"
},
"require-dev": {
- "php": ">=5.6",
- "nette/application": "~2.4@dev",
- "nette/bootstrap": "~2.4@dev",
- "nette/caching": "~2.4@dev",
- "nette/component-model": "~2.2@dev",
- "nette/database": "~2.4@dev",
- "nette/deprecated": "~2.4@dev",
- "nette/di": "~2.4@dev",
- "nette/finder": "~2.4@dev",
- "nette/forms": "~2.4@dev",
- "nette/http": "~2.4@dev",
- "nette/mail": "~2.4@dev",
- "nette/neon": "~2.4@dev",
- "nette/php-generator": "~2.4@dev",
- "nette/reflection": "~2.4@dev",
- "nette/robot-loader": "~2.4@dev",
- "nette/safe-stream": "~2.3@dev",
- "nette/security": "~2.4@dev",
- "nette/tokenizer": "~2.2@dev",
- "nette/utils": "~2.4@dev",
- "latte/latte": "~2.4@dev",
- "tracy/tracy": "~2.4@dev",
-
- "nette/tester": "~1.7"
+ "nette/application": "^3.0@rc",
+ "nette/bootstrap": "^3.0@rc",
+ "nette/di": "^3.0@rc",
+ "tracy/tracy": "^2.6",
+ "nette/tester": "^2.2"
},
"autoload": {
- "psr-0": {
- "Kdyby\\Replicator\\": "src/"
+ "psr-4": {
+ "Kdyby\\Replicator\\": "src/Replicator/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "KdybyTests\\Replicator\\": "tests/Replicator/"
}
},
+ "suggest": {
+ "nette/di": "to use ReplicatorExtension[CompilerExtension]"
+ },
"extra": {
"branch-alias": {
- "dev-master": "1.2-dev"
+ "dev-master": "2.0-dev"
}
}
}
diff --git a/src/Kdyby/Replicator/Container.php b/src/Kdyby/Replicator/Container.php
deleted file mode 100644
index 93ab02e..0000000
--- a/src/Kdyby/Replicator/Container.php
+++ /dev/null
@@ -1,562 +0,0 @@
-
- * @author Jan Tvrdík
- *
- * @method \Nette\Application\UI\Form getForm()
- * @property \Nette\Forms\Container $parent
- */
-class Container extends Nette\Forms\Container
-{
-
- /** @var bool */
- public $forceDefault;
-
- /** @var int */
- public $createDefault;
-
- /** @var string */
- public $containerClass = 'Nette\Forms\Container';
-
- /** @var callable */
- protected $factoryCallback;
-
- /** @var boolean */
- private $submittedBy = FALSE;
-
- /** @var array */
- private $created = [];
-
- /** @var \Nette\Http\IRequest */
- private $httpRequest;
-
- /** @var array */
- private $httpPost;
-
-
-
- /**
- * @param callable $factory
- * @param int $createDefault
- * @param bool $forceDefault
- *
- * @throws \Nette\InvalidArgumentException
- */
- public function __construct($factory, $createDefault = 0, $forceDefault = FALSE)
- {
- parent::__construct();
- $this->monitor('Nette\Application\UI\Presenter');
- $this->monitor('Nette\Forms\Form');
-
- try {
- $this->factoryCallback = Callback::closure($factory);
- } catch (Nette\InvalidArgumentException $e) {
- $type = is_object($factory) ? 'instanceof ' . get_class($factory) : gettype($factory);
- throw new Nette\InvalidArgumentException(
- 'Replicator requires callable factory, ' . $type . ' given.', 0, $e
- );
- }
-
- $this->createDefault = (int)$createDefault;
- $this->forceDefault = $forceDefault;
- }
-
-
-
- /**
- * @param callable $factory
- */
- public function setFactory($factory)
- {
- $this->factoryCallback = Callback::closure($factory);
- }
-
-
-
- /**
- * Magical component factory
- *
- * @param \Nette\ComponentModel\IContainer
- */
- protected function attached($obj)
- {
- parent::attached($obj);
-
- if (
- !$obj instanceof Nette\Application\UI\Presenter
- &&
- $this->form instanceof Nette\Application\UI\Form
- ) {
- return;
- }
-
- $this->loadHttpData();
- $this->createDefault();
- }
-
-
-
- /**
- * @param boolean $recursive
- * @return \ArrayIterator|\Nette\Forms\Container[]
- */
- public function getContainers($recursive = FALSE)
- {
- return $this->getComponents($recursive, 'Nette\Forms\Container');
- }
-
-
-
- /**
- * @param boolean $recursive
- * @return \ArrayIterator|Nette\Forms\Controls\SubmitButton[]
- */
- public function getButtons($recursive = FALSE)
- {
- return $this->getComponents($recursive, 'Nette\Forms\ISubmitterControl');
- }
-
-
-
- /**
- * Magical component factory
- *
- * @param string $name
- * @return \Nette\Forms\Container
- */
- protected function createComponent($name)
- {
- $container = $this->createContainer($name);
- $container->currentGroup = $this->currentGroup;
- $this->addComponent($container, $name, $this->getFirstControlName());
-
- Callback::invoke($this->factoryCallback, $container);
-
- return $this->created[$container->name] = $container;
- }
-
-
-
- /**
- * @return string
- */
- private function getFirstControlName()
- {
- $controls = iterator_to_array($this->getComponents(FALSE, 'Nette\Forms\IControl'));
- $firstControl = reset($controls);
- return $firstControl ? $firstControl->name : NULL;
- }
-
-
-
- /**
- * @param string $name
- *
- * @return \Nette\Forms\Container
- */
- protected function createContainer($name)
- {
- $class = $this->containerClass;
- return new $class();
- }
-
-
-
- /**
- * @return boolean
- */
- public function isSubmittedBy()
- {
- if ($this->submittedBy) {
- return TRUE;
- }
-
- foreach ($this->getButtons(TRUE) as $button) {
- if ($button->isSubmittedBy()) {
- return $this->submittedBy = TRUE;
- }
- }
-
- return FALSE;
- }
-
-
-
- /**
- * Create new container
- *
- * @param string|int $name
- *
- * @throws \Nette\InvalidArgumentException
- * @return \Nette\Forms\Container
- */
- public function createOne($name = NULL)
- {
- if ($name === NULL) {
- $names = array_keys(iterator_to_array($this->getContainers()));
- $name = $names ? max($names) + 1 : 0;
- }
-
- // Container is overriden, therefore every request for getComponent($name, FALSE) would return container
- if (isset($this->created[$name])) {
- throw new Nette\InvalidArgumentException("Container with name '$name' already exists.");
- }
-
- return $this[$name];
- }
-
-
-
- /**
- * @param array|\Traversable $values
- * @param bool $erase
- * @param bool $onlyDisabled
- * @return \Nette\Forms\Container|Container
- */
- public function setValues($values, $erase = FALSE, $onlyDisabled = FALSE)
- {
- if (!$this->form->isAnchored() || !$this->form->isSubmitted()) {
- foreach ($values as $name => $value) {
- if ((is_array($value) || $value instanceof \Traversable) && !$this->getComponent($name, FALSE)) {
- $this->createOne($name);
- }
- }
- }
-
- return parent::setValues($values, $erase, $onlyDisabled);
- }
-
-
-
- /**
- * Loads data received from POST
- * @internal
- */
- protected function loadHttpData()
- {
- if (!$this->getForm()->isSubmitted()) {
- return;
- }
-
- foreach ((array) $this->getHttpData() as $name => $value) {
- if ((is_array($value) || $value instanceof \Traversable) && !$this->getComponent($name, FALSE)) {
- $this->createOne($name);
- }
- }
- }
-
-
-
- /**
- * Creates default containers
- * @internal
- */
- protected function createDefault()
- {
- if (!$this->createDefault) {
- return;
- }
-
- if (!$this->getForm()->isSubmitted()) {
- foreach (range(0, $this->createDefault - 1) as $key) {
- $this->createOne($key);
- }
-
- } elseif ($this->forceDefault) {
- while (iterator_count($this->getContainers()) < $this->createDefault) {
- $this->createOne();
- }
- }
- }
-
-
-
- /**
- * @param string $name
- * @return array|null
- */
- protected function getContainerValues($name)
- {
- $post = $this->getHttpData();
- return isset($post[$name]) ? $post[$name] : NULL;
- }
-
-
-
- /**
- * @return mixed|NULL
- */
- private function getHttpData()
- {
- if ($this->httpPost === NULL) {
- $path = explode(self::NAME_SEPARATOR, $this->lookupPath('Nette\Forms\Form'));
- $this->httpPost = Nette\Utils\Arrays::get($this->getForm()->getHttpData(), $path, NULL);
- }
-
- return $this->httpPost;
- }
-
-
-
- /**
- * @internal
- * @param \Nette\Application\Request $request
- * @return Container
- */
- public function setRequest(Nette\Application\Request $request)
- {
- $this->httpRequest = $request;
- return $this;
- }
-
-
-
- /**
- * @return \Nette\Application\Request
- */
- private function getRequest()
- {
- if ($this->httpRequest !== NULL) {
- return $this->httpRequest;
- }
-
- return $this->httpRequest = $this->getForm()->getPresenter()->getRequest();
- }
-
-
-
- /**
- * @param \Nette\Forms\Container $container
- * @param boolean $cleanUpGroups
- *
- * @throws \Nette\InvalidArgumentException
- * @return void
- */
- public function remove(Nette\Forms\Container $container, $cleanUpGroups = FALSE)
- {
- if ($container->parent !== $this) {
- throw new Nette\InvalidArgumentException('Given component ' . $container->name . ' is not children of ' . $this->name . '.');
- }
-
- // to check if form was submitted by this one
- foreach ($container->getComponents(TRUE, 'Nette\Forms\ISubmitterControl') as $button) {
- /** @var \Nette\Forms\Controls\SubmitButton $button */
- if ($button->isSubmittedBy()) {
- $this->submittedBy = TRUE;
- break;
- }
- }
-
- /** @var \Nette\Forms\Controls\BaseControl[] $components */
- $components = $container->getComponents(TRUE);
- $this->removeComponent($container);
-
- // reflection is required to hack form groups
- $groupRefl = Nette\Reflection\ClassType::from('Nette\Forms\ControlGroup');
- $controlsProperty = $groupRefl->getProperty('controls');
- $controlsProperty->setAccessible(TRUE);
-
- // walk groups and clean then from removed components
- $affected = [];
- foreach ($this->getForm()->getGroups() as $group) {
- /** @var \SplObjectStorage $groupControls */
- $groupControls = $controlsProperty->getValue($group);
-
- foreach ($components as $control) {
- if ($groupControls->contains($control)) {
- $groupControls->detach($control);
-
- if (!in_array($group, $affected, TRUE)) {
- $affected[] = $group;
- }
- }
- }
- }
-
- // remove affected & empty groups
- if ($cleanUpGroups && $affected) {
- foreach ($this->getForm()->getComponents(FALSE, 'Nette\Forms\Container') as $container) {
- if ($index = array_search($container->currentGroup, $affected, TRUE)) {
- unset($affected[$index]);
- }
- }
-
- /** @var \Nette\Forms\ControlGroup[] $affected */
- foreach ($affected as $group) {
- if (!$group->getControls() && in_array($group, $this->getForm()->getGroups(), TRUE)) {
- $this->getForm()->removeGroup($group);
- }
- }
- }
- }
-
-
-
- /**
- * Counts filled values, filtered by given names
- *
- * @param array $components
- * @param array $subComponents
- * @return int
- */
- public function countFilledWithout(array $components = [], array $subComponents = [])
- {
- $httpData = array_diff_key((array)$this->getHttpData(), array_flip($components));
-
- if (!$httpData) {
- return 0;
- }
-
- $rows = [];
- $subComponents = array_flip($subComponents);
- foreach ($httpData as $item) {
- $filter = function ($value) use (&$filter) {
- if (is_array($value)) {
- return count(array_filter($value, $filter)) > 0;
- }
- return strlen($value);
- };
- $rows[] = array_filter(array_diff_key($item, $subComponents), $filter) ?: FALSE;
- }
-
- return count(array_filter($rows));
- }
-
-
-
- /**
- * @param array $exceptChildren
- * @return bool
- */
- public function isAllFilled(array $exceptChildren = [])
- {
- $components = [];
- foreach ($this->getComponents(FALSE, 'Nette\Forms\IControl') as $control) {
- /** @var \Nette\Forms\Controls\BaseControl $control */
- $components[] = $control->getName();
- }
-
- foreach ($this->getContainers() as $container) {
- foreach ($container->getComponents(TRUE, 'Nette\Forms\ISubmitterControl') as $button) {
- /** @var \Nette\Forms\Controls\SubmitButton $button */
- $exceptChildren[] = $button->getName();
- }
- }
-
- $filled = $this->countFilledWithout($components, array_unique($exceptChildren));
- return $filled === iterator_count($this->getContainers());
- }
-
-
-
- /**
- * @param $name
- * @return \Nette\Forms\Container
- */
- public function addContainer($name)
- {
- return $this[$name] = new Nette\Forms\Container();
- }
-
-
-
- /**
- * @param \Nette\ComponentModel\IComponent $component
- * @param $name
- * @param null $insertBefore
- * @return \Nette\ComponentModel\Container|\Nette\Forms\Container
- */
- public function addComponent(Nette\ComponentModel\IComponent $component, $name, $insertBefore = NULL)
- {
- $group = $this->currentGroup;
- $this->currentGroup = NULL;
- parent::addComponent($component, $name, $insertBefore);
- $this->currentGroup = $group;
- return $this;
- }
-
-
-
- /**
- * @var bool
- */
- private static $registered = FALSE;
-
- /**
- * @param string $methodName
- * @return void
- */
- public static function register($methodName = 'addDynamic')
- {
- if (self::$registered) {
- Nette\Utils\ObjectMixin::setExtensionMethod(Nette\Forms\Container::class, self::$registered, function () {
- throw new Nette\MemberAccessException;
- });
- }
-
- Nette\Utils\ObjectMixin::setExtensionMethod(Nette\Forms\Container::class, $methodName, function (Nette\Forms\Container $_this, $name, $factory, $createDefault = 0, $forceDefault = FALSE) {
- $control = new Container($factory, $createDefault, $forceDefault);
- $control->currentGroup = $_this->currentGroup;
- return $_this[$name] = $control;
- });
-
- if (self::$registered) {
- return;
- }
-
- Nette\Utils\ObjectMixin::setExtensionMethod(SubmitButton::class, 'addRemoveOnClick', function (SubmitButton $_this, $callback = NULL) {
- $_this->setValidationScope(FALSE);
- $_this->onClick[] = function (SubmitButton $button) use ($callback) {
- $replicator = $button->lookup(__NAMESPACE__ . '\Container');
- /** @var Container $replicator */
- if (is_callable($callback)) {
- Callback::invoke($callback, $replicator, $button->parent);
- }
- if ($form = $button->getForm(FALSE)) {
- $form->onSuccess = [];
- }
- $replicator->remove($button->parent);
- };
- return $_this;
- });
-
- Nette\Utils\ObjectMixin::setExtensionMethod(SubmitButton::class, 'addCreateOnClick', function (SubmitButton $_this, $allowEmpty = FALSE, $callback = NULL) {
- $_this->onClick[] = function (SubmitButton $button) use ($allowEmpty, $callback) {
- $replicator = $button->lookup(__NAMESPACE__ . '\Container');
- /** @var Container $replicator */
- if (!is_bool($allowEmpty)) {
- $callback = Callback::closure($allowEmpty);
- $allowEmpty = FALSE;
- }
- if ($allowEmpty === TRUE || $replicator->isAllFilled() === TRUE) {
- $newContainer = $replicator->createOne();
- if (is_callable($callback)) {
- Callback::invoke($callback, $replicator, $newContainer);
- }
- }
- $button->getForm()->onSuccess = [];
- };
- return $_this;
- });
-
- self::$registered = $methodName;
- }
-
-}
diff --git a/src/Replicator/Container.php b/src/Replicator/Container.php
new file mode 100644
index 0000000..ac46c78
--- /dev/null
+++ b/src/Replicator/Container.php
@@ -0,0 +1,462 @@
+
+ * @author Jan Tvrdík
+ *
+ * @method Nette\Application\UI\Form getForm()
+ * @property Nette\Forms\Container $parent
+ */
+class Container extends Nette\Forms\Container
+{
+
+ /** @var bool */
+ public $forceDefault;
+
+ /** @var int */
+ public $createDefault;
+
+ /** @var string */
+ public $containerClass = Nette\Forms\Container::class;
+
+ /** @var callable */
+ protected $factoryCallback;
+
+ /** @var boolean */
+ private $submittedBy = FALSE;
+
+ /** @var array */
+ private $created = [];
+
+ /** @var array */
+ private $httpPost;
+
+
+ /**
+ * @throws Nette\InvalidArgumentException
+ */
+ public function __construct(callable $factory, int $createDefault = 0, bool $forceDefault = FALSE)
+ {
+ $this->monitor(Nette\Application\UI\Presenter::class);
+ $this->monitor(Nette\Forms\Form::class);
+
+ try {
+ $this->factoryCallback = Closure::fromCallable($factory);
+ } catch (Nette\InvalidArgumentException $e) {
+ $type = is_object($factory) ? 'instanceof ' . get_class($factory) : gettype($factory);
+ throw new Nette\InvalidArgumentException(
+ 'Replicator requires callable factory, ' . $type . ' given.', 0, $e
+ );
+ }
+
+ $this->createDefault = $createDefault;
+ $this->forceDefault = $forceDefault;
+ }
+
+
+ public function setFactory(callable $factory): void
+ {
+ $this->factoryCallback = Closure::fromCallable($factory);
+ }
+
+
+ /**
+ * Magical component factory
+ */
+ protected function attached(Nette\ComponentModel\IComponent $obj): void
+ {
+ parent::attached($obj);
+
+ if (
+ !$obj instanceof Nette\Application\UI\Presenter
+ &&
+ $this->form instanceof Nette\Application\UI\Form
+ ) {
+ return;
+ }
+
+ $this->loadHttpData();
+ $this->createDefault();
+ }
+
+
+ /**
+ * @return Iterator|Nette\Forms\Container[]
+ */
+ public function getContainers(bool $recursive = FALSE): Iterator
+ {
+ return $this->getComponents($recursive, \Nette\Forms\Container::class);
+ }
+
+
+ /**
+ * @return Iterator|Nette\Forms\Controls\SubmitButton[]
+ */
+ public function getButtons(bool $recursive = FALSE): Iterator
+ {
+ return $this->getComponents($recursive, Nette\Forms\ISubmitterControl::class);
+ }
+
+
+ /**
+ * Magical component factory
+ *
+ * @return Nette\Forms\Container
+ */
+ protected function createComponent(string $name): ?Nette\ComponentModel\IComponent
+ {
+ $container = $this->createContainer();
+ $container->currentGroup = $this->currentGroup;
+ $this->addComponent($container, $name, $this->getFirstControlName());
+
+ ($this->factoryCallback)($container);
+
+ return $this->created[$container->name] = $container;
+ }
+
+
+ private function getFirstControlName(): ?string
+ {
+ $controls = iterator_to_array($this->getComponents(FALSE, Nette\Forms\IControl::class));
+ $firstControl = reset($controls);
+
+ return $firstControl ? $firstControl->name : NULL;
+ }
+
+
+ protected function createContainer(): Nette\Forms\Container
+ {
+ $class = $this->containerClass;
+
+ return new $class();
+ }
+
+
+ public function isSubmittedBy(): bool
+ {
+ if ($this->submittedBy) {
+ return TRUE;
+ }
+
+ foreach ($this->getButtons(TRUE) as $button) {
+ if ($button->isSubmittedBy()) {
+ return $this->submittedBy = TRUE;
+ }
+ }
+
+ return FALSE;
+ }
+
+
+ /**
+ * @throws Nette\InvalidArgumentException
+ */
+ public function createOne(?string $name = NULL): Nette\Forms\Container
+ {
+ if ($name === NULL) {
+ $names = array_keys(iterator_to_array($this->getContainers()));
+ $name = $names ? max($names) + 1 : 0;
+ }
+
+ // Container is overriden, therefore every request for getComponent($name, FALSE) would return container
+ if (isset($this->created[$name])) {
+ throw new Nette\InvalidArgumentException("Container with name '$name' already exists.");
+ }
+
+ return $this[$name];
+ }
+
+
+ /**
+ * @param array|Traversable $values
+ *
+ * @return Nette\Forms\Container|Container
+ */
+ public function setValues($values, bool $erase = FALSE, bool $onlyDisabled = FALSE)
+ {
+ if (!$this->form->isAnchored() || !$this->form->isSubmitted()) {
+ foreach ($values as $name => $value) {
+ if ((is_array($value) || $value instanceof Traversable) && !$this->getComponent($name, FALSE)) {
+ $this->createOne($name);
+ }
+ }
+ }
+
+ return parent::setValues($values, $erase, $onlyDisabled);
+ }
+
+
+ /**
+ * @internal
+ */
+ protected function loadHttpData(): void
+ {
+ if (!$this->getForm()->isSubmitted()) {
+ return;
+ }
+
+ foreach ((array) $this->getHttpData() as $name => $value) {
+ if ((is_array($value) || $value instanceof Traversable) && !$this->getComponent($name, FALSE)) {
+ $this->createOne($name);
+ }
+ }
+ }
+
+
+ /**
+ * @internal
+ */
+ protected function createDefault(): void
+ {
+ if (!$this->createDefault) {
+ return;
+ }
+
+ if (!$this->getForm()->isSubmitted()) {
+ foreach (range(0, $this->createDefault - 1) as $key) {
+ $this->createOne($key);
+ }
+
+ } elseif ($this->forceDefault) {
+ while (iterator_count($this->getContainers()) < $this->createDefault) {
+ $this->createOne();
+ }
+ }
+ }
+
+
+ /**
+ * @return mixed|NULL
+ */
+ private function getHttpData()
+ {
+ if ($this->httpPost === NULL) {
+ $path = explode(self::NAME_SEPARATOR, $this->lookupPath(Nette\Forms\Form::class));
+ $this->httpPost = Nette\Utils\Arrays::get($this->getForm()->getHttpData(), $path, NULL);
+ }
+
+ return $this->httpPost;
+ }
+
+
+ /**
+ * @throws Nette\InvalidArgumentException
+ */
+ public function remove(Nette\ComponentModel\Container $container, bool $cleanUpGroups = FALSE): void
+ {
+ if ($container->parent !== $this) {
+ throw new Nette\InvalidArgumentException('Given component ' . $container->name . ' is not children of ' . $this->name . '.');
+ }
+
+ // to check if form was submitted by this one
+ foreach ($container->getComponents(TRUE, Nette\Forms\ISubmitterControl::class) as $button) {
+ /** @var Nette\Forms\Controls\SubmitButton $button */
+ if ($button->isSubmittedBy()) {
+ $this->submittedBy = TRUE;
+ break;
+ }
+ }
+
+ /** @var Nette\Forms\Controls\BaseControl[] $components */
+ $components = $container->getComponents(TRUE);
+ $this->removeComponent($container);
+
+ // reflection is required to hack form groups
+ $groupRefl = new ReflectionClass(Nette\Forms\ControlGroup::class);
+ $controlsProperty = $groupRefl->getProperty('controls');
+ $controlsProperty->setAccessible(TRUE);
+
+ // walk groups and clean then from removed components
+ $affected = [];
+ foreach ($this->getForm()->getGroups() as $group) {
+ /** @var SplObjectStorage $groupControls */
+ $groupControls = $controlsProperty->getValue($group);
+
+ foreach ($components as $control) {
+ if ($groupControls->contains($control)) {
+ $groupControls->detach($control);
+
+ if (!in_array($group, $affected, TRUE)) {
+ $affected[] = $group;
+ }
+ }
+ }
+ }
+
+ // remove affected & empty groups
+ if ($cleanUpGroups && $affected) {
+ foreach ($this->getForm()->getComponents(FALSE, Nette\Forms\Container::class) as $cont) {
+ if ($index = array_search($cont->currentGroup, $affected, TRUE)) {
+ unset($affected[$index]);
+ }
+ }
+
+ /** @var Nette\Forms\ControlGroup[] $affected */
+ foreach ($affected as $group) {
+ if (!$group->getControls() && in_array($group, $this->getForm()->getGroups(), TRUE)) {
+ $this->getForm()->removeGroup($group);
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Counts filled values, filtered by given names
+ */
+ public function countFilledWithout(array $components = [], array $subComponents = []): int
+ {
+ $httpData = array_diff_key((array) $this->getHttpData(), array_flip($components));
+
+ if (!$httpData) {
+ return 0;
+ }
+
+ $rows = [];
+ $subComponents = array_flip($subComponents);
+ foreach ($httpData as $item) {
+ $filter = function ($value) use (&$filter) {
+ if (is_array($value)) {
+ return count(array_filter($value, $filter)) > 0;
+ }
+
+ return strlen($value);
+ };
+ $rows[] = array_filter(array_diff_key($item, $subComponents), $filter) ?: FALSE;
+ }
+
+ return count(array_filter($rows));
+ }
+
+
+ public function isAllFilled(array $exceptChildren = []): bool
+ {
+ $components = [];
+ foreach ($this->getComponents(FALSE, Nette\Forms\IControl::class) as $control) {
+ /** @var Nette\Forms\Controls\BaseControl $control */
+ $components[] = $control->getName();
+ }
+
+ foreach ($this->getContainers() as $container) {
+ foreach ($container->getComponents(TRUE, Nette\Forms\ISubmitterControl::class) as $button) {
+ /** @var Nette\Forms\Controls\SubmitButton $button */
+ $exceptChildren[] = $button->getName();
+ }
+ }
+
+ $filled = $this->countFilledWithout($components, array_unique($exceptChildren));
+
+ return $filled === iterator_count($this->getContainers());
+ }
+
+
+ public function addContainer($name): Nette\Forms\Container
+ {
+ return $this[$name] = new Nette\Forms\Container();
+ }
+
+
+ public function addComponent(Nette\ComponentModel\IComponent $component, ?string $name, ?string $insertBefore = NULL): Nette\ComponentModel\IContainer
+ {
+ $group = $this->currentGroup;
+ $this->currentGroup = NULL;
+ parent::addComponent($component, $name, $insertBefore);
+ $this->currentGroup = $group;
+
+ return $this;
+ }
+
+
+ /**
+ * @var bool
+ */
+ private static $registered = FALSE;
+
+
+ public static function register(string $methodName = 'addDynamic'): void
+ {
+ if (self::$registered) {
+ Nette\Forms\Container::extensionMethod(self::$registered, function () {
+ throw new Nette\MemberAccessException;
+ });
+ }
+
+ Nette\Forms\Container::extensionMethod(
+ $methodName,
+ function (Nette\Forms\Container $_this, string $name, callable $factory, int $createDefault = 0, bool $forceDefault = FALSE) {
+ $control = new Container($factory, $createDefault, $forceDefault);
+ $control->currentGroup = $_this->currentGroup;
+
+ return $_this[$name] = $control;
+ }
+ );
+
+ if (self::$registered) {
+ return;
+ }
+
+ Nette\Forms\Controls\SubmitButton::extensionMethod(
+ 'addRemoveOnClick',
+ function (Nette\Forms\Controls\SubmitButton $_this, ?callable $callback = NULL) {
+ $_this->setValidationScope([]);
+ $_this->onClick[] = function (Nette\Forms\Controls\SubmitButton $button) use ($callback) {
+ /** @var Container $replicator */
+ $replicator = $button->lookup(Container::class);
+ if (is_callable($callback)) {
+ $callback($replicator, $button->parent);
+ }
+ if ($form = $button->getForm(FALSE)) {
+ $form->onSuccess = [];
+ }
+ $replicator->remove($button->parent);
+ };
+
+ return $_this;
+ }
+ );
+
+ Nette\Forms\Controls\SubmitButton::extensionMethod(
+ 'addCreateOnClick',
+ function (Nette\Forms\Controls\SubmitButton $_this, bool $allowEmpty = FALSE, ?callable $callback = NULL) {
+ $_this->onClick[] = function (Nette\Forms\Controls\SubmitButton $button) use ($allowEmpty, $callback) {
+ /** @var Container $replicator */
+ $replicator = $button->lookup(Container::class);
+ if (!is_bool($allowEmpty)) {
+ $callback = Closure::fromCallable($allowEmpty);
+ $allowEmpty = FALSE;
+ }
+ if ($allowEmpty === TRUE || $replicator->isAllFilled() === TRUE) {
+ $newContainer = $replicator->createOne();
+ if (is_callable($callback)) {
+ $callback($replicator, $newContainer);
+ }
+ }
+ $button->getForm()->onSuccess = [];
+ };
+
+ return $_this;
+ }
+ );
+
+ self::$registered = $methodName;
+ }
+
+}
diff --git a/src/Kdyby/Replicator/DI/ReplicatorExtension.php b/src/Replicator/DI/ReplicatorExtension.php
similarity index 77%
rename from src/Kdyby/Replicator/DI/ReplicatorExtension.php
rename to src/Replicator/DI/ReplicatorExtension.php
index aaa951a..054d418 100644
--- a/src/Kdyby/Replicator/DI/ReplicatorExtension.php
+++ b/src/Replicator/DI/ReplicatorExtension.php
@@ -10,10 +10,8 @@
namespace Kdyby\Replicator\DI;
-use Kdyby;
+use Kdyby\Replicator\Container;
use Nette;
-use Nette\PhpGenerator as Code;
-
/**
@@ -22,20 +20,16 @@
class ReplicatorExtension extends Nette\DI\CompilerExtension
{
- public function afterCompile(Code\ClassType $class)
+ public function afterCompile(Nette\PhpGenerator\ClassType $class): void
{
parent::afterCompile($class);
$init = $class->getMethod('initialize');
- $init->addBody('Kdyby\Replicator\Container::register();');
+ $init->addBody(Container::class . '::register();');
}
-
- /**
- * @param \Nette\Configurator $configurator
- */
- public static function register(Nette\Configurator $configurator)
+ public static function register(Nette\Configurator $configurator): void
{
$configurator->onCompile[] = function ($config, Nette\DI\Compiler $compiler) {
$compiler->addExtension('formsReplicator', new ReplicatorExtension());
diff --git a/tests/KdybyTests/.gitignore b/tests/.gitignore
old mode 100755
new mode 100644
similarity index 100%
rename from tests/KdybyTests/.gitignore
rename to tests/.gitignore
diff --git a/tests/KdybyTests/Replicator/Container.phpt b/tests/KdybyTests/Replicator/Container.phpt
deleted file mode 100644
index 7060961..0000000
--- a/tests/KdybyTests/Replicator/Container.phpt
+++ /dev/null
@@ -1,280 +0,0 @@
-
- * @package Kdyby\Replicator
- */
-
-namespace KdybyTests\Replicator;
-
-use Kdyby;
-use Kdyby\Replicator\Container;
-use Nette;
-use Nette\Application\Request;
-use Nette\Application\UI;
-use Nette\Forms\Controls;
-use Tester;
-use Tester\Assert;
-
-require_once __DIR__ . '/../bootstrap.php';
-
-
-
-/**
- * @author Filip Procházka
- */
-class ContainerTest extends Tester\TestCase
-{
-
- public function testReplicating()
- {
- $replicator = new Container(function (Nette\Forms\Container $container) {
- $container->addText('name', "Name");
- });
-
- Assert::true($replicator[0]['name'] instanceof Controls\TextInput);
- Assert::true($replicator[2]['name'] instanceof Controls\TextInput);
- Assert::true($replicator[1000]['name'] instanceof Controls\TextInput);
- }
-
-
-
- public function testRendering_attachAfterDefinition()
- {
- $form = new BaseForm();
- $users = $form->addDynamic('users', function (Nette\Forms\Container $user) {
- $user->addText('name');
- }, 1);
- $users->addSubmit('add');
-
- $this->connectForm($form);
-
- // container and submit button
- Assert::same(2, iterator_count($users->getComponents()));
-
- // simulate rendering additional key
- Assert::true($users[2]['name'] instanceof Controls\TextInput);
-
- // 2 containers and submit button
- Assert::same(3, iterator_count($users->getComponents()));
-
- Assert::same(['users' => [
- 0 => ['name' => ''],
- 2 => ['name' => ''],
- ]], $form->getValues(TRUE));
- }
-
-
-
- public function testRendering_attachBeforeDefinition()
- {
- $form = new BaseForm();
-
- $this->connectForm($form);
-
- $users = $form->addDynamic('users', function (Nette\Forms\Container $user) {
- $user->addText('name');
- }, 1);
- $users->addSubmit('add');
-
- // container and submit button
- Assert::same(2, iterator_count($users->getComponents()));
-
- // simulate rendering additional key
- Assert::true($users[2]['name'] instanceof Controls\TextInput);
-
- // 2 containers and submit button
- Assert::same(3, iterator_count($users->getComponents()));
-
- Assert::same(['users' => [
- 0 => ['name' => ''],
- 2 => ['name' => ''],
- ]], $form->getValues(TRUE));
- }
-
-
-
- public function testSubmit_attachAfterDefinition()
- {
- $form = new BaseForm();
- $users = $form->addDynamic('users', function (Nette\Forms\Container $user) {
- $user->addText('name');
- }, 1);
- $users->addSubmit('add');
-
- $this->connectForm($form, [
- 'users' => [
- 0 => ['name' => 'David'],
- 2 => ['name' => 'Holy'],
- 3 => ['name' => 'Rimmer'],
- ],
- 'do' => 'form-submit'
- ]);
-
- // container and submit button
- Assert::same(4, iterator_count($users->getComponents()));
-
- Assert::same(['users' => [
- 0 => ['name' => 'David'],
- 2 => ['name' => 'Holy'],
- 3 => ['name' => 'Rimmer'],
- ]], $form->getValues(TRUE));
- }
-
-
-
- public function testSubmit_attachBeforeDefinition()
- {
- $form = new BaseForm();
-
- $this->connectForm($form, [
- 'users' => [
- 0 => ['name' => 'David'],
- 2 => ['name' => 'Holy'],
- 3 => ['name' => 'Rimmer'],
- ],
- 'do' => 'form-submit'
- ]);
-
- $users = $form->addDynamic('users', function (Nette\Forms\Container $user) {
- $user->addText('name');
- }, 1);
- $users->addSubmit('add');
-
- // container and submit button
- Assert::same(4, iterator_count($users->getComponents()));
-
- Assert::same(['users' => [
- 0 => ['name' => 'David'],
- 2 => ['name' => 'Holy'],
- 3 => ['name' => 'Rimmer'],
- ]], $form->getValues(TRUE));
- }
-
-
-
- public function testSubmit_nestedReplicator_notFilled()
- {
- $form = new BaseForm();
- $this->connectForm($form, [
- 'users' => [0 => ['emails' => [0 => ['email' => '']]]],
- 'do' => 'form-submit',
- ]);
- $users = $form->addDynamic('users', function (Nette\Forms\Container $user) {
- $user->addDynamic('emails', function (Nette\Forms\Container $email) {
- $email->addText('email');
- $email->addText('note');
- });
- });
- $users->addSubmit('add')->addCreateOnClick();
- Assert::false($users->isAllFilled());
- }
-
-
-
- public function testSubmit_nestedReplicator_filled()
- {
- $form = new BaseForm();
- $this->connectForm($form, [
- 'users' => [0 => ['emails' => [0 => ['email' => 'foo']]]],
- 'do' => 'form-submit',
- ]);
- $users = $form->addDynamic('users', function (Nette\Forms\Container $user) {
- $user->addDynamic('emails', function (Nette\Forms\Container $email) {
- $email->addText('email');
- $email->addText('note');
- });
- });
- $users->addSubmit('add')->addCreateOnClick();
- Assert::true($users->isAllFilled());
- }
-
-
-
- protected function connectForm(UI\Form $form, array $post = [])
- {
- $container = $this->createContainer();
-
- /** @var MockPresenter $presenter */
- $presenter = $container->createInstance('KdybyTests\Replicator\MockPresenter', ['form' => $form]);
- $container->callInjects($presenter);
- $presenter->run(new Request('Mock', $post ? 'POST' : 'GET', ['action' => 'default'], $post));
-
- $presenter['form']; // connect form
-
- return $presenter;
- }
-
-
-
- /**
- * @return \SystemContainer|\Nette\DI\Container
- */
- protected function createContainer()
- {
- $config = new Nette\Configurator();
- $config->setTempDirectory(TEMP_DIR);
- Kdyby\Replicator\DI\ReplicatorExtension::register($config);
-
- return $config->createContainer();
- }
-
-
- // TODO: add tests using standalone \Nette\Forms\Form and not the UI\Form.
- // https://github.com/Kdyby/Replicator/issues/40
- // The Replicator can't be used with standalone \Nette\Forms\Form (so without the UI\Form).
- // Problem is that attached is not triggered, so values from Request are not populated to the container.
-
-}
-
-
-
-class BaseForm extends UI\Form
-{
-
- /**
- * @param string $name
- * @param callable $factory
- * @param int $createDefault
- * @param bool $forceDefault
- * @return Container
- */
- public function addDynamic($name, $factory, $createDefault = 0, $forceDefault = FALSE)
- {
- $control = new Container($factory, $createDefault, $forceDefault);
- $control->currentGroup = $this->currentGroup;
- return $this[$name] = $control;
- }
-
-}
-
-class MockPresenter extends Nette\Application\UI\Presenter
-{
-
- /**
- * @var UI\Form
- */
- private $form;
-
- public function __construct(UI\Form $form)
- {
- $this->form = $form;
- }
-
- protected function beforeRender()
- {
- $this->terminate();
- }
-
- protected function createComponentForm()
- {
- return $this->form;
- }
-
-}
-
-
-\run(new ContainerTest());
diff --git a/tests/KdybyTests/Replicator/Extension.phpt b/tests/KdybyTests/Replicator/Extension.phpt
deleted file mode 100644
index 9fc15b7..0000000
--- a/tests/KdybyTests/Replicator/Extension.phpt
+++ /dev/null
@@ -1,60 +0,0 @@
-
- * @package Kdyby\Replicator
- */
-
-namespace KdybyTests\Replicator;
-
-use Kdyby;
-use Nette;
-use Tester;
-use Tester\Assert;
-
-require_once __DIR__ . '/../bootstrap.php';
-
-
-
-/**
- * @author Filip Procházka
- */
-class ExtensionTest extends Tester\TestCase
-{
-
- protected function setUp()
- {
- parent::setUp();
- Tester\Environment::$checkAssertions = FALSE;
- }
-
-
-
- /**
- * @return \SystemContainer|\Nette\DI\Container
- */
- protected function createContainer()
- {
- $config = new Nette\Configurator();
- $config->setTempDirectory(TEMP_DIR);
- Kdyby\Replicator\DI\ReplicatorExtension::register($config);
-
- return $config->createContainer();
- }
-
-
-
- public function testExtensionMethodIsRegistered()
- {
- $this->createContainer(); // initialize
-
- $form = new Nette\Forms\Form();
- $form->addDynamic('people', function () {});
- }
-
-}
-
-\run(new ExtensionTest());
diff --git a/tests/Replicator/ContainerTest.php b/tests/Replicator/ContainerTest.php
new file mode 100644
index 0000000..fec4fff
--- /dev/null
+++ b/tests/Replicator/ContainerTest.php
@@ -0,0 +1,326 @@
+
+ * @package Kdyby\Replicator
+ */
+
+namespace KdybyTests\Replicator;
+
+use Kdyby\Replicator\Container;
+use Nette;
+use Tester\Assert;
+use Tester\TestCase;
+
+
+require_once __DIR__ . '/../bootstrap.php';
+
+
+/**
+ * @author Filip Procházka
+ */
+class ContainerTest extends TestCase
+{
+
+ public function testReplicating()
+ {
+ $replicator = new Container(function (Nette\Forms\Container $container) {
+ $container->addText('name', 'Name');
+ });
+
+ Assert::type(Nette\Forms\Controls\TextInput::class, $replicator[0]['name']);
+ Assert::type(Nette\Forms\Controls\TextInput::class, $replicator[2]['name']);
+ Assert::type(Nette\Forms\Controls\TextInput::class, $replicator[1000]['name']);
+ }
+
+
+ public function testRenderingAttachAfterDefinition(): void
+ {
+ $form = new BaseForm();
+ $users = $form->addDynamic('users', function (Nette\Forms\Container $user) {
+ $user->addText('name');
+ }, 1);
+ $users->addSubmit('add');
+
+ $this->connectForm($form);
+
+ // container and submit button
+ Assert::same(2, iterator_count($users->getComponents()));
+
+ // simulate rendering additional key
+ Assert::type(Nette\Forms\Controls\TextInput::class, $users[2]['name']);
+
+ // 2 containers and submit button
+ Assert::same(3, iterator_count($users->getComponents()));
+
+ Assert::equal(Nette\Utils\ArrayHash::from([
+ 'users' => [
+ 0 => ['name' => ''],
+ 2 => ['name' => ''],
+ ],
+ ]), $form->getValues());
+ }
+
+
+ public function testRenderingAttachBeforeDefinition(): void
+ {
+ $form = new BaseForm();
+
+ $this->connectForm($form);
+
+ $users = $form->addDynamic('users', function (Nette\Forms\Container $user) {
+ $user->addText('name');
+ }, 1);
+ $users->addSubmit('add');
+
+ // container and submit button
+ Assert::same(2, iterator_count($users->getComponents()));
+
+ // simulate rendering additional key
+ Assert::type(Nette\Forms\Controls\TextInput::class, $users[2]['name']);
+
+ // 2 containers and submit button
+ Assert::same(3, iterator_count($users->getComponents()));
+
+ Assert::equal(Nette\Utils\ArrayHash::from([
+ 'users' => [
+ '0' => ['name' => ''],
+ '2' => ['name' => ''],
+ ],
+ ]), $form->getValues());
+ }
+
+
+ public function testSubmitAttachAfterDefinition(): void
+ {
+ $form = new BaseForm();
+ $users = $form->addDynamic('users', function (Nette\Forms\Container $user) {
+ $user->addText('name');
+ }, 1);
+ $users->addSubmit('add');
+
+ $this->connectForm($form, [
+ 'users' => [
+ 0 => ['name' => 'David'],
+ 2 => ['name' => 'Holy'],
+ 3 => ['name' => 'Rimmer'],
+ ],
+ '_do' => 'form-submit',
+ ]);
+
+ // container and submit button
+ Assert::same(4, iterator_count($users->getComponents()));
+
+ Assert::equal(Nette\Utils\ArrayHash::from([
+ 'users' => [
+ 0 => ['name' => 'David'],
+ 2 => ['name' => 'Holy'],
+ 3 => ['name' => 'Rimmer'],
+ ],
+ ]), $form->getValues());
+ }
+
+
+ public function testSubmitAttachBeforeDefinition(): void
+ {
+ $form = new BaseForm();
+
+ $this->connectForm($form, [
+ 'users' => [
+ 0 => ['name' => 'David'],
+ 2 => ['name' => 'Holy'],
+ 3 => ['name' => 'Rimmer'],
+ ],
+ '_do' => 'form-submit',
+ ]);
+
+ $users = $form->addDynamic('users', function (Nette\Forms\Container $user) {
+ $user->addText('name');
+ }, 1);
+ $users->addSubmit('add');
+
+ // container and submit button
+ Assert::same(4, iterator_count($users->getComponents()));
+
+ Assert::equal(Nette\Utils\ArrayHash::from([
+ 'users' => [
+ 0 => ['name' => 'David'],
+ 2 => ['name' => 'Holy'],
+ 3 => ['name' => 'Rimmer'],
+ ],
+ ]), $form->getValues());
+ }
+
+
+ public function testSubmitNestedReplicatorNotFilled(): void
+ {
+ $form = new BaseForm();
+ $this->connectForm($form, [
+ 'users' => [
+ 0 => ['emails' => [0 => ['email' => '']]],
+ ],
+ '_do' => 'form-submit',
+ ]);
+ $users = $form->addDynamic('users', function (Nette\Forms\Container $user) {
+ $user->addDynamic('emails', function (Nette\Forms\Container $email) {
+ $email->addText('email');
+ $email->addText('note');
+ });
+ });
+ $users->addSubmit('add')->addCreateOnClick();
+ Assert::false($users->isAllFilled());
+ }
+
+
+ public function testSubmitNestedReplicatorFilled(): void
+ {
+ $form = new BaseForm();
+ $this->connectForm($form, [
+ 'users' => [
+ 0 => ['emails' => [0 => ['email' => 'foo', 'note' => 'aa']]],
+ ],
+ '_do' => 'form-submit',
+ ]);
+ $users = $form->addDynamic('users', function (Nette\Forms\Container $user) {
+ $user->addDynamic('emails', function (Nette\Forms\Container $email) {
+ $email->addText('email');
+ $email->addText('note');
+ });
+ });
+ $users->addSubmit('add')->addCreateOnClick();
+ Assert::true($users->isAllFilled());
+ }
+
+
+ public function testAddContainer(): void
+ {
+ $form = new BaseForm();
+ $this->connectForm($form, [
+ 'users' => [
+ 0 => ['emails' => [0 => ['email' => 'foo']]],
+ 2 => ['emails' => [0 => ['email' => 'bar']]],
+ ],
+ '_do' => 'form-submit',
+ ]);
+ $users = $form->addDynamic('users', function (Nette\Forms\Container $user) {
+ $user->addDynamic('emails', function (Nette\Forms\Container $email) {
+ $email->addText('email');
+ });
+ });
+ /** @var Nette\Forms\Controls\SubmitButton $submit */
+ $submit = $users->addSubmit('add')->addCreateOnClick();
+
+ Assert::same(2, iterator_count($users->getContainers()));
+
+ $submit->click();
+
+ Assert::same(3, iterator_count($users->getContainers()));
+ }
+
+
+ public function testRemoveContainer(): void
+ {
+ $form = new BaseForm();
+ $this->connectForm($form, [
+ 'users' => [
+ 0 => ['emails' => [0 => ['email' => 'foo']]],
+ 2 => ['emails' => [0 => ['email' => 'bar']]],
+ ],
+ '_do' => 'form-submit',
+ ]);
+ $users = $form->addDynamic('users', function (Nette\Forms\Container $user) {
+ $user->addDynamic('emails', function (Nette\Forms\Container $email) {
+ $email->addText('email');
+ });
+ $user->addSubmit('remove')->addRemoveOnClick();
+ });
+
+ Assert::same(2, iterator_count($users->getContainers()));
+
+ /** @var Container $section */
+ $section = $users['2'];
+ $users->remove($section);
+
+ Assert::same(1, iterator_count($users->getContainers()));
+
+ /** @var Nette\Forms\Controls\SubmitButton $sectionRemoveButton */
+ $sectionRemoveButton = $users['0']['remove'];
+ $sectionRemoveButton->click();
+
+ Assert::same(0, iterator_count($users->getContainers()));
+ }
+
+
+ private function connectForm(Nette\Application\UI\Form $form, array $post = []): MockPresenter
+ {
+ $container = Helper::createContainer();
+
+ /** @var MockPresenter $presenter */
+ $presenter = $container->createInstance(MockPresenter::class, ['form' => $form]);
+ $container->callInjects($presenter);
+ $presenter->run(new Nette\Application\Request('Mock', $post ? 'POST' : 'GET', ['action' => 'default'], $post));
+
+ $presenter->getComponent('form'); // connect form
+
+ return $presenter;
+ }
+
+ // TODO: add tests using standalone \Nette\Forms\Form and not the UI\Form.
+ // https://github.com/Kdyby/Replicator/issues/40
+ // The Replicator can't be used with standalone \Nette\Forms\Form (so without the UI\Form).
+ // Problem is that attached is not triggered, so values from Request are not populated to the container.
+
+}
+
+
+class BaseForm extends Nette\Application\UI\Form
+{
+
+ public function addDynamic(string $name, callable $factory, int $createDefault = 0, bool $forceDefault = FALSE): Container
+ {
+ $control = new Container($factory, $createDefault, $forceDefault);
+ $control->currentGroup = $this->currentGroup;
+
+ return $this[$name] = $control;
+ }
+
+}
+
+
+class MockPresenter extends Nette\Application\UI\Presenter
+{
+
+ /**
+ * @var Nette\Application\UI\Form
+ */
+ private $form;
+
+
+ public function __construct(Nette\Application\UI\Form $form)
+ {
+ parent::__construct();
+ $this->form = $form;
+ }
+
+
+ /**
+ * @throws Nette\Application\AbortException
+ */
+ protected function beforeRender(): void
+ {
+ $this->terminate();
+ }
+
+
+ protected function createComponentForm(): Nette\Application\UI\Form
+ {
+ return $this->form;
+ }
+
+}
+
+
+(new ContainerTest())->run();
diff --git a/tests/Replicator/ExtensionTest.php b/tests/Replicator/ExtensionTest.php
new file mode 100644
index 0000000..98b5ad5
--- /dev/null
+++ b/tests/Replicator/ExtensionTest.php
@@ -0,0 +1,46 @@
+
+ * @package Kdyby\Replicator
+ */
+
+namespace KdybyTests\Replicator;
+
+use Nette;
+use Tester\Environment;
+use Tester\TestCase;
+
+
+require_once __DIR__ . '/../bootstrap.php';
+
+
+/**
+ * @author Filip Procházka
+ */
+class ExtensionTest extends TestCase
+{
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ Environment::$checkAssertions = FALSE;
+ }
+
+
+ public function testExtensionMethodIsRegistered(): void
+ {
+ Helper::createContainer();
+
+ $form = new Nette\Forms\Form();
+ $form->addDynamic('people', function () {
+ });
+ }
+
+}
+
+
+(new ExtensionTest())->run();
diff --git a/tests/Replicator/Helper.php b/tests/Replicator/Helper.php
new file mode 100644
index 0000000..c73dbf0
--- /dev/null
+++ b/tests/Replicator/Helper.php
@@ -0,0 +1,23 @@
+setTempDirectory(TEMP_DIR);
+ $config->addConfig(__DIR__ . '/config.neon');
+ ReplicatorExtension::register($config);
+
+ return $config->createContainer();
+ }
+
+}
diff --git a/tests/Replicator/config.neon b/tests/Replicator/config.neon
new file mode 100644
index 0000000..e71372e
--- /dev/null
+++ b/tests/Replicator/config.neon
@@ -0,0 +1,3 @@
+application:
+ scanDirs: false
+ scanComposer: false
diff --git a/tests/KdybyTests/bootstrap.php b/tests/bootstrap.php
old mode 100755
new mode 100644
similarity index 59%
rename from tests/KdybyTests/bootstrap.php
rename to tests/bootstrap.php
index 405399a..092d811
--- a/tests/KdybyTests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -8,30 +8,30 @@
* For the full copyright and license information, please view the file license.md that was distributed with this source code.
*/
-if (@!include __DIR__ . '/../../vendor/autoload.php') {
+if (@!include __DIR__ . '/../vendor/autoload.php') {
echo 'Install Nette Tester using `composer update --dev`';
exit(1);
}
// configure environment
Tester\Environment::setup();
-class_alias('Tester\Assert', 'Assert');
date_default_timezone_set('Europe/Prague');
// create temporary directory
-define('TEMP_DIR', __DIR__ . '/../tmp/' . (isset($_SERVER['argv']) ? md5(serialize($_SERVER['argv'])) : getmypid()));
+define('TEMP_DIR', __DIR__ . '/tmp/' . (isset($_SERVER['argv']) ? md5(serialize($_SERVER['argv'])) : getmypid()));
Tester\Helpers::purge(TEMP_DIR);
$_SERVER = array_intersect_key($_SERVER, array_flip([
- 'PHP_SELF', 'SCRIPT_NAME', 'SERVER_ADDR', 'SERVER_SOFTWARE', 'HTTP_HOST', 'DOCUMENT_ROOT', 'OS', 'argc', 'argv']));
+ 'PHP_SELF',
+ 'SCRIPT_NAME',
+ 'SERVER_ADDR',
+ 'SERVER_SOFTWARE',
+ 'HTTP_HOST',
+ 'DOCUMENT_ROOT',
+ 'OS',
+ 'argc',
+ 'argv',
+]));
$_SERVER['REQUEST_TIME'] = 1234567890;
$_ENV = $_GET = $_POST = [];
-
-function id($val) {
- return $val;
-}
-
-function run(Tester\TestCase $testCase) {
- $testCase->run();
-}
diff --git a/tests/composer-nette-2.2.json b/tests/composer-nette-2.2.json
deleted file mode 100644
index 8c52abf..0000000
--- a/tests/composer-nette-2.2.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "name": "kdyby/forms-replicator",
- "type": "library",
- "description": "Nette forms container replicator aka addDynamic",
- "keywords": ["nette", "kdyby", "forms", "replicator", "addDynamic"],
- "homepage": "http://kdyby.org",
- "license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"],
- "authors": [
- {
- "name": "Filip Procházka",
- "homepage": "http://filip-prochazka.com",
- "email": "filip@prochazka.su"
- }
- ],
- "support": {
- "email": "filip@prochazka.su",
- "issues": "https://github.com/kdyby/replicator/issues"
- },
- "require": {
- "nette/forms": "2.2.*"
- },
- "require-dev": {
- "nette/nette": "2.2.*",
- "nette/bootstrap": "2.2.*",
-
- "nette/tester": "@dev",
- "jakub-onderka/php-parallel-lint": "~0.6"
- },
- "autoload": {
- "psr-0": {
- "Kdyby\\Replicator\\": "src/"
- }
- }
-}
diff --git a/tests/conventions.txt b/tests/conventions.txt
deleted file mode 100644
index bf471e6..0000000
--- a/tests/conventions.txt
+++ /dev/null
@@ -1,26 +0,0 @@
-Test case file name
-===================
-
-Nette\....phpt
-
-Nette\Debug.phpt - tests for a class's basic behaviour
-Nette\Debug.fireLog().phpt - tests for a method's basic behaviour
-Nette\Debug.fireLog().inc - common code for more test cases
-Nette\Debug.fireLog().expect - expected raw output
-Nette\Debug.fireLog().area.phpt - tests for a specified area of class/method
-
-- areas: basic, error, bug#123
-- numbers have three digits
-
-
-Test case phpDoc
-================
-
-/**
- * Test: some test name
- *
- * @author John Doe
- * @phpVersion < 5.3 default operator is >=
- * @skip some reason why test is skipped
- * @phpIni short_open_tag=on
- */
diff --git a/tests/php.ini-unix b/tests/php.ini-unix
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/prepare-composer.php b/tests/prepare-composer.php
deleted file mode 100644
index ddc9590..0000000
--- a/tests/prepare-composer.php
+++ /dev/null
@@ -1,16 +0,0 @@
-