IFormRenderer beforeRender

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Pipaslot
Člen | 19
+
0
-

Snažím se aplikovat renderování šablony, například pro Bootstrap do formulářů ale narazil jsem na problém při manuálním vykreslování. Původní myšlenku renderování šablon například pro bootstrap jsem zapsal do třídy rendereru dědícího od DefaultFormRenderer

class Bootstrap3Renderer extends DefaultFormRenderer
{
	/**
	 * Bootstrap3Renderer constructor.
	 */
	public function __construct()
	{
		$this->wrappers['controls']['container'] = NULL;
		$this->wrappers['pair']['container'] = 'div class=form-group';
		$this->wrappers['pair']['.error'] = 'has-error';
		$this->wrappers['control']['container'] = 'div class=col-sm-9';
		$this->wrappers['label']['container'] = 'div class="col-sm-3 control-label"';
		$this->wrappers['control']['description'] = 'span class=help-block';
		$this->wrappers['control']['errorcontainer'] = 'span class=help-block';
		$this->wrappers['error']['container'] = 'div class="col-sm-9 col-sm-offset-3"';
		$this->wrappers['error']['item'] = 'div class="alert alert-danger" role="alert"';
	}

	/**
	 * Provides complete form rendering.
	 * @param Form $form
	 * @return string
	 */
	function render(Form $form)
	{
		$usedPrimary = FALSE;
		foreach ($form->getControls() as $control) {
			if ($control instanceof Controls\Button) {
				$control->getControlPrototype()->addClass(empty($usedPrimary) ? 'btn btn-primary' : 'btn btn-default');
				$usedPrimary = TRUE;
			} elseif ($control instanceof Controls\TextBase || $control instanceof Controls\SelectBox || $control instanceof Controls\MultiSelectBox) {
				$control->getControlPrototype()->addClass('form-control');
			} elseif ($control instanceof Controls\Checkbox || $control instanceof Controls\CheckboxList || $control instanceof Controls\RadioList) {
				$control->getSeparatorPrototype()->setName('div')->addClass($control->getControlPrototype()->type);
			}
		}

		return parent::render($form);
	}
}

Toto využívám spolu s továrnou na formuláře, která mi vrátí předchystaný formulář i s nasaveným rendererem. Zatím je vše v pohodě.
Když poté v šabloně zavolám {control myForm}, tak se formulář vykreslí se vší parádou, jelikož se zavolalo na rendereru render(Form $form) , kde se projdou všechny prvky a doupraví se k obrazu šablony.

Komplikovat se to začíná, když formulář chci podrobit manuálnímu vykreslování, takže zavolám:

{form myForm}
		{foreach $form->getComponents() ...}
			... vykreslím formulářová pole ...
		{/foreach}
{/form}

Bohužel při manuální vykreslování se mi žádným způsobem nezkontaktuje renderer, který by mi právě prvky přidané do formuláře až po nastavení rendereru dodatečně upravil.

Napadlo mě tedy, přidal do rozhranní IFormRenderer metodu beforeRender(Form $form), která by se poté volala jak v metodě Nette\Forms\Form::render, tak v Nette\Bridges\FormsLatte\Runtime::renderFormBegin ,která se spouští při vyžádání vykreslení elementu formuláře.
Tím bycho mohli přesunout kód z metody render u rendereru, který se stará o vykreslení celého formuláře do metody beforeRender.No a výchozí implementace u třídy DefaultFormRenderer by byla prázdná, tak aby to bylo kompatibilní.

Co vy na to? Je v tom nějaká záludnost kterou nevidím?

F.Vesely
Člen | 369
+
-1
-

Pouzij https://github.com/…Renderer.php, stejnak uz to mas az nenapadne podobne. :)

Pipaslot
Člen | 19
+
0
-

Pokud chápu tvé řešení správně, tak bych poté v šabloně vždy musel volat místo makra {input …} a {label …} něco jako: $form->getRenderer()->renderControl($control) .
Tomu jsem se ale chtěl vyhnout.
Nicméně stále tam vidím záludnost a to že při inicializaci prvků potřebuješ předat formulář, který se předá až metodou render, jenže ta se při manuálním vykreslení nespustí.
Napadá me ještě přepsání maker pro formuláře, tak aby se spustila metoda render ale druhým parametrem bych řekl že se má jen zinicializovat a nikoli vykreslit. Nicméně mi to připadá moc krkolomné.

F.Vesely
Člen | 369
+
-1
-

Ne, nechapes to spravne. V sablone pouzij normalne makro input, ono uz se o to postara.

David Matějka
Moderator | 6445
+
+1
-

@F.Vesely macro input se o to nepostara, to nezavola nic z rendereru: https://github.com/…rmMacros.php#…

F.Vesely
Člen | 369
+
-1
-

@DavidMatějka jasne, uz jsem byl pouze na mobilu a nechtelo se mi rozepisovat. co vsechno se vola. Chtel jsem tim jen rict, ze to proste funguje, aniz by musel neco prepisovat.

Pipaslot
Člen | 19
+
0
-

Teď jsem zkoušel na sandboxu příklad:

class HomepagePresenter extends BasePresenter
{

	public function createComponentForm1()
	{
		$form = new Nette\Application\UI\Form();
		$form->addText("name");
		$form->addSubmit("submit");
		$form->setRenderer(new Bs3FormRenderer());
		return $form;
	}
	public function createComponentForm2()
	{
		$form = new Nette\Application\UI\Form();
		$form->addText("name");
		$form->addSubmit("submit");
		$form->setRenderer(new Bs3FormRenderer());
		return $form;
	}
}

S šablonou:

{* Funguje*}
{control form1}

{* Nefunguje*}
{form form2}
	{input name}
	{input submit}
{/form}

a první formulář se vykreslí podle rendereru, zatím co druhý nikoli. Platí přesně poznámka Davida Matějky.

Nicméně jsem zksil nalinkovat makra, které uvádíš ve svém repozitáři a poté se oba formuláře začaly vykreslovat podobně.
Vidím v tom ale jednu neduhu, a to že kromě rendereru si musí človek ještě hlídta , zda má nalinkované správné makra. Bohužel využívám vícero různých rendererů v jedné aplikaci, takže to takto nemohu použít.

F.Vesely
Člen | 369
+
0
-

No jo, mas pravdu, ono to i dava smysl, aby se nevolalo nic z rendereru, kdyz mas manualni render.
Ty budes muset nejak zaradit, aby kdyz se nastavi formulari Renderer, tak se vsem inputum pridaly ty tridy, ktere potrebujes. Jo a nove pridanym inputum po nastaveni Rendereru taky.

Pipaslot
Člen | 19
+
0
-

Tak jako nejschůdnější řešení se mi zdá definovat si rozhranní, kde udělám metodu before render a překriju makro {form}, kde zjistím zda renderer má definované rozhranní a případně spustím before render metodu. Tím změny renderování zustanou stále jen v rendereru a mělo by pak fungovat snad vše normálně :)

Pipaslot
Člen | 19
+
0
-

Tak nakonec jsem si to vymyslel takto:

interface IManualRenderer extends IFormRenderer
{
	function renderFormBegin(Form $form);
}

class Bootstrap3Renderer extends DefaultFormRenderer implements IManualRenderer
{
	public function __construct()
	{
		$this->wrappers['controls']['container'] = NULL;
		$this->wrappers['pair']['container'] = 'div class=form-group';
		$this->wrappers['pair']['.error'] = 'has-error';
		$this->wrappers['control']['container'] = 'div class=col-sm-9';
		$this->wrappers['label']['container'] = 'div class="col-sm-3 control-label"';
		$this->wrappers['control']['description'] = 'span class=help-block';
		$this->wrappers['control']['errorcontainer'] = 'span class=help-block';
		$this->wrappers['error']['container'] = 'div class="col-sm-9 col-sm-offset-3"';
		$this->wrappers['error']['item'] = 'div class="alert alert-danger" role="alert"';
	}
	private function beforeRender(Form $form)
	{
		$form->getElementPrototype()->class[] = 'form-horizontal';
		BootstrapHelper::ApplyBootstrapToControls($form);
	}

	function render(Form $form, $mode = NULL)
	{
		$this->beforeRender($form);
		return parent::render($form);
	}

	function renderFormBegin(Form $form)
	{
		$this->beforeRender($form);
	}
}
class BootstrapHelper
{
	public static function ApplyBootstrapToControls(Form $form)
	{
		$usedPrimary = FALSE;
		foreach ($form->getControls() as $control) {
			if ($control instanceof Controls\Button) {
				$control->getControlPrototype()->addClass(empty($usedPrimary) ? 'btn btn-primary' : 'btn btn-default');
				$usedPrimary = TRUE;
			} elseif ($control instanceof Controls\TextBase || $control instanceof Controls\SelectBox || $control instanceof Controls\MultiSelectBox) {
				$control->getControlPrototype()->addClass('form-control');
			} elseif ($control instanceof Controls\Checkbox || $control instanceof Controls\CheckboxList || $control instanceof Controls\RadioList) {
				$control->getSeparatorPrototype()->setName('div')->addClass($control->getControlPrototype()->type);
			}
		}
	}
}

Pak jsem překryl původní makra

class FormMacros extends FormsLatte\FormMacros
{
	public static function install(Compiler $compiler)
	{
		$me = new static($compiler);
		$me->addMacro('form', array($me, 'macroForm'), 'echo Pipas\Forms\Latte\Runtime::renderFormEnd($_form)');
		$me->addMacro('formContainer', array($me, 'macroFormContainer'), '$formContainer = $_form = array_pop($_formStack)');
		$me->addMacro('label', array($me, 'macroLabel'), array($me, 'macroLabelEnd'));
		$me->addMacro('input', array($me, 'macroInput'), NULL, array($me, 'macroInputAttr'));
		$me->addMacro('name', array($me, 'macroName'), array($me, 'macroNameEnd'), array($me, 'macroNameAttr'));
		$me->addMacro('inputError', array($me, 'macroInputError'));
	}
	public function macroForm(MacroNode $node, PhpWriter $writer)
	{
		if ($node->modifiers) {
			trigger_error('Modifiers are not allowed here.', E_USER_WARNING);
		}
		if ($node->prefix) {
			throw new CompileException('Did you mean <form n:name=...> ?');
		}
		$name = $node->tokenizer->fetchWord();
		if ($name === FALSE) {
			throw new CompileException("Missing form name in {{$node->name}}.");
		}
		$node->tokenizer->reset();
		return $writer->write(
			'echo Pipas\Forms\Latte\Runtime::renderFormBegin($form = $_form = '
			. ($name[0] === '$' ? 'is_object(%node.word) ? %node.word : ' : '')
			. '$_control[%node.word], %node.array)'
		);
	}
}
class Runtime extends FormsLatte\Runtime
{
	public static function renderFormBegin(Form $form, array $attrs, $withTags = TRUE)
	{
		$renderer = $form->getRenderer();
		if ($renderer instanceof IManualRenderer) {
			$renderer->renderFormBegin($form);
		}
		return parent::renderFormBegin($form, $attrs, $withTags);
	}
}

Editoval Pipaslot (10. 1. 2016 0:55)