Created by @novak in Q&As
@novak
@novak

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

@jahudka
@jahudka

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
@novak
@novak

Thank you for your quick answer, but when I use n:dialog="@self:keep" macro, the application crashes with phpWriter error

@jahudka
@jahudka

@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").

@jahudka
@jahudka

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 :-)

@jahudka
@jahudka

... 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

@novak
@novak

Thank you for your reply. It helped me a lot. It pointed me in the right direction.

@jahudka
@jahudka

You're welcome, happy to help :-)

Sign in to post a reply