Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added FormMacros, DynamicContainer

Createc README
  • Loading branch information...
commit fc9f2d9121f00a8df3ab3c802bb21a0fc30d490e 1 parent e87f5f4
@foowie authored
Showing with 541 additions and 12 deletions.
  1. +211 −0 DynamicContainer.php
  2. +34 −12 DynamicContainerCore.php
  3. +236 −0 FormMacros.php
  4. +60 −0 README
View
211 DynamicContainer.php
@@ -0,0 +1,211 @@
+<?php
+
+/**
+ * Nette addon that allows dynamically add/remove set of items in Form (FormContainer)
+ *
+ * @author Daniel Robenek
+ * @license MIT
+ */
+
+namespace Addons\Forms;
+
+// <editor-fold defaultstate="collapsed" desc="use">
+
+use \InvalidArgumentException;
+use \InvalidStateException;
+use Nette\Callback;
+use Nette\Forms\FormContainer;
+use Nette\Application\Presenter;
+use \LogicException;
+use Nette\ComponentContainer;
+use Nette\IComponent;
+use Nette\Forms\SubmitButton;
+
+// </editor-fold>
+
+class DynamicContainer extends DynamicContainerCore {
+
+ // <editor-fold defaultstate="collapsed" desc="variables">
+
+ /** array(add_delete_button?, label, name) */
+ protected $deleteButtonOptions = array(true, "Odebrat", "deleteButton");
+ /** array(add_add_button?, label, name) */
+ protected $addButtonOptions = array(true, "Nový záznam", "addButton");
+ /** @var bool Add ajaxClass to buttons? */
+ protected $useAjax = true;
+ /** @var string Ajax html class */
+ protected $ajaxClass = "ajax";
+
+ // </editor-fold>
+
+ // <editor-fold defaultstate="collapsed" desc="constructor">
+
+ /**
+ * Create FormContainer
+ * @param IComponentContainer $parent
+ * @param string $name
+ */
+ public function __construct(IComponentContainer $parent = null, $name = null) {
+ parent::__construct($parent, $name, null);
+ }
+
+ // </editor-fold>
+
+ // <editor-fold defaultstate="collapsed" desc="getters / setters">
+
+ /**
+ * Set delete button options
+ * @param bool $enable Create delete button?
+ * @param string $label Button label
+ * @param string $name Button name
+ * @return DynamicContainer
+ */
+ public function setDeleteButton($enable = true, $label = "Odebrat", $name = "deleteButton") {
+ if($this->getRowCount() > 0)
+ throw new InvalidStateException("Button options have to be set before factory is set and form is attached !");
+ $this->deleteButtonOptions = array($enable, $label, $name);
+ return $this;
+ }
+
+ /**
+ * Set add button options
+ * @param bool $enable Create add button?
+ * @param string $label Button label
+ * @param string $name Button name
+ * @return DynamicContainer
+ */
+ public function setAddButton($enable = true, $label = "Nový záznam", $name = "addButton") {
+ if($this->getRowCount() > 0)
+ throw new InvalidStateException("Button options have to be set before factory is set and form is attached !");
+ $this->addButtonOptions = array($enable, $label, $name);
+ if ($enable) {
+ if(!$this->lookup("Nette\Forms\FormContainer", false) === null)
+ $this->attachAddButton();
+ }
+ return $this;
+ }
+
+ /**
+ * Use ajax / ajax class
+ * @param bool $useAjax
+ * @param string $ajaxClass
+ * @return DynamicContainer
+ */
+ public function setAjax($useAjax = true, $ajaxClass = "ajax") {
+ $this->useAjax = $useAjax;
+ $this->ajaxClass = $ajaxClass;
+ return $this;
+ }
+
+ /**
+ * Use ajax class to buttons?
+ * @param bool $useAjax
+ * @return DynamicContainer
+ */
+ public function setUseAjax($useAjax) {
+ $this->useAjax = $useAjax;
+ return $this;
+ }
+
+ /**
+ * Class for ajax buttons
+ * @param string $ajaxClass
+ * @return DynamicContainer
+ */
+ public function setAjaxClass($ajaxClass) {
+ $this->ajaxClass = $ajaxClass;
+ return $this;
+ }
+
+ // </editor-fold>
+
+ // <editor-fold defaultstate="collapsed" desc="other protected+ methods">
+
+ /**
+ * Create "Add row" button, if enabled
+ */
+ protected function attachAddButton() {
+ list($enableAddButton, $addButtonLabel, $addButtonName) = $this->addButtonOptions;
+ if ($enableAddButton) {
+ $container = $this->getParent();
+ if (isset($container[$addButtonName]))
+ return; // todo: exception? (add param attached)
+ $container->addComponent(new SubmitButton($addButtonLabel), $addButtonName, $this->getName());
+ $container[$addButtonName]->setValidationScope(false)->onClick[] = callback($this, "addRow");
+ if ($this->useAjax)
+ $container[$addButtonName]->getControlPrototype()->class($this->ajaxClass);
+ }
+ }
+
+ /**
+ * Overriden method createRow, added "Delete row" button
+ * @param string $name
+ */
+ protected function createRow($name = null) {
+ $innerContainer = parent::createRow($name);
+
+ list($enableDeleteButton, $deleteButtonLabel, $deleteButtonName) = $this->deleteButtonOptions;
+ if ($enableDeleteButton) {
+ $button = $innerContainer->addSubmit($deleteButtonName, $deleteButtonLabel)->setValidationScope(false);
+ $button->onClick[] = callback($this, "removeRowClickHandler");
+ if ($this->useAjax)
+ $button->getControlPrototype()->class($this->ajaxClass);
+ }
+ }
+
+ /**
+ * Look at parent ;)
+ */
+ protected function attached($obj) {
+ parent::attached($obj);
+ if ($obj instanceof Presenter) {
+ $this->attachAddButton();
+ }
+ }
+
+ /**
+ * Called when is clicked to "Remove row" button
+ * @param SubmitButton $button
+ */
+ public function removeRowClickHandler($button) {
+ $name = $button->getParent()->getName();
+ $this->removeRow($name);
+ }
+
+ /**
+ * Call this before rendering
+ */
+ public function beforeRender() {
+ list($enableDeleteButton, $deleteButtonLabel, $deleteButtonName) = $this->deleteButtonOptions;
+ if ($enableDeleteButton && $this->getRowCount() <= $this->minimumRows) {
+ foreach ($this->getRows() as $container) {
+ $button = $container[$deleteButtonName];
+ $button->setDisabled()->getControlPrototype()->style("display: none;");
+ }
+ }
+
+ list($enableAddButton, $addButtonLabel, $addButtonName) = $this->addButtonOptions;
+ if ($enableAddButton && $this->getRowCount() >= $this->maximumRows) {
+ $button = $this->parent[$addButtonName];
+ $button->setDisabled()->getControlPrototype()->style("display: none;");
+ }
+ }
+
+ // </editor-fold>
+
+ // <editor-fold defaultstate="collapsed" desc="register helpers">
+
+ public static function FormContainer_addDynamicContainer(FormContainer $_this, $name = null) {
+ return $_this[$name] = new DynamicContainer(null, $name);
+ }
+
+ public static function register($methodName = "addDynamicContainer") {
+ if(PHP_VERSION_ID >= 50300)
+ FormContainer::extensionMethod($methodName, "Addons\Forms\DynamicContainer::FormContainer_addDynamicContainer");
+ else
+ FormContainer::extensionMethod("FormContainer::$methodName", array("DynamicContainer", "FormContainer_addDynamicContainer"));
+ }
+
+ // </editor-fold>
+
+}
View
46 DynamicContainerCore.php
@@ -1,7 +1,16 @@
<?php
+/**
+ * Nette addon that allows dynamically add/remove set of items in Form (FormContainer)
+ *
+ * @author Daniel Robenek
+ * @license MIT
+ */
+
namespace Addons\Forms;
+// <editor-fold defaultstate="collapsed" desc="use">
+
use Nette\IComponentContainer;
use Nette\IComponent;
use Nette\Forms\FormContainer;
@@ -9,6 +18,8 @@
use \InvalidStateException;
use \LogicException;
+// </editor-fold>
+
class DynamicContainerCore extends FormContainer {
// <editor-fold defaultstate="collapsed" desc="variables">
@@ -18,7 +29,7 @@ class DynamicContainerCore extends FormContainer {
/** Restriction for rows count */
protected $defaultRows = 1;
- protected $minimumRows = 0;
+ protected $minimumRows = 2;
protected $maximumRows = 100;
@@ -53,6 +64,14 @@ public function getRowCount() {
}
/**
+ * Get all dynamic rows
+ * @return Iterator of FormContainer
+ */
+ public function getRows() {
+ return $this->getComponents();
+ }
+
+ /**
* Set minimum number of container rows
* @param int $min
* @return $this
@@ -85,7 +104,7 @@ public function setDefaultCount($count) {
/**
* Set callback which content add items into container
- * @param Callback $callback
+ * @param Callback $callback function($container, $dynamicContainer, $form)
* @return $this
*/
public function setFactory($callback) {
@@ -102,7 +121,7 @@ public function setFactory($callback) {
* @return bool Was inserting successfull? (false = max rows reached)
*/
public function addRow() {
- if(count($this->getComponents()) >= $this->maximumRows)
+ if($this->getRowCount() >= $this->maximumRows)
return false;
$this->createRow();
$this->fireOnChange();
@@ -114,6 +133,7 @@ public function addRow() {
* Fires onChange
* @param int|string $name
* @return bool true = ok false = minimum limit exceeded
+ * @throws InvalidArgumentException
*/
public function removeRow($name) {
if(!isset($this[$name]))
@@ -149,7 +169,7 @@ public function addComponent(IComponent $component, $name) {
// <editor-fold defaultstate="collapsed" desc="other protected+ methods">
/**
- * Called when component is attached
+ * Called when component is attached to monitored object
* @param Presenter $obj
*/
protected function attached($obj) {
@@ -161,8 +181,8 @@ protected function attached($obj) {
/**
* Remove all subcontainers
*/
- protected function clear() {
- foreach($this->getComponents() as $name => $component)
+ protected function clearRows() {
+ foreach($this->getRows() as $name => $component)
unset($this[$name]);
}
@@ -171,7 +191,7 @@ protected function clear() {
*/
protected function rebuild() {
if($this->getForm()->isSubmitted()) {
- $this->clear();
+ $this->clearRows();
$containerPath = explode("-", $this->lookupPath("Nette\Forms\Form"));
$containerData = $this->getForm()->getHttpData();
foreach($containerPath as $step) { // goes through structure and extract neccesery data
@@ -191,7 +211,7 @@ protected function rebuild() {
* Create minimum count of rows if neccessery
*/
protected function fixMinRows() {
- $count = count($this->getComponents());
+ $count = $this->getRowCount();
if($count < $this->minimumRows) {
for($i = $count; $i < $this->minimumRows; $i++)
$this->createRow();
@@ -202,7 +222,7 @@ protected function fixMinRows() {
* Create default count of rows if neccessery
*/
protected function fixDefaultRows() {
- $count = count($this->getComponents());
+ $count = $this->getRowCount();
if($count < $this->defaultRows) {
for($i = $count; $i < $this->defaultRows; $i++)
$this->createRow();
@@ -212,17 +232,19 @@ protected function fixDefaultRows() {
/**
* Creates row of given name
* @param string|int $name
+ * @return FormContainer created container
*/
protected function createRow($name = null) {
- $items = $this->getComponents();
+ $items = $this->getRows();
$itemCount = count($items);
if($name === null) {
- $name = $itemCount == 0 ? "0" : (string)(1 + (int)end($items)->getName());
+ $name = $itemCount == 0 ? "0" : (string)(1 + (int)(end($items)->getName()));
}
$innerContainer = $this->addPrivateContainer($name);
call_user_func($this->factoryCallback, $innerContainer, $this, $this->getForm());
+ return $innerContainer;
}
/**
@@ -231,7 +253,7 @@ protected function createRow($name = null) {
* @return FormContainer added contanier
*/
private function addPrivateContainer($name) {
- $control = new FormContainer;
+ $control = new FormContainer();
$control->currentGroup = $this->currentGroup;
parent::addComponent($control, $name);
return $this[$name];
View
236 FormMacros.php
@@ -0,0 +1,236 @@
+<?php
+
+/**
+ * Form macros
+ *
+ * @author Jan Marek, Daniel Robenek
+ * @license MIT
+ */
+
+namespace Addons\Forms;
+
+// <editor-fold defaultstate="collapsed" desc="use">
+
+use \LogicException;
+use \InvalidArgumentException;
+use Nette\Templates\LatteMacros;
+use Nette\String;
+use Nette\Forms\Form;
+use Nette\Web\Html;
+
+// </editor-fold>
+
+class FormMacros extends \Nette\Object {
+
+ // <editor-fold defaultstate="collapsed" desc="variables">
+
+ protected static $stack;
+
+ public static $defaultOuterError = "div class='form-errors'";
+ public static $defaultInnerError = "p class='error'";
+
+ // </editor-fold>
+
+ // <editor-fold defaultstate="collapsed" desc="constructor">
+
+ public function __construct() {
+ throw new \LogicException("Static class could not be instantiated !");
+ }
+
+ // </editor-fold>
+
+ // <editor-fold defaultstate="collapsed" desc="{form}">
+
+ public static function macroFormBegin($content) {
+ list($name, $modifiers) = self::fetchNameAndModifiers($content);
+ return "Addons\Forms\FormMacros::formBegin($name, \$control, $modifiers)";
+ }
+ public static function formBegin($form, $control, $modifiers) {
+ $form = ($form instanceof Form) ?: $control[$form];
+ self::$stack = array($form);
+ self::applyModifiers($form->getElementPrototype(), $modifiers);
+ $form->render("begin");
+ }
+
+ public static function macroFormEnd($content) {
+ return "Addons\Forms\FormMacros::formEnd()";
+ }
+ public static function formEnd() {
+ self::getForm()->render("end");
+ }
+
+ // </editor-fold>
+
+ // <editor-fold defaultstate="collapsed" desc="{formErrors}">
+
+ public static function macroFormErrors($content) {
+ $params = LatteMacros::formatArray($content);
+ return "Addons\Forms\FormMacros::formErrors($params)";
+ }
+ public static function formErrors($parameters) { // todo: refactor
+ $innerHtml = !empty($parameters) ? array_shift($parameters) : null;
+ if($innerHtml === null)
+ $innerHtml = Html::el(self::$defaultInnerError);
+ else if(!($innerHtml instanceof Html))
+ $innerHtml = Html::el($innerHtml);
+
+ $outerHtml = !empty($parameters) ? array_shift($parameters) : null;
+ if($outerHtml === null)
+ $outerHtml = Html::el(self::$defaultOuterError);
+ else if(!($outerHtml instanceof Html))
+ $outerHtml = Html::el($outerHtml);
+
+ $errors = self::getForm()->getErrors();
+ if(empty($errors))
+ return;
+
+ foreach($errors as $error) {
+ $currentInner = clone($innerHtml);
+ $currentInner->setText($error);
+ $outerHtml->add($currentInner);
+ }
+ echo($outerHtml->render());
+ }
+
+ // </editor-fold>
+
+ // <editor-fold defaultstate="collapsed" desc="{formContainer}">
+
+ public static function macroBeginContainer($content) {
+ list($name) = self::fetchNameAndModifiers($content);
+ return "Addons\Forms\FormMacros::beginContainer($name)";
+ }
+ public static function beginContainer($name) {
+ self::$stack[] = self::getControl($name);
+ }
+
+ public static function macroEndContainer($content) {
+ return "Addons\Forms\FormMacros::endContainer()";
+ }
+ public static function endContainer() {
+ array_pop(self::$stack);
+ }
+
+ // </editor-fold>
+
+ // <editor-fold defaultstate="collapsed" desc="{input}">
+
+ public static function macroInput($content) {
+ list($name, $modifiers) = self::fetchNameAndModifiers($content);
+ return "Addons\Forms\FormMacros::input($name, $modifiers)";
+ }
+ public static function input($name, $modifiers) {
+ $input = self::getControl($name)->getControl();
+ self::applyModifiers($input, $modifiers);
+ echo $input;
+ }
+
+ // </editor-fold>
+
+ // <editor-fold defaultstate="collapsed" desc="{label}">
+
+ public static function macroLabel($content) {
+ list($name, $modifiers) = self::fetchNameAndModifiers($content);
+ return "Addons\Forms\FormMacros::label($name, $modifiers)";
+ }
+ public static function label($name, $modifiers) {
+ $label = self::getControl($name)->getLabel();
+ self::applyModifiers($label, $modifiers);
+ echo $label;
+ }
+
+ // </editor-fold>
+
+ // <editor-fold defaultstate="collapsed" desc="{inputValue}">
+
+ public static function macroInputValue($content) {
+ list($name, $modifiers) = self::fetchNameAndModifiers($content);
+ return "Addons\Forms\FormMacros::inputValue($name, $modifiers)";
+ }
+ public static function inputValue($name, $modifiers = array()) {
+ $input = self::getControl($name)->getControl();
+ echo $input->getValue();
+ }
+
+ // </editor-fold>
+
+ // <editor-fold defaultstate="collapsed" desc="{dynamicContainer}">
+
+ public static function macroBeginDynamicContainer($content) {
+ list($name) = self::fetchNameAndModifiers($content);
+ return '$dynamicContainers = Addons\Forms\FormMacros::getControl('.$name.')->getComponents(); Addons\Forms\FormMacros::beginContainer('.$name.'); foreach($dynamicContainers as $dynamicContainerName => $dynamicContainer): Addons\Forms\FormMacros::beginContainer($dynamicContainerName);';
+ }
+ public static function macroEndDynamicContainer($content) {
+ return "Addons\Forms\FormMacros::endContainer(); endforeach; Addons\Forms\FormMacros::endContainer();";
+ }
+
+ // </editor-fold>
+
+ // <editor-fold defaultstate="collapsed" desc="Helpers">
+
+ public static function register() {
+ LatteMacros::$defaultMacros["form"] = '<?php %Addons\Forms\FormMacros::macroFormBegin% ?>';
+ LatteMacros::$defaultMacros["/form"] = '<?php %Addons\Forms\FormMacros::macroFormEnd% ?>';
+
+ LatteMacros::$defaultMacros["formErrors"] = '<?php %Addons\Forms\FormMacros::macroFormErrors% ?>';
+
+ LatteMacros::$defaultMacros["input"] = '<?php %Addons\Forms\FormMacros::macroInput% ?>';
+ LatteMacros::$defaultMacros["label"] = '<?php %Addons\Forms\FormMacros::macroLabel% ?>';
+ LatteMacros::$defaultMacros["inputValue"] = '<?php %Addons\Forms\FormMacros::macroInputValue% ?>';
+
+ LatteMacros::$defaultMacros["formContainer"] = '<?php %Addons\Forms\FormMacros::macroBeginContainer% ?>';
+ LatteMacros::$defaultMacros["/formContainer"] = '<?php %Addons\Forms\FormMacros::macroEndContainer% ?>';
+
+ LatteMacros::$defaultMacros["dynamicContainer"] = '<?php %Addons\Forms\FormMacros::macroBeginDynamicContainer% ?>';
+ LatteMacros::$defaultMacros["/dynamicContainer"] = '<?php %Addons\Forms\FormMacros::macroEndDynamicContainer% ?>';
+ }
+
+ /**
+ * Return current rendered form
+ * @return Form
+ */
+ public static function getForm() {
+ return self::$stack[0];
+ }
+
+ /**
+ * Return form control of given name/path
+ * When name starts with "-", it means absolute path
+ * Containers are separated by "-"
+ * Examples:
+ * -container1-control => return $form["container1"]["control"]
+ * containerx-control => return $currentContainer["containerx"]["control"]
+ * @param <type> $name
+ * @return <type>
+ */
+ public static function getControl($name) {
+ $name = (string)$name;
+ if($name == "" || $name == "-") // todo: "" = form or container?
+ throw new InvalidArgumentException("Control must be specified !");
+ $names = explode("-", $name);
+ if($names[0] == "") {
+ $container = reset(self::$stack);
+ unset($names[0]);
+ } else
+ $container = end(self::$stack);
+ foreach($names as $name)
+ $container = $container[$name];
+ return $container;
+ }
+
+ protected static function applyModifiers(Html $element, array $modifiers) {
+ foreach($modifiers as $key => $value)
+ $element->$key($value);
+ }
+
+ protected static function fetchNameAndModifiers($content) {
+ $name = LatteMacros::fetchToken($content);
+ $name = String::startsWith($name, '$') ? $name : "'$name'";
+ $modifiers = LatteMacros::formatArray($content);
+ $modifiers = $modifiers ?: "array()";
+ return array($name, $modifiers);
+ }
+
+ // </editor-fold>
+
+}
View
60 README
@@ -0,0 +1,60 @@
+Dynamic Form Container
+----------------------
+Nette addon that allows dynamically add/remove set of items in Form
+
+@author Daniel Robenek
+@license MIT
+
+class DynamicContainerCore - Base of functionality, allow to create dynamic container, add / remove row
+class DynamicContainer - Extending DynamicContainerCore, added options for auto-create add and remove buttons
+class FormMacros - Class created by Jan Marek, but upgraded to better functionality and implements DynamicContainer rendering
+
+Example of DynamicContainer:
+
+* Presenter *
+<?php
+ protected function startup() {
+ parent::startup();
+ Addons\Forms\FormMacros::register(); // register form macros
+ Addons\Forms\DynamicContainer::register(); // register $form->addDynamicContainer($name);
+ }
+
+ public function createComponentForm($name) {
+ $form = new AppForm($this, $name);
+ $dynamicContainer = $form->addDynamicContainer("dynamicContainer");
+ $dynamicContainer->setAddButton(true, "Give me new row !", "addButtonOfDynamicContainer"); // set button options not neccessery / have to be set befor form attached & factory set
+ $dynamicContainer->setDeleteButton(true, "I dont wana this !", "removeButtonOfDynamicContainer"); // set button options not neccessery / have to be set befor form attached & factory set
+ $dynamicContainer->setMinCount(1); // minimum count of rows
+ $dynamicContainer->setMaxCount(5); // maximum count of rows
+ $dynamicContainer->setDefaultCount(2); // default count of rows
+ $dynamicContainer->setFactory(function(FormContainer $container) { // set factory which add content to each of container
+ $container->addText("text", "Input")
+ ->addRule(Form::FILLED, "Input must be fillet !");
+ $container->addCheckbox("isok", "Is it ok?");
+ });
+ return $form;
+ }
+
+ public function renderDefault() {
+ $this["form"]["dynamicContainer"]->beforeRender(); // needed :(
+ }
+?>
+
+* template (w/o formatting tags) *
+<?php
+ {form form}
+
+ {formErrors 'li style="color: red"', 'ul style="font-size: 1.5em"'}
+
+ {input addButtonOfDynamicContainer}
+
+ {dynamicContainer dynamicContainer}
+ {label text}{input text}
+ {label isok}{input isok}
+ {input removeButtonOfDynamicContainer}
+ {/dynamicContainer}
+
+ {/form}
+?>
+
+Enjoy :)
Please sign in to comment.
Something went wrong with that request. Please try again.