Komponenta – změna view (+ ajax)

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

Komponenta na formulář, v presenteru zavolána přes factory, k tomu namíchaný ajax, funguje.
Nicméně nemůžu přijít na způsob jak po odeslání formuláře onen formulář vymazat a nechat tam jenom success zprávu (a tady by mě zajímalo jestli to jde udělat a) flash-kou a za b) přes jiný view / render / handle )

Kódy

Presenter:

	protected function createComponentRegistrationForm()
	{
		$control = $this->registrationFormFactory->create();
		return $control;
	}

Komponenta (očištěná od věcí, které nejsou důležité):

<?php

namespace App\FrontModule\Components;

use Nette\Application\UI,
	Nette,
	Nette\Mail\Message,
	Nette\Mail\SendmailMailer,
	App;


interface IRegistrationFormFactory
{
	/** @return RegistrationForm */
	public function create ();
}


class RegistrationForm extends UI\Control
{

	protected $userModel;

	public function __construct (App\Model\User $user)
	{
		parent::__construct();

		$this->userModel = $user;
	}

	public function handleSave()
	{
        // nebo renderSave nebo jak kua?!
	}


	public function render()
	{
		$this->template->setFile(__DIR__ . '/../templates/components/registration-form.latte');
		$this->template->render();
	}


	public function createComponentForm()
	{
		$form = new UI\Form;
		.
		.
		.
		$form->onSuccess[] = array($this, 'registerFormSubmitted');

		return $form;
	}


	public function registerFormSubmitted($form)
	{
		$values = $form->getValues(TRUE);
		$isRegistered = $this->userModel->isEmailDuplicity($values["email"]);

		if ($isRegistered)
		{
			$form->addError("Tento email je již registrován, zvolte si jiný");

			$this->redrawControl();
			return;
		}

		$this->flashMessage("Registrace dokončena", "alert-success");

        if ($this->presenter->isAjax())
        {
           	$this->redrawControl('ajaxform');
           	$form->setValues(array(), TRUE);

        }
        else
        {
        	$this->redirect("this");
        }

	}


}

Šablona formuláře:

{snippet ajaxform}

	<div n:foreach="$flashes as $flash" class="flash alert {$flash->type}">{$flash->message}</div>

	{form form "novalidate" => "", "class" => "ajax"}
		...
	{/form}

{/snippet}

Může mi tedy prosím někdo říct jak následující kód inteligetně a čistě upravit tak, abych po odeslání mohl vyměnit render za jiný (nechci na původním místě zobrazovat formulář, jen místo něj zobrazit hlášku / případně jiné HTML v rámci oné komponenty = v jiném view?)

PS Můžu si do šablony přidat nějaké označení jestli byl formulář již poslán či nikoliv, ale to není co bych chtěl (chtěl bych to vyřešit v jiném renderu – jestli to tedy jde vůbec).

Editoval Kcko (27. 9. 2014 2:14)

Šaman
Člen | 2666
+
0
-
  1. Buď ta komponenta bude stavová a sama si určí, jestli vykreslovat formulář, nebo ne. Pak ji musíš navázat na úložiště (třeba jen session), kde si bude stav uchovávat.
  2. Po odeslání formuláře přesměrovat na jiný pohled presenteru.
  3. Nebo trochu jiná varianta první možnosti – ten stav si můžeš předávat v URL a šablona presenteru, nebo komponenty podle existence nějaké proměnné (ne)vykreslí formulář (nebo i celou komponentu).

P.S. Pak budeš mít handleSave (není dobré ukládat data v pohledu, tedy render metodě – co když dá někdo F5-obnovit stránku?) a z ní redirektuješ na jiný pohled, jehož šablona komponentu vůbec nevykreslí. Tedy možnost 2.

Editoval Šaman (27. 9. 2014 2:17)

Kcko
Člen | 470
+
0
-

@Šaman

Díky za komentář.

  1. Tohle není možné (resp. ano, ale příčí se mi logika i pracnost). Pokud bych přesměroval na jiný view/render v Presenteru, tak budu muset zduplikovat šablonu, tj. v mém případě default.latte, protože formulář se nachází na homepage webu, a jediné co v té zduplikované šabloně vyměním bude controlka formuláře za výpis hlášky // třeba flashmessages nebo to tam bude jako překlad … → to je dost ugly ne, mít 2 view, které se liší jen tím že v jednom je controlka a v druhém ne a místo ní je tam napsano odeslano.

1+3) Ještě později v noci než jsem málem pošel :-) jsem to tak udělal, posílám si do šablony proměnnou, zda-li je odesláno či nikoliv a dle toho kontrolku vypíšu.

Tj.

{snippet ajaxform}

	<div n:foreach="$flashes as $flash" class="flash alert {$flash->type}">{$flash->message}</div>

	{if !isset($sent)}
	{form form "novalidate" => "", "class" => "ajax"}

a callback formuláře – konec metody

		$this->flashMessage("Registrace dokončena", "alert-success");

if ($this->presenter->isAjax())
{
	$this->template->sent = 1;
   	$this->redrawControl('ajaxform');
   	$form->setValues(array(), TRUE);

}
else
{
	$this->redirect("this");
}

Ale nelíbí se mi to. Moje představa byla, že kontrolce řeknu o změnu pohledu a v jiné renderovací metodě vypíšu nějaké jiné HTML s hláškou o odeslání. (Sem už trošku kantare po probdělé noci, tak bych možná potřeboval kus kódu).

Editoval Kcko (27. 9. 2014 9:49)

Zax
Člen | 370
+
0
-

Bohužel, komponenty v Nette pohledy nemají, tedy aspoň ne pohledy, mezi kterými by šlo v rámci komponenty přepínat.

Sám jsem strávil celkem značné množství času tím, že jsem zkoušel komponenty různě hackovat, protože:

  1. Nejsou pohledy
  2. Nebaví mě pořád všude psát „if is ajax“
  3. Nebaví mě neustále určovat cestu k šablonám

Bohužel se to ani ve finále neobešlo bez jistých (drobných) kompromisů.

Kdyby tě zajímaly detaily, můžeš si počíst zde https://github.com/…doc/index.md#…, kdyby byl zájem, můžu komponentu vyčlenit do samostatného repa a uvolnit ji třeba pod licencí MIT.

Kcko
Člen | 470
+
0
-

@Zax aha, tak jsem myslel, že to je pouze mojí neznalostí a pronikáním do Nette.
Určite můžeš :) zájem bude.

Jinak jsem tu komponentu upravil, aby dělala to co chci, zbývá poslední krok k úspěchu, co mám napsat do Ajaxu, aby mi vykreslil / přesměroval na handle jako v případě else větve? Viz zcela dole.

class RegistrationForm extends UI\Control
{
	protected $view;

	public function handleDone()
	{
		$this->view = 'registration-form-save';
	}

	public function render()
	{

		if (!$this->view)
			$this->template->setFile(__DIR__ . '/../templates/components/registration-form.latte');
		else
			$this->template->setFile(__DIR__ . '/../templates/components/'.$this->view.'.latte');
		$this->template->render();
	}

	public function registerFormSubmitted($form)
	{
		...

        if ($this->presenter->isAjax())
        {
		 /* CO BUDE TADY ??? */

        }
        else
        {
        	$this->redirect("done!"); // FUNGUJE mimo ajax, nacte se spravna sablona
        }

	}


}

Editoval Kcko (27. 9. 2014 11:01)

Zax
Člen | 370
+
0
-

$view dát public a přidat anotaci @persistent (tak to mám já ve svých komponentách), čímž si přidáš možnost měnit pohledy přes URL a tedy i odkazovat na ně.

Ten submit bych přepsal zhruba takto

if($this->presenter->isAjax()) {
	$this->view = 'registration-form-save';
	$this->redrawControl();
} else {
	// normální redirect s persistentním parametrem
	$this->redirect('this', ['view' => 'registration-form-save']);
}

Bacha na jednu věc – parametr $view je přístupný v URL, takže si ho při nastavování šablony ověř např. přes whitelist, jinak tam budeš mít díru. (já to řeším ověřením jestli existuje metoda view<View>())

BTW místo toho $this->view = 'registration-form-save'; by možná bylo hezčí použít nějaký obecnější $this->forward('this', ['view' => 'registration-form-save']); a od toho už je to jen krůček k něčemu jako $this->go('this', ['view' => 'registration-form-save']);, které ti udělá i tu kontrolu „if is ajax“. Narazíš ale na to, že forward ti zruší překreslování snippetů, proto jsem napsal i vlastní forward (trochu zbastlený, testy na něj nemám, ale pohání mi už mnoho komponent, takže je funkční ;-) )

Brzy tu komponentu vyčlením do samostatného repa.

Kcko
Člen | 470
+
0
-

@Zax zajímavé řešení.

  1. Moc nerozumím tomu white listu. ve $view je pouze šablona, nejsou to žadné metody, jak to tedy nastavujes?

Já bych na to šel asi takhle jednoduše.

	/** @persistent */
	public $view;

	protected $allowedView = ["foo", "bar"];

	public function render()
	{
		if (!$this->view)
			$view = $allowedView[0];
		else
		{
			if (!in_array($this->view, $allowedView))
				$view = $allowedView[0];
			else
				$view = $this->view;
		}

			$this->template->setFile(__DIR__ . '/../templates/components/'.$view);

		$this->template->render();
	}
  1. Upravil jsem na persistentní a public, pokud pošlu bez ajaxu funguje, pokud s tak nikolivěk a je zcela jedno jestli mám
    • kontrolku v šabloně presenteru obalenou snippetem viz
			{snippet ajaxform}
				{control registrationForm}
			{/snippet}

nebo snippetem obalím formulář v šabloně komponenty

{snippet ajaxform}
	<div n:foreach="$flashes as $flash" class="flash alert {$flash->type}">{$flash->message}</div>
	{form form "novalidate" => "", "class" => "ajax"}
	...

Editoval Kcko (27. 9. 2014 13:20)

Kcko
Člen | 470
+
0
-

Moje chyba! Chci překreslit snippet který je v šabloně presenteru tak z controlky musím volat $this->presenter->redrawControl a už to KONEČNĚ FUNGUJE

Zeptám se už na (snad) poslední věc. Kdybych nechtěl měnit view přes URL a měnil ho přes handle (tak jak jsem to měl původně, což je defakto whitelist, jak to tomu ajaxu řeknu, že chci „přesměrovat“ na handle a překreslit … bez ajaxu je to jednoduche $this->redirect('save!'), ale ted vůbec nevím jak na to v odchytnutí ajaxu.

Zax
Člen | 370
+
0
-

Signály (handle* metody) jsou určeny pro nějakou akci (v url je ?do=) a má po nich následovat přesměrování na normální adresu bez signálu. To co ty zkoušíš by sice šlo, ale nemá se to tak dělat :-)

Komponentu jsem nahrál na github a přidal pár testů (doporučuju projít zdroják, není v tom nic extra složitého, jen pár hacků ;-)). Měla by být dostupná přes composer (zaxxx/zaxcontrol). Tvá situace by šla řešit cca takto:

class RegistrationForm extends Zax\Application\UI\Control
{

    public function viewDefault() {}

    public function viewDone() {}

    public function registerFormSubmitted($form)
    {
        ...

        $this->redrawControl();
		$this->go('this', ['view' => 'Done']);
    }

}

K tomu přidáš složku templates, do ní dvě šablony Default.latte a Done.latte a mělo by to frčet. Nezapomeň obě šablony obalit snippetem (IMHO je lepší to dělat přímo v komponentě než v presenteru).

Jsou tam ještě cesty, jak to trochu víc zautomatizovat, ale tím tě zatěžovat nebudu, je to dost magické :-D

Editoval Zax (27. 9. 2014 13:37)

Kcko
Člen | 470
+
0
-

Díky už se s tím poperu :-)