Dynamic snippets for repeated forms

At the time of this writing, Nette lacks a good support for dynamic snippets. You can define them wrapped in a static snippet but you can’t invalidate them individually (unless they each belong to a different control, I think). You can only invalidate the static parent snippet which means you receive all its dynamic children snippets in the payload.

If you defined only a few snippets with small amount of data in each then no big deal. If however, you have a pageful of forms, each form containing a snippet that you want updated upon the parent form submission then you have to resort to tricks. For instance, you don’t want a message “Saved” displayed in each row/form in a product administration table.

I’m going to assume that you know how to submit forms via AJAX. Your application is already working with AJAX and the only missing piece is the dynamic snippets invalidation.

The trick that I ended up using is kind of mirroring the built-in functionality. I keep track of invalidated dynamic snippets and skipping definitions of those that haven’t changed. Sounds more difficult than it is, actually.

Notice that I define invalidSnippets in startup().

  2. class Admin_ProductPresenter extends NPresenter {
  4.   protected function startup() {
  5.     parent::startup();
  7.     // this is better placed into BasePresenter
  8.     $this->template->isAjax = $this->isAjax();
  9.     $this->template->invalidSnippets = array();
  10.   }
  12.   public function renderDefault() {
  13.     // load products from db
  14.     // and set defaults with $this["productForm-$productId"]->setDefaults($data);
  15.   }
  17.   public function createComponentProductForm() {
  18.   // create your repeated forms
  19.     return new NMultiplier(function() {
  20.       return new ProductForm();
  21.     });
  22.   }
  23. }

When processing form submission in ProductForm::process(), I take the form’s unique identificator and use it to mark all its snippets as invalidated.

  2. class ProductForm extends NAppForm {
  4.   public function __construct($parent = null, $name = null) {
  5.     // form elements are added here
  6.     $this->onSuccess[] = callback($this, ‘process’);
  7.   }
  9.   public function process(NAppForm $form) {
  10.     $values = $form->getValues();
  12.     // update the product in db
  14.     if ($this->presenter->isAjax()) {
  15.       $this->presenter->invalidateControl("products");
  16.       // invalidate the wrapping static snippet
  18.       // $this->presenter->invalidateControl("message-".$productId);
  19.       // it makes no sense to invalidate the dynamic snippet – it doesn’t do anything
  21.       $productId = $values[‘productId’];
  22.       $this->presenter->template->invalidSnippets[$productId] = true;
  23.       $this->presenter->template->message = ‘Saved.’;
  25.     } else {
  26.       $this->presenter->flashMessage("Saved.");
  27.       $this->presenter->redirect(‘this’);
  28.     }
  29.   }
  30. }

Reaching from ProductForm over to the parent presenter’s template is smelly but let’s keep it short here. You can attach another method to onSuccess[] and do the presenter related stuff in your presenter’s method.

And here’s the template. Notice that I define the snippets themselves only if they are needed. That is during the initial non-AJAX rendering or if they have been invalidated.

  2. {snippet products}
  3. {* this is the static wrapper that Nette needs to allow dynamic snippets *}
  5. {foreach $products as $product}
  6.   {var $productId = $product[‘id’]}
  7.   {form productForm-$productId}
  8.     {input name} {* your form elements *}
  9.     {input save} {* something to submit the form with *}
  11.     {* wrapper: only define if invalidated or during the initial rendering *}
  12.     {if isset($invalidSnippets[$productId]) || !$isAjax}
  13.       {snippet message-$productId span}
  14.         {isset($message) ? $message}
  15.       {/snippet}
  16.     {/if}
  17.   {/form}
  18. {/foreach}
  20. {/snippet}

You can define as many dynamic snippets as you want just make sure their name is unique (e.g. “something-$productId”) and you wrapped them with the if statement.

There’s also an alternative way, a completely different approach which ignores all the above. And that’s to detect a submitted form in the presenter and only work with its product further on. The added benefit is reduced server load – you don’t process information for other products only to throw it away at the end. The same for the template – define only this particular product’s form.


2 Responses to “Dynamic snippets for repeated forms”

  1. Schmutzka says:

    Just wondering about the topic, don’t you want to add this to Planette?

  2. To be honest, no. I’ve had my share of problems getting my code published in docs. The admins are always quick to delete the whole text if they find it’s not clean enough. That’s why I started posting to my blog instead. I understand the rules shouldn’t be as strict at Planette however, I just gave up. Feel free to link to me though.

Leave a Reply for Petr 'PePa' Pavel