Drobná komponenta, která mi vyloženě v Nette chybí a jako bonus modály pro Bootstrap

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

V Nette máme UI\Multiplier, velmi užitečnou věcičku když člověk potřebuje vyrobit hromadu formulářů. Trochu problém nastává ve chvíli, kdy chceme takové formuláře překreslovat pomocí Ajaxu nebo načítat do modálu. Buď na to musíme použít dynamické snippety (přiznám se, že jsem pořád nepochopil, jak se s nimi pracuje), nebo můžeme formulář přesunout do subkomponenty a tu překreslit. No jo, ale přeci si nebudu dělat pokaždé novou komponentu jen kvůli formuláři…

Proto jsem si vyrobil takovou malou věcičku inspirovanou Multiplierem:

use Nette, Nette\Application\UI;

class ControlWrapper extends UI\Control {

    protected $factory;

    protected $controlName;

    public function __construct($factory, $controlName = 'control') {
        parent::__construct();
        $this->factory = Nette\Utils\Callback::check($factory);
        $this->controlName = $controlName;
    }

    protected function createComponent($name) {
        if($name === $this->controlName) {
            return call_user_func($this->factory, $name, $this);
        }
    }

    public function render() {
        $template = $this->getTemplate();
        $template->setFile(__DIR__ . '/wrapper.latte');
        $template->controlName = $this->controlName;
        $template->render();
    }
}
# wrapper.latte
{snippet control}
    {control $controlName}
{/snippet}

Něco takového mít už v základu v Nette by IMHO vůbec neuškodilo, spíš naopak. Jak člověk postupně Ajaxovatí aplikaci, zjistí, že se mu nechtějí dělat obalové komponenty na formuláře a že spousta komponent má zhruba takovouhle šablonu, tak proč nevytvořit pidivrstvičku?

{snippet nejakyRandomNazev}
    # komponenta
{/snippet}

Dá se to celkem parádně rozšiřovat. Je libo třeba Bootstrap modální okna univerzálně pro jakoukoliv komponentu?

class ModalControl extends ControlWrapper {

    public function modalWindow($title='') {
        $presenter = $this->getPresenter();
        if($presenter->isAjax()) {
            $presenter->payload->modal = array(
                'snippet' => $this->getSnippetId('control'),
                'title' => $title
            );
            $this->redrawControl('control');
            return TRUE;
        }
        return FALSE;
    }

}
// nette.ajax.js extension
$.nette.ext('bootstrapModal',{
    success: function(payload) {
        if(payload.modal && payload.snippets) {
            var modal = $('#modal');
            var modalBody = $('#modal .modal-body');
            var modalTitle = $('#modal .modal-title');
            if(payload.snippets[payload.modal.snippet]) {
                modalTitle.html(payload.modal.title);
                modalBody.html('<div id="' + payload.modal.snippet + '">' + payload.snippets[payload.modal.snippet] + '</div>');
                payload.snippets[payload.modal] = null;
                modal.modal();
                $.nette.load();
            }
        }
    }
});

Komponentu jednoduše obalíme podobně jako multiplierem

protected function createComponentModalForm() {
    return new ModalControl(function() {
        $form = new UI\Form;
        // ...
        return $form;
    });
}

A zobrazíme modální okno

public function handleShowModal() {
    if(!$this['modal']->modalWindow('nadpis')) {
        // if not ajax do redirect or something...
    }
}

Samozřejmě je možné kombinovat s multiplierem

protected function createComponentModalForm() {
    return new UI\Multiplier(function($id) {
        return new ModalControl(function() use ($id) {
            $form = new UI\Form;
            // ...
            return $form;
        });
    });
}

public function handleShowModal($id) {
    $this['modal-' . $id]->modalWindow();
}

Editoval Zax (14. 5. 2014 10:37)

Filip Procházka
Moderator | 4668
+
+4
-

Dovolil bych si tvrdit, že modaly mám vyřešené krapet lépe a výrazně jednodušeji :)

Js extension

$.nette.ext('bs-modal', {
	init: function () {
		this.ext('snippets', true).after($.proxy(function ($el) {
			if (!$el.is('.modal')) {
				return;
			}

			$el.modal({});

		}, this));

		$('.modal[id^="snippet-"]').each(function () {
			var content = $(this).find('.modal-content');
			if (!content.length) {
				return; // ignore empty modal
			}

			$(this).modal({});
		});
	}
});

A pak v komponentě tohle.

<div n:snippet n:inner-if="$renderModal" class="modal fade">
	<div class="modal-dialog">

Kdykoliv se mi ajaxem vrátí element jehož snippet má classu modal, tak ho zobrazí a když při načtení stránky už je v těle a má nějaké elementy tak ho taky zobrazí.

Tedy je možné například velice jednoduše poslat odkaz na signál který zobrazuje modal, kde už je modal vykreslený v těle a nemusí se řešit speciální zpracování situace, že se má zobrazit (což jsem u hodně aplikací viděl, že vůbec neřeší).

Zax
Člen | 370
+
0
-

A používáš obalovou komponentu nebo to celé <div n:snippet …> píšeš rovnou do všech těch komponent?

Protože to je celá pointa mého příspěvku – mít komponentu, která nedělá nic jiného, než že nějakou komponentu zabalí do snippetu. Pak už jen záleží na tvé fantazii, co s tím vymyslíš, já jako ukázku udělal takovýto modál. Způsobů řešení modálů je samozřejmě asi tolik, kolik je programátorů, co kdy nějaký modál kdy dělali.

Při psaní jsem navíc vycházel z tohoto:

Overlapping modals not supported
Be sure not to open a modal while another is still visible. Showing more than one modal at a time requires custom code.

(http://getbootstrap.com/javascript/#…)
a neměl jsem zatím šanci zkoumat jaké jsou možnosti.

Ten hlavní vtip je v tom, že člověku nemusí hned docvaknout (však mně to docvaklo až asi po roce používání Nette… heh), že by si mohl udělat jednoduchý univerzální obalovač komponent, a přitom to podobně jako multiplier řeší celkem dost věcí. A princip je stejný – jde jen o to, dostat komponentu jakoby o úroveň níž.

Filip Procházka
Moderator | 4668
+
0
-

Já si právě myslím, že dělat tu obalovou komponentu je trochu plýtvání prostředky. Přece jenom je to jeden řádek latte… DRY samozřejmě znám, ale je potřeba všechno dělat s mírou. Nevidím problém mít ten jeden řádek latte v každé komponentě.

Navíc jsi použil dost obskurdní invalidaci. Chtěl bych vidět jak v tom tvém systému v jednom response pošleš dva snippety :)

U formulářů by mohlo dávat smysl mít obalovací komponentu, protože nemají vlastní šablonu, na druhou stranu je imho lepší udělat to rovnou pořádně.

akadlec
Člen | 1326
+
0
-

Tady bych to viděl jako Filip a mám i podobné řešení. Pokud se mi v response objeví snippet kde je zmínka o modalu tak jej zpracuje a to ještě tak že jej buď přímo zobrazí a nebo nechá skrytý a nabinduje ho na nějaké tlačítko. Např. mám modal na form a v tom formu mám další modal s krátkým info co tam vložit ale tento druhý modal zobrazím až po kliku na info button.

Zax
Člen | 370
+
0
-

Jojo děkuji za názory! Jako člověk, co má za sebou jenom SŠ, všechno se učí sám a na vejšku nemá moc času, prostředků ani ochoty, jsem rád za každý názor.

Dělat formuláře jako potomky UI\Control mě taky napadlo, na znovupoužitelnost je to asi ideální, ale zas je to dost psaní navíc pokud člověk potřebuje konkrétní jednoúčelový formulář (a třeba přes multiplier), který se nikde jinde nepoužije.

@Filip Procházka: Nepochopil jsem to s tou invalidací.. samozřejmě si můžu poslat víc snippetů a taky to i dělám. Mám například snippet, který obaluje seznam, kliknutím na jedno tlačítko u položky si zobrazím inline editaci a kliknutím na druhé tlačítko si tu samou editaci zobrazím v modálu. Aby se mi nemlátily IDčka v DIVech, tak si posílám snippet s modálem a je-li třeba tak i zároveň snippet, který mi překreslí seznam a zavře inline editaci. Sice opět asi špinavé řešení, ale funkční. Další snippety si s modálem pošlu celkem v klidu.

Editoval Zax (15. 5. 2014 17:11)

MW
Člen | 615
+
-3
-

Filip Procházka napsal(a):

Dovolil bych si tvrdit, že modaly mám vyřešené krapet lépe a výrazně jednodušeji :)

Nemel by jsi nekde prosim nejaky priklad ?
Potrebuji co nejjednodušeji udelat editacni modal form a nachazim stále jen hodne slozita reseni a vždy zustanu viset u nezavreni okna, validace a nebo rozhozeni obsahu modalu v pripade naplneni formu (setDefaults()).

Moc diky..

mishak
Člen | 94
+
0
-

Jen prosím pánové modály s mírou. Většinou jdou obejít zamyšlením se nad původním problémem a optimalizací rychlosti stránek (aby načítání bylo pocitově okamžité).

Například implementace funkce „Vrátit zpátky“ je lepší než potvrzení smazání.

Editoval mishak (16. 5. 2014 0:28)

akadlec
Člen | 1326
+
0
-

@MW: nezavření modalu máš zřejmě proto že se provede jeho „vytržení“ z DOMu a umístění na konec a přitom ti snippet zůstane na původním místě. Nehledej v tom magii, překreslovaní formu atd dělej stejně jako by to modal nebyl a hlídej si manipulaci v DOMu. Řešení se pak musí udělat podle toho jaké modály se použijou (bootstrap, jquery atd.)

mkoubik
Člen | 728
+
0
-

Pokud nepotřebuješ nějaké extra efekty pro otevírání a zavírání modalu, tak žádný javascript nepotřebuješ. Stačí standardní podpora překreslování snippetů ajaxem a nastylovat ten modál tak, aby vypadal jako modál. Pak nějaká komponenta se signály open! a close! a není co zbytečně řešit.

MW
Člen | 615
+
0
-

akadlec napsal(a):

@MW: nezavření modalu máš zřejmě proto že se provede jeho „vytržení“ z DOMu a umístění na konec a přitom ti snippet zůstane na původním místě. Nehledej v tom magii, překreslovaní formu atd dělej stejně jako by to modal nebyl a hlídej si manipulaci v DOMu. Řešení se pak musí udělat podle toho jaké modály se použijou (bootstrap, jquery atd.)

Snazil jsem se o bootstrap. Ano, udelal jsem to jako bez modalu a pak jen obaloval v latte… poslední pokus po setDetafult() skončil takto .. v a přitom se prazdny form vykreslil v poradku.

Resil s nedoresil jsem ta tady.

Jak predejit takovému „vytrzeni“ z DOMu?

Omlouvam se, JS není rozhodne moji doménou a snazim se implementovat občas a spise celistve věci.. ale presto se je snazim pochopit a nekdy udelat krok po kroku, jako třeba prave ted ten modal :-)

Prosim tedy nekoho o nejaky jednoduchy priklad..

Moc diky!

Editoval MW (16. 5. 2014 14:25)

Climber007
Člen | 105
+
0
-

@FilipProcházka Chápu to správně, že máš signál na otevření modalu, který jednoduše hodí do `$renderModal` `TRUE`? Bez nějaká abstrakce máš de facto ve všech komponentách s modalem tenhle signál? Nic víc není potřeba?

Zatím jsem se dostal jen na teoretickou úroveň, ale pořád přemýšlím co ještě jsem zapomněl až to budu nahazovat :-)

Filip Procházka
Moderator | 4668
+
+5
-

Výsledné chování si můžeš prohlédnout na https://help.kdyby.org/, jak vidíš, tak když klikneš na Log In, tak se modal načte ajaxem a když klikneš pravým na Log In a dáš otevřít v novém okně, tak se modal vyrenderuje rovnou a js ext ho tedy rovnou zobrazí.

Podle mě je to nejlepší možné řešení, protože všechny takto udělané modaly mají pak vlastní url a můžeš tedy snadno do aplikace přidat history.nette.ajax.js. A protože to využívá snippety „správně“, tak si jich můžu poslat kolik chci a s ničím mi to nekoliduje – místo toho abych obcházel systém tak ho využívám.

Zax
Člen | 370
+
0
-

@FilipProcházka

FYI moje řešení nic neobchází – snippety se normálně využívají, akorát se navíc posílá idčko snippetu, který se má zobrazit jako modál (dalo by se posílat pole idček, ale počítal jsem s omezením bootstrapu na jeden modál zároveň) a to si pak odchytne a zpracuje JS.

Víš, co se stane, když si člověk vypne JS, vleze na help.kdyby.org a klikne na „log in“? Nestane se vůbec nic, protože ty to rovnou programuješ v modálu. Já to mám udělaný tak, že si nejdřív udělám obyčejnou komponentu, normálně ji vložím do stránky (vyzkouším jak funguje) a pak tomu na pár řádků řeknu, aby se to otevíralo v modálu. Pokud má uživatel JS vyplý (klikne na link a přejde na pevnou URL adresu), pak se mu zobrazí komponenta na původním místě ve stránce (nebo ho můžu přesměrovat jinam, cokoliv) a nemá problém.

BTW tohle téma by beztak mělo umřít, byl jsem totální noob když jsem ho zakládal a dneska bych to udělal asi jinak, nehledě na to, že jsem za těch půl roku neměl potřebu udělat jeden jediný modál, ale stojím si za tím, že mé řešení je přinejmenším přístupnější (a koncového zákazníka nezajímá kód, zajímá ho, jestli to funguje) :-P Každopádně s výrazy typu „nejlepší možné řešení“ by měl být člověk opatrný, protože to má většinou do pravdy hodně daleko ;-)

(prosím neber to osobně)

newPOPE
Člen | 648
+
+2
-

@Zax robi si ako sa ti paci!

@FilipProcházka napisal, ze podla neho je to naj. mozne riesenie cize toto si moze tvrdit kolko sa mu len chce.

PS: „vypne JS“, s tymto by som pred par rokmi suhlasil ale v dobe FB a SPA aplikacii je to uplna blbost…

Edit: Prave som vyskusal Filipovo riesenie. Je luxusne! Nechces to dat ako ext pre nette.ajax.js?

Editoval newPOPE (21. 11. 2014 11:06)

Filip Procházka
Moderator | 4668
+
0
-

Víš, co se stane, když si člověk vypne JS, vleze na help.kdyby.org a klikne na „log in“? Nestane se vůbec nic, protože ty to rovnou programuješ v modálu.

To je problém toho, jak moc mi vadí, že web nebude správně fungovat lidem co si vypnout javascript. A mně to nevadí vůbec :) Ale díky tobě jsem si uvědomil zásadní věc, díky!

Teď mě napadlo, že by se to moje dalo vylepšit tak, že bych měl odkaz na samostatný pohled v tom odkazu, který na tom webu stejně je a pak bych jenom přetížil href přes nějaký další extension.

<a href="/login/" data-href="/?do=login-showModal" class="ajax" rel="nofollow">Log in</a>

Každopádně jsi mi nasadil brouka do hlavy, a ikdyž se mi rozhodně nelíbí ten způsob jakým říkáš javascriptu že má otevřít ten modal, tak udělat si obalovou komponentu by nemuselo být úplně špatné. Třeba k tomu taky časem dojdu :)


@newPOPE myslíš poslat pullrequest @vojtech.dobes, nebo udělat samostatné repo? Nevím jestli je vhodné k Vojtovi cpát takhle moc custom věci.

newPOPE
Člen | 648
+
0
-

@FilipProcházka to uz je v zasade jedno ci samostatne repo alebo nie. Ide mi o rozsirenie nette.ajax.js tak aby vedelo pracovat s modalmi.

Nakolko sa mi paci sposob akym to riesis (a tw bootstrap v 4.* odstrihne remote loading ajaxom ktory momentalne hojne vyuzivam) tak hlasujem za rozsirenie. Skus nieco nahodit a rad prispejem :).

akadlec
Člen | 1326
+
+1
-

Pokud můžu přidat svou vlastní trochu do mlýna/pranice ;) Já ten modál řeším tak aby frčel i bez JS. Okamžik kdy třeba @FilipProcházka chce na help.kdyby.org otevřít modal pro přihlášení by to v mém řešení nebyl signál, ale klasická action, protože se v podstatě jakoby načítá nová stránka. A to zda se otevře modal či nová stránka určí samotný JS a appka. Když příjde na server request z ajaxu tak se pošle zpět snippet modalu, pokud ovšem přijde klasický request tak se otevře samostatná stránka pro přihlášení, případně se přeformátuje do modalu.

A aby se dokáza použít snippet pro modal, tak mám v hlavním layoutu snippet pro modaly. Na tomto snippetu naslouchá JS a pokud se mu tam něco objeví tak jednoduše aktivuje modal. Není tám žádné znásilňování komponent, JS, atd. Jediné co je navíc je že phpku je udělána v presenteru detekce zda zobrazit modal či klasickou stránku…kdyby to někoho zajímalo tak to nějak z appky vyzobu jako ukázku ;)

Šaman
Člen | 2635
+
0
-

akadlec napsal(a):

Kkdyby to někoho zajímalo tak to nějak z appky vyzobu jako ukázku ;)

Zajímalo, moc. S JS se nekamarádím, ale mít možnost jen pomocí třídy (třeba ajax modal) ovlivnit, zda se okno otevře modální, nebo běžné, to by bylo super.

Filip Procházka
Moderator | 4668
+
0
-

@akadlec pochlub se :)

JakubJarabica
Gold Partner | 184
+
0
-

My už dlhšiu dobu používame takmer identické riešenie ako @akadlec, len s tým rozdielom, že nebloatujeme globálny layout, ale v payloade posielame názov snippetu, ktorý sa má renderovať do modalu. Mne osobne to príde takto čistejšie – v presenteri jednak riešim invalidáciu snippetov, tak jeden riadok naviac ma nezabije(a závislosť na názve snippetu v šablóne už je pri volaní redrawControl). Fajn je aj to, že často potrebujem invalidovať viac snippetov – po pridaní produktu do košíka z listingu produktov chcem v modali zobraziť toto, ale zároveň v hlavičke zinvalidovať celkovú sumu + počet ks a nie som tým limitovaný. S vypnutým JS sa mi vykreslí normálna akcia a mám kľud :)

akadlec
Člen | 1326
+
+3
-

Takže základ je snippet blok v layoutu:

<html>
<body>

	....

	<!--  PopUp windows -->
	<div n:snippet="modalWindowBlock">{block #modalWindow}{/block}</div>

	.....

</body>
</html>

Je to uděláno jako snippet a do něj se hází obsah bloku které se vytváří pak v jednotlivých šablonách akcí a já to mám uděláno tak že buď se načte šablona modalu při ajaxovém požadavku a nebo klasická šablona při klasickém požadavku, ale dá se to řešit i tak že vše bude v modalu, ale mě osobně se to nelíbilo ;)

Takže v šabloně akce např. v security presenteru:

{layout '@layout.latte'}

{define #modalWindow}
	{include #parent}
	// Tato podmínka jednoduše reprezentuje detekci isAjax() z presenteru
	// a to proto aby se mě při klasickém požadavku nenačtely oba formy, tedy klasický a modální
	{if $inModalWindow}{control passwordForm:modal}{/if}
{/define}

{define #content}
	<div class="page-header">
		<h1><i class="fa fa-key"></i> Změna hesla</h1>
	</div>

	//...klasika komponenta formuláře a nebo obyč komponenta,
	//s tím že jí určím že se má použít šablona klasického zobrazení
	{control passwordForm:default}
{/define}

No a magie co provede aktivaci modalu je pomocí extension z nette.ajax.js ale není problém si ji napsat jako solo plugin atd.

	/**
	 * Theme styling
	 */
	$.nette.ext('themeElements', {
		// Init form elements
		init: function() {
			// Apply form elements styling
			this.initialize($('body'));
		},
		success: function (payload) {
			var snippetsExtension = this.ext('snippets');

			// Some snippets were returned...
			if ( payload.snippets ) {
				// ...go through all snippets
				for ( var id in payload.snippets ) {
					var $el = snippetsExtension.getElement(id);

					// Check if snippet element exists
					if ( $el.exists() ) {
						// Apply form elements styling
						this.initialize($el);
					}
				}
			}
		}
	}, {
		initialize : function($el){
			// Activate modal windows
			$('.modal.display', $el)
				.modal();
		}
	});

No a funguje to jendoduše, musí tam být classa modal a display to proto že občas si tam chci načíst modální okno ale zobrazit jej podle podmínky proto ta classa display

Takže když proběhne klasický ajaxový dotaz a nette.ajax.js naplní daný snippet pro modaly je aktivuje pak tento js.

No a aby se invalidoval ten snippet pro modaly, stačí si to BasePresenteru přidat beforeRender metodu:

Class BasePresenter extends \Nette\Application\UI\Presenter
{
	//....

	/**
	 * Process modal window displaying
	 */
	protected function showModalWindow()
	{
		// Check if action signal was received...
		if (!$this->getSignal()) {
			// ...if not redraw correct modal snippets
			$this->redrawControl(NULL, FALSE);
			$this->redrawControl('modalWindowBlock');
		}
	}

	protected function beforeRender()
	{
		parent::beforeRender();

		// For ajax request...
		if ($this->isAjax()) {
			if ($this->getSignal() === NULL && $this->isSignal === NULL) {
				// Invalidate content snippet
				if (!$this->isControlInvalid('modalWindowBlock')) {
					// Snippet pro titulek, jen pro ty co jej mají že ;)
					$this->redrawControl('title');

					// Když tak ještě pro jistotu předávám title hlavičky, ale opět to je jen moje customizace
					// aby se při ajax požadavku změnil title
					$this->payload->title = $this['header']->getTitleString();
				}
			}
		}
	}

	/**
	 * Detect if is possible use modal window
	 *
	 * @return bool
	 */
	public function useModalWindow()
	{
		// For ajax request and nod mobile device show it in modal
		if ($this->isAjax() && (!$this->mobileDetect->isMobile() && !$this->mobileDetect->isTablet())) {
			// Check if request is submitted form...
			if ($this->httpRequest->isMethod('post')) {
				//...& check if modal flag is present or not
				if ((bool) $this->httpRequest->getPost('isInModal', FALSE)) {
					// Show in modal window
					return TRUE;

				} else {
					// Show in classic window
					return FALSE;
				}

			} else {
				// Show in modal window
				return TRUE;
			}

		// For mobile device and normal request show it in page
		} else {
			return FALSE;
		}
	}
}

No a jak je vidět je tady navíc jedna metoda showModalWindow která v podstatě provede invalidaci/validaci patřičných snippetu aby se modal korektně zobrazil. Původně jsem měl invalidaci modal snippetu přímo v before render ale občas to házelo prasárny takže sem to vyřešil touto metodou se kterou pak v jednotlivích akcích pracuju následovně:

class SecurityPresenter extends BasePresenter
{
	/**
	 * Change user password action
	 */
	public function actionPassword()
	{
		// For ajax request and nod mobile device show it in modal
		if ($this->useModalWindow()) {
			// Init modal window displaying
			$this->showModalWindow();
		}

		// Get password form component
		$this['passwordForm']
			// Get component form
			->getForm()
				// When form was successfully processed
				->onSuccess[] = (function(Application\UI\Form $form, $values) {
					// When the form is not in modal window...
					if (!$values['isInModal']) {
						// ...redirect to settings page
						$this->go('Settings:');
					}
				});
	}
}

No a jak je vidět je tady ještě jedna metoda useModalWindow a tu mám proto protože chci rozlišovat kdy ty modaly zobrazit a kdy ne. Např. pokud se stránka zobrazuje na nějakém mobilním zařízení co má malé rozlišení tak tam prostě chci zobrazit form a nebo okno klasicky proto ta detekce mobilního zařízení. Dále co se tam děje je detekce POST/GET a to je z jednoho prostého důvodu, potřebuju nějak ošetřit kdy form zavřít a kdy jen překreslit a tak sem si do těch formů přidal jeden hidden input který mě určí zda je to v modalu nebo ne a to je opět jen proto protože ten form se může zobrazit klasicky a nebo v modalu ;)

No a abych to shrnul základ se dá udělat jen z toho snippetu v layoutu, JS části co otevře okno, a pak v beforeRender metodě invalidovat onen snippet.

A jak tady zmiňuje @JAM3SoN že chce invalidovat více snippetu, tak to samo jde a já je taky občas invaliduju.

No snad sem to popsal nějak pochopitelně :-D