Jak na modální okna v nette?

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

Zdravim,

na stránkách používám modální okna a to tak, že v BasePresenteru mám definované metody setDialog($blockName) a closeDialog(). setDialog nastaví jméno bloku v templatě, který se má zobrazit v modálním okně (podobně jako setView() nastaví view). setDialog() volám v handle funkci (přijde mi to logické, když se nemění view). Problem ale nastane, když v mod. okně zobrazím formulář. Při odeslání formuláře se volá funkce pro zpracování formu (do=form-submit) místo handle pro zobrazení modalu (do=zobrazitForm), takže v případě, že nastane chyba při odeslání formu, tak se formulář už nezobrazí s výpisem chyb, prostě se nic nestane. Napadla mě 2 řešení :

  1. V případě chyby formuláře znovu volat setDialog()
  2. v metodě setDialog() nenastavovat property $blockName, ale uložit název bloku do session, tzn. že se form bude zobrazovat dokud nezavolám closeDialog()

Teď mě napadlo, jestli by se nedalo využít perzistentních parametrů, to jsem ještě nezkoušel.
Jak řešíte modální okna vy?

khero2t
Člen | 6
+
0
-

Ahoj,
Já modální okna řeším takto.
Vyhovuje mi to snad na všechny možné případy.
Určitě by to šlo napsat mnohem pěkněji, ale aspoň pro inspiraci :)
Když chci zobrazit v dialogu např nějakou komponentu tak ji volám následovně.

<?php
public function handleShowDialog(){
	$this->dialog()->show($this['myComponent'])->width("600px");
	// nebo
	$this->dialog()->show("Zpráva")->width("600px");
	// nebo
	$this->dialog()->show(Array("<div class='form'>",$this['myComponent'],"</div>"))->width("600px");
}
?>

Dialog pak mohu skrýt např následovně

<?php
public function handleCloseDialog(){
	$this->dialog()->close();
}
?>

v BasePresenteru mám vytvořenou továrničku na dialog

<?php
public function createComponentDialog($name) {
	return new \Any\Components\Dialog($this, $name);
}

/** @var \Any\Components\Dialog */
public function dialog(){
	return $this['dialog'];
}
?>

a v šabloně @layout.latte

{control dialog}

Samotný kód komponenty

<?php
namespace Any\Components;

class Dialog extends \Any\AnyControl {

	protected $config;

	public function show($config, $title = null, $width = "450px", $height=null){
		$this->config = $config;
		$this->template->config = $config;
		$this->template->title = $title;
		$this->template->width = $width;
		$this->template->height = $height;
		$this->invalidateControl();
		return $this;
	}

	public function title($title){
		$this->template->title = $title;
		return $this;
	}

	public function width($width){
		$this->template->width = $width;
		return $this;
	}

	public function height($height){
		$this->template->height = $height;
		$heightContent = (int)trim(str_replace("px", "", $height))-30;
		$this->template->heightContent = $heightContent."px";
		return $this;
	}

	public function close(){
		$this->template->close = true;
		$this->invalidateControl();
	}

	public function flashMessage($message, $type = 'info') {
	    parent::flashMessage($message, $type);
	    $this->invalidateControl("flashes");
	}

	public function render() {
		$this->template->componentName = $this->getName();
		$this->template->setFile(dirname(__FILE__) . '/dialog.latte');
		$this->template->render();
	}

}
?>

Šablona komponenty

{snippet}
{if isset($config) && !isset($close)}
<div id="customDialog{$componentName}">
    <div id="customDialogBtnClose{$componentName}"></div>
    {if $title!=null}<div id="customDialogTitle{$componentName}">{$title}</div>{/if}
    {snippet flashes}
	{foreach $flashes as $flash}<div class="flash {$flash->type}">{$flash->message}<div class="closeFlashes"></div></div>{/foreach}
    {/snippet}

    <div class="clear"></div>
    <div class="customDialogContent{$componentName}">
		{if is_array($config)}
			{foreach $config as $c}
				{if $c instanceof \Nette\Application\UI\Control}
					{$c->render()}
				{else}
					{!$c}
				{/if}
			{/foreach}
		{else}
			{if $config instanceof \Nette\Application\UI\Control}
				{$config->render()}
			{else}
				{!$config}
			{/if}

		{/if}
	</div>
</div>

<script type="text/javascript" n:syntax="double">
/*
 * jQuery resize event - v1.1 - 3/14/2010
 * http://benalman.com/projects/jquery-resize-plugin/
 */
(function($,h,c){var a=$([]),e=$.resize=$.extend($.resize,{}),i,k="setTimeout",j="resize",d=j+"-special-event",b="delay",f="throttleWindow";e[b]=250;e[f]=true;$.event.special[j]={setup:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.add(l);$.data(this,d,{w:l.width(),h:l.height()});if(a.length===1){g()}},teardown:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.not(l);l.removeData(d);if(!a.length){clearTimeout(i)}},add:function(l){if(!e[f]&&this[k]){return false}var n;function m(s,o,p){var q=$(this),r=$.data(this,d);r.w=o!==c?o:q.width();r.h=p!==c?p:q.height();n.apply(this,arguments)}if($.isFunction(l)){n=l;return m}else{n=l.handler;l.handler=m}}};function g(){i=h[k](function(){a.each(function(){var n=$(this),m=n.width(),l=n.height(),o=$.data(this,d);if(m!==o.w||l!==o.h){n.trigger(j,[o.w=m,o.h=l])}});g()},e[b])}})(jQuery,this);
</script>

<script type="text/javascript">
	jQuery.fn.center = function () {
		this.css("position","fixed");
		this.css("top", Math.max(0, (($(window).height() - this.outerHeight()) / 2) ) + "px");
		this.css("left", Math.max(0, (($(window).width() - this.outerWidth()) / 2) + $(window).scrollLeft()) + "px");

		if(this.height() > $(window).height()){
		    newHeight = ($(window).height()-50)+"px";
		    newHeightContent = ($(window).height()-70)+"px";
		    this.css("height", newHeight);
		    $('.customDialogContent{!$componentName}').css("height", newHeightContent);
		    $('.customDialogContent{!$componentName}').css("overflow-y", "scroll");
		}

		return this;
	}

	$(function(){
		$('#customDialogBtnClose{!$componentName}').click(function(){
			$(this).parent("#customDialog{!$componentName}").remove();
			return false;
		});

		$('#customDialog{!$componentName}').center();

		$('.customDialogContent{!$componentName}').resize(function(){
			$('#customDialog{!$componentName}').center();
		});
	});

</script>



<style>
    #customDialog{$componentName}{
	display: block;
	position: fixed;
	top: 200px;
	left: 400px;
	width: {$width};
		{if $height!=null}
		height: {$height};
	{/if}
		background-color: #fff;
	-webkit-border-radius: 5px;
	-moz-border-radius: 5px;
	border-radius: 5px;
	padding: 20px;
	box-shadow: 10px 10px 30px #888;
	z-index: 9999;
    }

    .customDialogContent{$componentName} {
	{if $height!=null}
		height: {$heightContent};
		overflow-y: scroll;
	{/if}
    }

    #customDialogTitle{$componentName} {
	font-size: 16px;
	float: left;
	margin-bottom: 15px
    }

    #customDialogBtnClose{$componentName} {
	cursor: pointer;
	background: url('{$basePath}/anylibs/libs/Any/components/Dialog/close.png') no-repeat;
	float: right;
	margin: -12px -12px 0 0;
	width: 20px;
	height: 20px;
    }

    #customDialog{$componentName} .flash {
	text-align: left;
	margin: 20px 0;
    }
</style>

{/if}
{/snippet}
mildhouse
Člen | 27
+
0
-

Ahoj
tohle ale neřeší ten problém když v dialogu zobrazím formulář. Při odeslání formuláře se volá funkce pro zpracování formu (do=form-submit) místo handleShowDialog() (do=showDialog), takže v případě, že nastane chyba při odeslání formu, tak se formulář už nezobrazí s výpisem chyb. Stejně tak, pokud je v handleShowDialog() nějaké ověřování – pak tam bude bezpečnostní díra. Jako nejlepší řešení mě napadá uložit si to do session, třeba :

<?php

public function actionDefault(){
    $session = $this->getSession('dialog');
    if(isset($session->starter)){
	$funcName = $session->starter;
        $this->$funcName();
    }
}

public function handleShowDialog(){
    $this->getSession('dialog')->starter = 'showDialog';
    $this->showDialog();
}

public function showDialog(){
    $this->dialog()->show($this['myComponent'])->width("600px");
    // nebo
    $this->dialog()->show("Zpráva")->width("600px");
    // nebo
    $this->dialog()->show(Array("<div class='form'>",$this['myComponent'],"</div>"))->width("600px");
}
?>

Takhle by se nastavil dialog při volání handleShowDialog() a i při odeslání formuláře v actionDefault().

Další možností je neukládat to do session, ale jako perzistentní parametr.

Ale líbí se mi, jak to máš zabalené v komponentě, to se mi nedařilo. Takže jako inspirace super. Zkusím něco spáchat a pak to sem taky dám.

Filip111
Člen | 244
+
0
-

Ahoj, vyzkoušel jsem si vytvořit komponentu pro práci s modálními okny – je to kopie od khero2t, a trochu jsem si to upravil k obrazu svému. Především to pracuje s bootstrap modal okny, ale část v Nette je skoro stejná.

Narazil jsem na problém, při zpracování formuláře z komponenty, která je zobrazena uvnitř modálního okna, tedy uvnitř jiné komponenty.
Háže mi to The signal receiver component 'EditAttModal-attDetailForm' is not found.

Protože komponenta modálního okna je zobrazena pomocí signálu, asi se to nějak hádá se signálem vnořené komponenty, ale nenapadá mě jak to vyřešit.

Vytvoření komponenty pro práci s modálními okny (v presenteru):

public function createComponentModalDialog($name) {
	return new \web132\Components\ModalDialog($this, $name);
}

public function dialog() {
	return $this['modalDialog'];
}

Zobrazní modálního okna s vykreslením jiné komponenty (v presenteru):

public function handleEditAttModal($contentId, $attId) {
	$this->dialog()
		->setTitle('Editace přílohy')
		->show(new \web123\Components\EditAttModal(
			$this,
			'EditAttModal',
			$attModel,
			$attId)
		);
}

Komponenta EditAttModal umí zobrazit formulář pro editaci, ale taky volá jinou komponentu pro oříznutí obrázku podle potřeby (jen zdůvodnění, proč nevolám samotný formulář v modálním okně, ale komponentu).
Formulář v komponentě EditAttModal a jeho zpracování:

public function createComponentAttDetailForm() {
	$form = new BaseForm;
	$form->addText('title', 'Název', 50, 100);
	$form->addTextArea('description', 'Popis', 40, 2);
	$form->addSubmit('send', 'Uložit');
	$form->onSuccess[] = callback($this, 'editFormSubmitted');
	return $form;
}

public function editFormSubmitted(BaseForm $form) {
	// NEZAVOLA SE
	$this->model->edit($this->attId, $form->values);
	//$this->presenter->redirect('this');
}

A právě odeslání tohoto formuláře vyhodí chybu BadSignalException,
The signal receiver component 'EditAttModal-attDetailForm' is not found.
Tedy funkce editFormSubmitted pro zpracování formuláře se nenajde.

Můj odhad je, že metoda show a inicializace EditAttModal komponenty nesmí být provedena v metodě handleEditAttModal. Ale tam to přesně potřebují mít, abych mohl ajaxově aktivovat modální okno a zobrazit v něm relevantní informace předané v tomto signálu.

Poradíte? Díky.

ps.: nevíte o komponentě pro práci s modálními okny – nemusel bych pak řešit výše uvedený problém. Taky si nejsem jist nakolik je tento koncept správný.

Editoval Filip111 (25. 4. 2013 15:04)

mildhouse
Člen | 27
+
0
-

Zdar, no to je totiž ten problém, o kterém mluvím od začátku. Možná kecám, ale myslim, že ty zadáš adresu např.

www.nejakastranka.cz/?do=editAttModal&contentId=123&attId=987

a tím se spustí handleEditAttModal($contentId, $attId) která do modálního okna přidá title a komponentu formuláře. Ty ten form vyplníš a odešleš na

www.nejakastranka.cz/?do=EditAttModal-attDetailForm-submitted

a handleEditAttModal() neproběhne, to znamená, že formulář neexistuje a tudíž není nikdo, kdo by ten signál odchytil.

Filip111
Člen | 244
+
0
-

Nojo, sorry – četl jsem ještě nějaký jiný témata ohledně modálních oknen a nějak jsem si zafixoval, že v tomhle vlákně se řeší problém s tím, že nejde modální okno zavřít. Když teď čtu ale tvůj první dotaz, tak je to stejnej problém na jaký jsem narazil já.

Tak co teď s tím?

mildhouse
Člen | 27
+
0
-

Já mam na to komponentu, která to řeší, prostě si tu handle (její název) uložim do session a následně jí volám v action metodě. Asi nějak takhle :

public function createComponentModalDialog($name) {
    return new \web132\Components\ModalDialog($this, $name);
}

public function dialog() {
    return $this['modalDialog'];
}

//Zobrazní modálního okna s vykreslením jiné komponenty :
public function editAttModal($contentId, $attId) {
    $this->dialog()
        ->setTitle('Editace přílohy')
        ->show(new \web123\Components\EditAttModal(
            $this,
            'EditAttModal',
            $attModel,
            $attId)
        );
}

public function actionDefault(){
	$session = $this->getSession('modalDialog');
	//pri odeslani formulare se handleEditAttModal() nezavola, ale mame ji ulozenou v sessions
	if(isset($session->methodName)){
		$name = $session->pseudoHandle;
		$params = $session->params;
		//a zavolame ji
		$this->$name($params['contentId'], $params['attId']);
	}
}

public function handleEditAttModal($contentId, $attId) {
	//ulozit metodu do sessions
	$session = $this->getSession('modalDialog');
	$session->methodName = 'editAttModal';
	$session->params = array('contentId' => $contentId, 'attId' => $attId);
	//a zavolat ji
	$this->editAttModal($contentId, $attId);
}

Píšu to z hlavy, ale tak nějak to v podstatě funguje. Napřed je volaná handle, která uloží nazev metody, ktera inicializuje dialog. okno a parametry, pak tu metodu zavolá. Při odeslání formuláře se v action zjistí, zda je co volat a pokud ano, zavolá se. Tím se vytvoří formulář a provede do=form-submitted.

Já sem když tak o víkendu dám tu komponentu.

Filip111
Člen | 244
+
0
-

Ahoj, to co popisuješ mi funguje, ale nelíbí se mi to. Má to daleko k samostatné, jednoduše a masivně použitelné komponentě.

Zaprvé se mi nelíbí obcházení problému s handle – ideální by bylo to vyřešit tak, aby vůbec nenastal a nemusel jsem to řešit pomocí nějakých session (nenapadá mě jak).

Zadruhé se mi nelíbí, že aby tohle řešení fungovalo, je potřeba vyjmou kód z handle funkce a umět ho volat samostatně, na jednom místě nastavit session, na jiném ji zpracovat a vyvolat handle (resp. jen tu vyjmutou funkci). Je to dost balastu co znepřehledňuje kód.

Koncepce modálních oken se mi líbí – spoustu kódu lze vyhodit do komponent a odlehčit jak presenter tak samotnou stránku. Zkusím s tím ještě něco vymyslet – pořád by mě ale zajímalo, jestli to někdo nepoužívá a koncepčně by nás nenakopnul lepším směrem.

Editoval Filip111 (26. 4. 2013 11:19)

Glottis
Člen | 129
+
0
-

hodim sem jen ukazku jak jsem to ted resil ja. nevim jestli vam to k necemu bude ale treba si z toho neco vyzobnete. je to jen modalni confirm form. ale ikdybych delal nejaky edit form tak to bude prakticky stejne

<a class="confirm-modal" data-confirm-header="{$invoice->invoiceNumber}" data-confirm-text="Opravdu chcete smazat fakturu <b>{$invoice->invoiceNumber}</b>" href="{link delete! $invoice->id}">Smazat</a>

{#confirmFrom}
<div id="modal-confirm-dialog" class="modal hide fade">
    <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
        <h3>Smazat</h3>
    </div>
    <div class="modal-body">
        <p>Opravdu chcete položku smazat?</p>
    </div>
    <div class="modal-footer">
        <a id="modal-confirmed-url" href="" class="btn danger ajax">Ano</a>
        <a href="javascript:$('#modal-confirm-dialog').modal('hide')" class="btn secondary">Ne</a>
    </div>
</div>

<script>
    $.nette.ext('confirm', {
        load: function () {
            $('.confirm-modal').click(function(e) {
                e.preventDefault();

                $("#modal-confirm-dialog .modal-header>h3").html($(this).data("confirm-header"));
                $("#modal-confirm-dialog .modal-body>p").html($(this).data("confirm-text"));
                $("#modal-confirmed-url").attr("href", $(this).attr('href'));
                $('#modal-confirm-dialog').data('modal-confirm-dialog', "xxx", 'yy').modal('show');
            });
        }
   });
</script>
{/#}

jestli to chapu dobre co delate, tak to co davate do session si castecne davam do data atributu

okna pak zaviram pres toto

		$.nette.ext('closeModal', {
    success: function (payload) {
        if (payload.closeModal) {
            $('#'+payload.closeModal).modal('hide');
        }

    }
});

a v presenteru v handle nebo action pak si poslu payload stim, co zavrit

$this->payload->closeModal = "modal-confirm-dialog";

jo a je to nad jquery a twitter bootstrap

Editoval Glottis (26. 4. 2013 11:35)

Filip111
Člen | 244
+
0
-

Sice jsem z toho nepochopil, jak je modální okno spravované v prezenteru, případně zda by mi to pomohlo vyřešit problém se signálem v jiném signálu, ale možná se inspiruji v otevření a uzavření modálního okna pomocí payloadu.

Glottis
Člen | 129
+
0
-

neni spravovane nijak, jen to zavreni vyprovokuje presenter. a otevrit si ho muzes klidne zas tim payloadem a klidne mu tam poslat i ty texty nebo cokoliv. fakt to byla jen inspirace :)

mildhouse
Člen | 27
+
0
-

@Filip111:
Ale payload budeš muset taky pořešit v handle ne? Zase se ti neprovede po odeslání formuláře. Máš pravdu, že ukládat to do sessions není ideální, to jsem zjistil, když se modální okno snažilo ukládat/načítat sessions při vykreslování, což vyhodilo chybu. Zkus to místo sessions ukládat do persistentních parametrů. Nechat parametr ‚do‘ volný pro odeslání formuláře a vytvořit si svůj parametr např. ‚dialog-do‘. To pak přece není obcházení, ale vyřešení problému, protože ke kolizi nedojde. Uděláš vlastně to, co dělá nette : v parametru předáš název metody a její parametry. Akorát si jí budeš muset volat sám někde v action.

Glottis
Člen | 129
+
0
-

mozn ami neco unika a cele to nechapu ale proc si ty pripadne formulare nedas do snipetu? ja to tak kdysi delal a to mi i fungovalo

<div id="{$control->name}" class="window-container" style="width: {$width}; max-width: {$maxWidth}">
    <div id="win-window-handle" class="window-head-bar">
        <span style="">
                {snippet win-caption}
                    {$caption}
                {/snippet}
        </span>
        <a href="{link close!}" class="ajax close"></a>
    </div>
    <div class="window-body" id="{$control->name}-windows-body">
        <div class="window-body-container" id="{$control->name}-windows-body-container" style="height: {$height}; max-height: {$maxHeight}">
            {snippet win-body}
                {snippet flashMessages}
            <div n:foreach="$flashes as $flash" class="flash {$flash->type}">{$flash->message}</div>
			{/snippet}
            {foreach $control->components as $comp}
                    {snippet $comp->name}
                            {control $comp}
                    {/snippet}
            <div style="clear: both;"></div>
            {/foreach}
            {/snippet}
        </div>
    </div>
</div>

//v tovarne
$form->onError[] = callback($this->presenter, 'errorForm');

//obsluha chyby
public function errorForm(Nette\Application\UI\Form $form) {
        /** @var Glo\Windows */
        $win = $this->getComponent('win');
        $win->setCaption('testicek error');

        $this->flashMessage('testaa');

        $win->invalidateControl('win-body');
        $this->invalidateControl();
}

a pripadna chyba se mi v okne prekreslila i s formularem a okno zustalo