Hi, I have problem, when I have open form in modal window and check form validation, modal window closes and validation error is displayed in classic form. Is any way how not to close modal window and display this error?
Thanks
Hi, yeah - this is how:
- add
n:dialog="@self:keep"
to the<form>
- in the form's
onSuccess
callback, right before calling$this->redirect()
or$this->postGet()
or something like that, add$this->closeDialog('name-of-the-dialog')
- in the
onError
callback simply redraw the snippet which contains the dialog content
Thank you for your quick answer, but when I use n:dialog="@self:keep" macro, the application crashes with phpWriter error
@novak wrote:
Thank you for your quick answer, but when I use n:dialog="@self:keep" macro, the application crashes with phpWriter error
That's weird.. can you try using n:dialog="@self:<snippet>"
, where <snippet>
is the name of the snippet which holds the dialog content? I can't find any usage of @self:keep
in any of my projects now, but reading the source code I think it was meant to be used when you wished to open another dialog without closing the existing one (so it would be @self:keep, otherName:otherSnippet
or something like that) - so maybe the fact that no other specifier is present is breaking the macro.. but I'm definitely using @self:content
, where content
means the content
snippet of the component which renders the form (it's the same snippet which is used in another template of the same component to open the dialog in the first place using something like n:dialog.form="editItem:content"
).
I have form https://pastebin.com/Cij09CwE and in latte https://pastebin.com/Jysmuuiw and in presenter https://pastebin.com/R9cnsyHJ and this error http://leteckaposta.cz/156405298
Try changing n:dialog="@self:keep"
to n:dialog="@self:coupon"
.
Also, it's important to know who owns the dialog. From your code it's not apparent where the n:dialog.form
macro is first used to open the dialog - I'm assuming it's in the presenter's or another component's template, rather than a template of the CouponForm
component. This is important because dialogs are scoped similarly to snippets.
If you write <a href="..." n:dialog.form="couponDlg:couponSnip">
in a presenter template, you're saying you want to open a snippet called snippet--couponSnip
in a dialog called dlg--couponDlg
- ie. a presenter-level snippet in a presenter-level dialog.
If you instead have the exact same code in a component template both the dialog name and the source snippet are scoped to the component, so if the component is called e.g. orderForm
, the dialog that will be opened will be called dlg-orderForm-couponDlg
and the snippet that it will try to display will be dlg-orderForm-couponSnip
.
The same is true for the utility methods from the nittro/nette-bridges
package: $presenter->closeDialog('couponDlg')
will close dlg--couponDlg
, whereas $presenter->getComponent('orderForm')->closeDialog('couponDlg')
will close dlg-orderForm-couponDlg
.
What this means is that you want to have all code related to a given dialog at the same level - either the presenter or a component. In your use case I'm guessing there's a button somewhere that the user clicks to open a dialog where they can enter a coupon code to get a discount or something - I'd probably implement that like this:
class CouponControl extends Control {
private boolean $active = false;
public function handleOpen(): void {
$this->active = true;
$this->redrawControl('content');
}
public function render(): void {
if ($this->active) {
$this->template->setFile(__DIR__ . '/form.latte');
} else {
$this->template->setFile(__DIR__ . '/button.latte');
}
// ...
}
public function createComponentForm(): Form {
$form = new Form();
// ...
$form->onSuccess[] = [$this, 'formOk'];
$form->onError[] = [$this, 'formFailed'];
return $form;
}
public function formOk(): void {
// ...
$this->postGet('this');
$this->redrawControl('content');
$this->closeDialog('form');
}
public function formFailed(): void {
// ...
$this->redrawControl('content');
$this->active = true;
}
}
To use the postGet()
and closeDialog()
methods in a component it should use the Nittro\Bridges\NittroUI\ComponentUtils
trait at some point, usually in a BaseControl
of some kind.
The button.latte
template would look like this:
<div n:snippet="content">
<a n:href="open!" n:dialog.form="form:content">Enter coupon code</a>
</div>
And the form.latte
template:
<div n:snippet="content">
<form n:name="form" n:dialog="@self:content">
<!-- ... -->
<!-- you can include a cancel button to close the dialog
without submitting the form -->
<button type="button" data-action="cancel">Cancel</button>
</form>
</div>
This, on the one hand, keeps everything contained within the component - if I needed to react to a successful submit in the presenter, I'd define a custom onSuccess
event in the component itself, because the presenter shouldn't know about the component's implementation (meaning it shouldn't know there is a Form
inside it, so it shouldn't attach listeners to its events).
On the other hand, imagine what happens if you now disable JavaScript (or, more probably, if something breaks on the client side): when the user clicks the button, the whole page will be redrawn (possibly losing data already entered in the main form, if there is one) and where there was just a button before there's now a whole other form inline in the page! Depending on your use case you might want to avoid this; in that case I'd recommend having a separate presenter for the dialog content (the dialog content would be your main content
snippet or something similar), the Enter coupon code
button would be a submit button for the main form instead of a link and its onClick
handler on the backend would look something like this:
$form->addSubmit('enterCouponCode')->onClick[] = function() {
// save the main $form's data in the session or something, and then
if ($this->isAjax()) {
// forward internally to save a roundtrip
$this->forward('CouponForm:');
} else {
$this->redirect('CouponForm:');
}
};
And later in CouponFormPresenter
:
public function render(): void {
// ...
if ($this->isAjax()) {
$this->postGet('this'); // to have the correct URL
$this->redrawControl('content'); // might not be needed, not sure
$this->openInDialog('couponForm', 'content', 'form');
}
}
Hope this helps :-)
... just realised the second approach has one disadvantage: Nittro wouldn't know in advance that you will be opening a dialog, so the dialog opening animation would only start after response from CouponFormPresenter::renderDefault()
is received on the client side. To get around this you could set a data-dialog
data attribute on the main form dynamically in a client-side onSubmit
handler if the dialog was submitted using the Enter coupon code
button instead of calling $this->openInDialog()
in the renderDefault()
method. Well, it's another way of doing it anyway :-D
Thank you for your reply. It helped me a lot. It pointed me in the right direction.
You're welcome, happy to help :-)
Sign in to post a reply