Editace položky / Zobrazení formuláře pomocí AJAXu

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

Dobrý den, mohl bych prosím dostat radu?

Mám funkční editaci jednoho políčka šablony takto:

Šablona detailu:

<p>Název: {$p->nazev}> <a href="{link edit, $p->id}">Editovat název</a></p>

V presenteru k šabloně mám formulář pro editaci:

public function renderEdit($id = 0) {
     $form = new Form;
     $form ->addText('nazev', 'Název:');
     $form->addSubmit('ulozit', 'Uložit');
     $this->template->form = $form;

     // Naplníme hodnoty polí
     if (!$form->isSubmitted()) {
         $detail = new Detail; //model
         $row = $detail->findDetail($id)->fetch();
         if (!$row) {throw new /*Nette\Application\*/BadRequestException('Záznam nenalezen.');}
         $form->setDefaults($row);
     } else { // jestliže je odesláno:
             $detail = new Detail;
             $detail->update($id, $form->getValues());
             $this->flashMessage('Změna byla uložena.');
         $this->redirect('Detail:sablona',$id);
     }
 }

Šablona pro editaci obsahuje jen {!$form}.

Všechno to funguje správně. Co bych chtěl udělat: Po kliknutí na Editovat název ze na tom místě pomocí AJAXu zobrazí ten malý formulářík, vyplním, potvrdím a opět na stejném místě se objeví aktualizovaná položka.

Zkoušel jsem to všelijak podle různých informací tady, ale vůbec se mi nedaří. Můžete mě prosím nasměrovat? Díky.

Jod
Člen | 701
+
0
-

Ak dobre rozumiem, tak ti to ajaxom nefunguje. Tam je problém, že po úspešnej požiadavke robíš redirect, nastav pri ajaxe na forwarding. Plus to treba dať do snippetu a invalidovať ho.

Lopo
Člen | 277
+
0
-

ja som inline editaciu vyriesil pomocou http://plugins.jquery.com/…ct/jeditable

jarks
Člen | 94
+
0
-

Jod napsal(a):
Ak dobre rozumiem, tak ti to ajaxom nefunguje…

Bohužel mi to nefunguje vůbec. Nedaří se mi ani, aby se formulář vůbec objevil ve stránce. Javascripty mám viditelně nalinkované správně, protože Datagrid jde krásně. Ale když zkouším ajaxový odkaz (class="ajax") nebo různě operovat se snippety, nedělá to buď nic, nebo chyby parse error na šabloně. Potřeboval bych prostě ukázat cestu, jak na to. Teprve začínám a trochu mi z toho jde hlava kolem.

Jeditable – In Place Editor plugin se mi moc použít nechce, protože bych rád, aby v nepřítomnosti javascriptu proběhlo normální zavolání presenteru na jeho stránce.

Editoval jarks (16. 7. 2009 14:26)

Panda
Člen | 569
+
0
-

Hm, kód zkusím napsat z fleku, bez testování, takže se prosím nehněvejte, když tam bude nějaká bota, sada bot, nebo dokonce celá obuv. Je to koncipované kousek jinak, než kód nahoře, ale vyjít by z toho jít mělo. Kód také využívá některé vlatnosti více či méně aktuálního Nette.

Template:

<!--
	Pokud je tento kód zanořen do podmínky či cyklu,
	musí být prefixován zavináčem:
		@{if ...}
		@{/if}
	Pokud používáš nové šablony, mělo by být prefixováno i
		@{include #content} (v layoutu)
		@{block #content}
		@{/block} (pravděpodobně může být vynecháno,
				pokud jde o poslední blok šablony)
-->
<p>
	{snippet title$p->id}
	Název: {$p->nazev}

	{if $editTitle != $p->id}
	<a href="{link editTitle!, 'editId' => $p->id}" class="ajax">Editovat název</a>
	{else}
	{control EditTitleForm}
	{/if}
	{/snippet}
</p>

Presenter:

<?php

class SomePresenter
{
	// ...

	// Následující konstrukci využívám k líné inicializaci modelu
	// a udržování jen jedné instance ve všech koutech Presenteru
	private $detailModel;

	/**
	 * @return Detail
	 */
	public function getDetailModel()
	{
		if ($this->detailModel === NULL)
			$this->detailModel = new Detail;
		return $this->detailModel;
	}

	// Proměnná, která určuje, zda zobrazíme formulář,
	// případně pro kterou položku
	protected $editTitle = FALSE;

	public function renderAction(/* ... */)
	{
		// Zde proběhne získání záznamu, popř. záznamů,
		// pokud jich je na stránce víc

		$this->template->editTitle = $this->editTitle;
	}


	public function createComponentEditTitleForm()
	{
		$form = new AppForm();
		// Formuláři nastavíme třídu ajax
		$form->getElementPrototype()->class('ajax');
		$form->addText('nazev', 'Název:');
		$form->addHidden('id');
		$form->addSubmit('ulozit', 'Uložit');
		$form->addSubmit('zrusit', 'Zrušit');
		// Funkce, která se zavolá po odeslání formuláře
		$form->onSubmit[] = array($this, 'EditTitleForm_Submit');

		/* Pokud používáš AJAXové odesílání formuláře, zkontroluj,
		zda je v JavaScriptu nastavené odesílání formuláře i na submit:
		$("form.ajax :submit").livequery("click", function () {
			$(this).ajaxSubmit();
			return false;
		});
		(vyžaduje plugin livequery, ale pokud používáš DataGrid,
		měl by být k dispozici)
		*/

		return $form;
	}

	public function EditTitleForm_Submit(Form $form)
	{
		$values = $form->getValues();
		if ($form['ulozit']->isSubmittedBy()) {
			$this->getDetailModel()->update(
				$values['id'],
				array(
					'nazev' => $values['nazev']
				)
			);
			/* Pokud používáš DibiTableX, můžeš použít variantu
			$this->getDetailModel()->update(
				NULL,
				$form->getValues()
			);
			*/
		}

		// Při ajaxu nepřesměrováváme, ale invalidujeme
		if (!$this->isAjax())
			$this->redirect('this');
		else {
			// Překreslíme jen jeden snippet, ne všechny
			$this->validateControl();
			$this->invalidateControl('title' . $values['id']);
		}
	}

	public function handleEditTitle($editId)
	{
		$this->editTitle = $editId;

		$row = $this->getDetailModel()->findDetail($id)->fetch();
		if ($row === FALSE)
			throw new BadRequestException('Záznam nebyl nalezen.');
		$this->getComponent('EditTitleForm')
			->setDefaults(array(
				'id' => $row->id,
				'nazev' => $row->nazev
			));

		// Překreslíme jen jeden snippet, ne všechny
		$this->validateControl();
		$this->invalidateControl('title' . $editId);
	}

	// ...
}
?>

Doufám, že kód bude alespoň k něčemu dobrý a že jsem tam těch bot neudělal mnoho.

//Úprava: opraven kód.

Editoval Panda (16. 7. 2009 19:02)

jarks
Člen | 94
+
0
-

Díky moc!
Napoprvé to nechodí. Toto: {snippet title$p->id} vygeneruje <div id="__title$p->id">, tzn. nepřeloží se to. Dál se vypíše jen název, žádný odkaz ani konec snippetu </div>. A vyhodí chybu: Undefined variable: editTitle. Budu se tomu snažit porozumět, snad na to nějak příjdu.

Taky vidím, že jsem předtím dělal chybu v tom, že jsem nepoužil zavináče. Mám předtím podínku {if $p} a pořád jsem dostával parse error s výpisem podobným tomuto, a nechápal jsem o co jde:

<?php } if ($_cb->foo = SnippetHelper::create($control, 'title$p->id')) { $_cb->snippets[] = $_cb->foo; ?>
Název: <?php echo TemplateHelpers::escapeHtml($p->nazev) ?>
Panda
Člen | 569
+
0
-

Tak jsem si to zkusil naroubovat do jedné mojí existující aplikace a chodilo mi to, až na drobnost – podmínku v šabloně jsem měl špatně, zapomněl jsem na negaci. Také jsem tam ještě přihodil přiřazení třídy ajax formuláři a pár komentářů.

Pokud se Ti nepřekládá název snippetu, zkusil bych nejnovější Nette. Nedávno CurlyBracketsFilter prodělal nějaké větší úpravy a refactoringy, může to být tím. Mám revizi 422 a ten překlad mi jel, dokonce to byla jediná věc, kterou jsem si ověřil, než jsem to začal psát.

Pokud to hlásí Undefined variable: editTitle, zkontroluj, jestli máš v metodě render toto přiřazení:

<?php
	$this->template->editTitle = $this->editTitle;
?>

//Doplnění: nebo pokud máš jen jeden prvek na stránce, tak ten snippet klidně můžeš pojmenovat jen title a do proměnné SomePresenter::$editTitle ukládat jen TRUE/FALSE.

Editoval Panda (17. 7. 2009 10:02)

jarks
Člen | 94
+
0
-

Funguje, díky! Musel jsem ale přejít na novější Nette. Proměnnou jsem skutečně zapomněl přiřadit.

Jeden problém ale zůstává: Jakmile kódu v šabloně předřadím podmínku, dostanu parse error, ať dávám zavináče nebo ne.

{if $p}
@{snippet title$p->id} //<<< parse error
Název: {$p->nazev}
@{if $editTitle != $p->id}
<a href="{link editTitle!, 'editId' => $p->id}" class="ajax">Editovat název</a>
@{else}
@{control EditTitleForm}
@{/if}
@{/snippet}

Přitom tato šablona je vkládána do layoutu také se zavináčem: @{include $content}. Zkusil jsem Nette rev422 a rev425.

Bohužel příliš nerozumím té zavináčové magii. Ani ona, ani značka {widget} není v popisu zatím vysvětlena.

Panda
Člen | 569
+
0
-

Makro widget skutečně vysvětleno není, je to relativně nová věc a celá komunita čeká na stabilizaci nových šablon a první článeček o změnách. Pak se to jistě dostane i do dokumentace, zatím jsme odkázání na zdroják. :)

Jinak u šablony zkus toto:

@{if $p}
{snippet title$p->id}
Název: {$p->nazev}
{if $editTitle != $p->id}
<a href="{link editTitle!, 'editId' => $p->id}" class="ajax">Editovat název</a>
{else}
{control EditTitleForm}
{/if}
{/snippet}
@{/if}

Zavináčovou magii se Ti pokusím trochu objasnit.

Zavináčová magie

Zavináčová magie není zas tak těžká. Je potřeba si uvědomit, co se u AJAXu děje – renderují se jen ty části, které Ty jako programátor chceš, aby se renderovaly. Zavináč šablonám řekne, že následující příkaz se má vykonat vždy, bez ohledu na AJAX. Pokud by u předchozí podmínky nebyl, podmínka by se vůbec nevykonala a s ní i celý obsah – šablona by tedy vůbec nevěděla, že uvnitř podmínky je nějaký snippet. Pokud tam ale zavináč dá, podmínka se vykoná, a uvnitř bloku opět jede selektivní vykreslování. Pokud se tedy narazí na snippet, zkontroluje se, zda je invalidní nebo validní. V případě, že je invalidní, vyrenderuje se a přidá se do AJAXové odpovědi. Snippety si svůj stav zjišťují samy, takže se před ně zavináče vůbec dávat nemusí.

Parse errory souvisí se současnou implementací snippetu. Nyní je v podstatě celá přeložená šablona (tzn. šablona, která prošla všemi filtry a vznikl PHP kód) uzavřena do podmínky:

<?php
if (SnippetHelper::$outputAllowed) {

	// Zde je přeložený kód šablony

}
?>

Zavináč nedělá nic jiného, než že tuto podmínku uzavře a pak znovu otevře:

<p>Odstavec 1</p>

@{control Form}

<p>Odstavec 2</p>

Se přeloží na (zformátováno pro větší čitelnost):

<?php
// U AJAXového požadavku SnippetHelper::$outputAllowed = FALSE
if (SnippetHelper::$outputAllowed) {
	// kód pro <p>Odstavec 1</p>
}

$control->getWidget("Form")->render();
/* Takto se mimo jiné překládá makro widget. Jde o alternativu k
	$control->getComponent("Form")->render();
*/

if (SnippetHelper::$outputAllowed) {
	// kód pro <p>Odstavec 2</p>
}
?>

Kód snippetu si vždy při překladu podmínku uzavře a po svém skončení jí opět otevře:

{* kód *}

{snippet main}
{* kód *}
{/snippet}

{* kód *}
<?php
if (SnippetHelper::$outputAllowed) {
	// kód
}

if ($_cb->foo = SnippetHelper::create($control, "main")) {
	$_cb->snippets[] = $_cb->foo;

	// kód
}

if (SnippetHelper::$outputAllowed) {
	// kód
}
?>

Podmínky a cykly jsou velmi prakticky překládány do své alternativní syntaxe. Pokud totiž člověk pak někde zapomene zavináč, zahlásí parser chybu, protože mu nesedí závorky:

{* kód *}
{if ...}
	{* kód v podmínce *}
	{snippet main}
	{* kód snippetu *}
	{/snippet}
	{* kód v podmínce *}
{/if}
{* kód *}
<?php
if (SnippetHelper::$outputAllowed) {
	// kód

	if (...):
		// kód v podmínce
	}
	/* Na tomto místě uzavíráme blok podmínky, která však
	byla vytvořena s alternativní syntaxí
		-> parser error
	*/

	/* K tomuto snippetu by se při renderování šablona vůbec nedostala -
	máme špatně zavináče a snippet je natvrdo uzavřen v bloku hlavní podmínky */
	if ($_cb->foo = SnippetHelper::create($control, "main")) {
		$_cb->snippets[] = $_cb->foo;

		// kód snippetu
	}

	if (SnippetHelper::$outputAllowed) {
		// kód v podmínce
	endif;
	/* Zde naopak zavíráme alternativní syntaxí podmínku,
	která byla vytvořena s normální syntaxí
		-> kdyby se sem parser dostal, tak parser error #2
	*/

	// kód
}
?>

Jen pro zajímavost kód, kde by alternativní syntaxe u podmínky použita nebyla – přeložit by šel, ale znamenal by úplně něco jiného!

<?php
if (SnippetHelper::$outputAllowed) {
	// kód

	if (...) {
		// kód v podmínce
	}
	// Konec podmínky!

	// K snippetu se opět šablona nedostane
	if ($_cb->foo = SnippetHelper::create($control, "main")) {
		$_cb->snippets[] = $_cb->foo;

		// kód snippetu
	}

	/* Tento "kód v podmínce" už ale s první podmínkou nemá co dělat -
	u normánlího požadavku by se vykonal vždy, u AJAXu nikdy */
	if (SnippetHelper::$outputAllowed) {
		// kód v podmínce
	}

	// kód
}
?>

Doufám, že jsem to napsal srozumitelně a že to alespoň někomu bude trochu užitečné. Zavináčová magie se však chystá Nette opustit, takže si o využitelnosti a přínosu nedělám moc velké iluze.

jarks
Člen | 94
+
0
-

Díky. Původně jsem to pochopil úplně obráceně.

Zachází se v té logice nějak jinak s {else}? Na něm to opět zhavaruje.

@{if $p}
    {snippet title$p->id}
        Název: {$p->nazev}
        {if $editTitle != $p->id}
            <a href="{link editTitle!, 'editId' => $p->id}" class="ajax">Editovat název</a>
            {else}
            {control EditTitleForm}
        {/if}
    {/snippet}
@{else} <p>Nic nebylo nalezeno.</p> //<< parse error
@{/if}
Panda
Člen | 569
+
0
-

Ve vygenerovaném kódu by měl být za závorkou před else středník. Může tam být vždy, středník navíc by nic poškodit neměl, takže jednoduchá oprava spočívá v úpravě proměnné CurlyBracketsFilter::$defaultMacros před vytvořením šablony, např. v metodě startup() u BasePresenteru nebo jako event do Application::$onStartup. Pokud to chceš mít opravené rychle a jednoduše, doporučuji tu metodu BasePresenter::startup().

CurlyBracketsFilter::$defaultMacros['else'] = '<?php ; else: ?>';

Po úpravě musíš promazat temp.

Otázkou zůstává, zda to má cenu reportovat jako bug, když nám zavináče snad již brzy pojdou…

//Doplnění: tak to nedělá paseku jen před zavináčem, takže to jdu reportovat jako bug.

//Doplnění 2: celá oprava vypadá nyní takto:

CurlyBracketsMacros::$defaultMacros['else'] = '<?php ; else: ?>';
CurlyBracketsMacros::$defaultMacros['elseif'] = '<?php ; elseif (%%): ?>';

Editoval Panda (29. 7. 2009 9:55)

jarks
Člen | 94
+
0
-

Díky moc za pomoc – už to dělá, co to dělat má. Zavináče jsou opravdu magie a myslím, že je dobře, že se uvažuje o změně.

Klokan
Člen | 47
+
0
-

Chtel bych se optat, jaké je nejlepsi reseni tohoto problemu v sablone. Zde je reseni na zaklade atributu tridy, ktery se prepina a v sablone se formular zobrazi na zaklade podminky.

<?php
Presenter:
$this->template->editTitle = $this->editTitle;

Template:
{if $editTitle != $p->id}
<a href="{link editTitle!, 'editId' => $p->id}" class="ajax">Editovat název</a>
{else}
{control EditTitleForm}
{/if}
?>

Je možné to řešit ještě jiným způsobem, např. zjištěním z URL ?