Uniform - multiple forms with ajax

I recently tried to implement the uniform plugin on a page with multiple forms. Each form comes from a structure field. The oldschool way worked after a big help from @mzur. He helped me with it in the comments of his blog http://blog.the-inspired-ones.de/kirby-with-uniform.
But now I want to integrate ajax to the forms, because each form is in a accordion. We decided to continue the discussion from the blog here in the form.
My question is now, how can I implement multiple ajax form on one page. Thanks for your help.

Okay, let’s give it a try. We can take the plain AJAX form as a reference.

Now you have two different pages. One displaying all the different forms and one (silently) handling the AJAX form requests. For the page displaying the forms you don’t need the Uniform controller code any more. Getting the token for each individual form in the template is sufficient:

<input type="hidden" name="_submit" value="<?php echo uniform('myform-'.$id)->token() ?>">

Note that the $id still is required from the loop over the structure field. In addition to that we need a hidden form field containing the form ID. This will tell the AJAX controller which form sent the request:

<input type="hidden" name="_id" value="<?php echo('myform-'.$id) ?>">

Now the AJAX controller (contact-send.php) needs to be able to handle requests from all forms. Since the form ID was sent with the request, we can use it to initialize the correct form:

$form = uniform(get('_id'), ...);

If you need some data from the structure field of the parent page you can get it like this:

$forms = $page->parent()->structurefield()->yaml();

The AJAX template doesn’t need any changes since we handle only a single form per request.

Last we need to update the JavaScript code of the template displaying the forms. With a single form we can take hardcoded IDs like in the blog post. With multiple forms we need to do this dynamically. One way to do this is to prefix/suffix all the IDs with the 'myform-'.$id and then loop the whole JavaScript code through all IDs like this:

<script type="text/javascript">
window.onload = function () {
    var ids = ["<?php echo implode('\",\"', array_keys($page->structurefield()->yaml())) ?>"];
    for (var i = ids.length; i >= 0; i--) {
        $('#form-submit-myform-' + ids[i]).click( /* ... */);
    }
};
</script>

This should do the trick but I haven’t tested any of it so there will be errors. See how far you get :wink:

That’s a good start. You are amazing.

Now I tried to implement your suggestions step by step. It’s really great to get the _id in the controller.
My javascript was also in the loop and so I attached the $id to de #ids. But with just one script it is much more elegant. I only had to remove the backslashes in “implode()” Now it looks like

var ids = ["0","1","2","3"]

I also put the ids to #form and #feedback. Is that correct or isn’t it necessary?
I think the only point how is missing is the JSON object. Because wenn I try to send a form I get a 500 (Internal Server Error). And if I open …/contact/send it shows a blank page. On a page with a single AJAX form it displays on that page

{
"success": false,
"message": "",
"errors": []
}

How can I get that json object right?

That’s correct. Each HTML ID must be unique.

Probably there is something wrong with the AJAX controller. Can you please provide the exact error message and the code from the contact-send.php controller?

Ok, here comes my controller code: its in the contact-send.php file:

<?php

return function($site, $pages, $page) {
  
   $form = uniform(get('_id'), array(
     'required' => array(
       'name' => '',
       '_from' => 'email',
       'message' => '',
     ),
     'actions'  => array(
        array(
          '_action' => 'email',
          'to'      => 'mail@domain.com',
          'sender'  => 'mail@domain.com',
          'subject' => 'New Message',
          'snippet' => 'uniform/uniform-contact'
        ),
        array(
           '_action' => 'log',
           'file'    => 'log/contact-form.log'
        )
      )
    )                            
  );
  return compact("form");
};

The error I get shows just in the console:

POST "[...]/contact/send" 500 (Internal Server Error)
  send @ jquery-1.11.2.min.js:4
  m.extend.ajax @ jquery-1.11.2.min.js:4
  m.(anonymous function) @ jquery-1.11.2.min.js:4
  (anonymous function) @ support:278
  m.event.dispatch @ jquery-1.11.2.min.js:3
  r.handle @ jquery-1.11.2.min.js:3

And I didn’t change anything in the contact-send.php (JSON template).

And here my javascript:

<script type="text/javascript">
window.onload = function () {
  var ids = ["<?php echo implode('","', array_keys($page->structurefield()->yaml())) ?>"];
  for (var i = ids.length; i >= 0; i--) {
    $('#form-submit-' + ids[i]).click(function (e) {
      e.preventDefault();
      $.post(
        '<?php echo $page->children()->find('send')->url()?>',
        $('#form-' + ids[i]).serialize()
      )
  .then(function (response) {
      var feedback = $('#feedback-' + ids[i]);
      feedback.removeClass('flash-success flash-error').text(response.message);
      $('input, textarea').removeClass('erroneous');
      if (response.success) {
          feedback.addClass('flash-success');
          $('input, textarea').prop('value', '');
          $('#form-submit-' + ids[i]).prop('disabled', 'disabled');
      } else {
          feedback.addClass('flash-error');
          for (var i = response.errors.length - 1; i >= 0; i--) {
              $('[name="' + response.errors[i] + '"]').addClass('erroneous');
          };
      }
   });
  });
 }
};
</script>

Is there anything missing? Thanks one time more for your time @mzur!

The actual error from the PHP server might be more informative. If you use Apache or something similar, take a look at the logfile. With the PHP dev server, the error will show up in the terminal.

Also in the JS, you might restrict all non-id selectors to the form that’s processed:

$('#form-submit-' + ids[i]).click(function (e) {
   var form = $('#form-' + ids[i]);
   // ...
   form.serialize()
   // ...
   form.children('input, textarea')// ...
   // ...
   form.children('[name="' + response.errors[i] + '"]')// ...

Else this would modify all forms on the page.

Ok, the php log file makes more sense:

[10-Sep-2015 07:45:50 UTC] PHP Fatal error:  Uncaught No Uniform ID was given.
thrown in .../site/plugins/uniform/lib/UniForm.php on line 77

So I think this has to do with the controller:

$form = uniform(get('_id'),...)

Or isn’t it that ID?

What does a HTTP request look like when you submit a form (tab ‘Network’ in the browser dev tools)? Does it contain the form _id?

Jep, it is set correctly:

<input name="_id" value="contact-form-0" type="hidden">

What I meant was the actual HTTP form request, not the HTML code. It looks like this:

Does the request contain the _id?

Else, what does a var_dump(get('_id'));die(); in the AJAX controller return?

The HTML form request contains nothing. Its not a 200 POST but a 500 POST.
The AJAX controller returns NULL.

I think the problem is the javascript with the for loop. Because if I put just the script from the blog post and set the correct id for one form, it seems to work.
At least the error-messages are display if a required field is missing. And I can send the form and receive the email. But to content in the form stays (doesn’t get cleared) and there is no success message.
But the HTML request contains the _id and all the form fields.

So this is my actual javascript, which seems to be the problem:

<script type="text/javascript">
window.onload = function () {
  var ids = ["<?php echo implode('","', array_keys($page->donations()->yaml())) ?>"];
  for (var i = ids.length; i >= 0; i--) {
    $('#donation-form-submit-' + ids[i]).click(function (e) {
      var form = $('#donation-form-' + ids[i]);
      e.preventDefault();
      $.post(
          '<?php echo $page->children()->find('send')->url()?>',
          form.serialize()
      )
      .then(function (response) {
          var feedback = $('#donation-feedback-' + ids[i]);
          feedback.removeClass('flash-success flash-error').text(response.message);
          form.children('input, textarea').removeClass('erroneous');
          if (response.success) {
              feedback.addClass('flash-success');
              form.children('input, textarea').prop('value', '');
              form.children('#donation-form-submit-' + ids[i]).prop('disabled', 'disabled');
          } else {
              feedback.addClass('flash-error');
              for (var i = response.errors.length - 1; i >= 0; i--) {
                  form.children('[name="' + response.errors[i] + '"]').addClass('erroneous');
              };
          }
      });
    });
  }
};
</script>

Maybe its because we have two times the var i = ...?

Good catch! The outer loop starts with the wrong index, too. It has to be:

for (var i = ids.length - 1; i >= 0; i--) {

And then the inner loop:

for (var j = response.errors.length - 1; j >= 0; j--) {
   form.children('[name="' + response.errors[j] + '"]').addClass('erroneous');
};

I corrected it. But still no luck.
That’s confusing. If I log ids[i] in the console I get the following order: 3, 2, 1, 0.
is that the correct order? Or doesn’t it matter?
If I index it with (var i = 0; i < ids.length; i++), I get the reversed order: 0, 1, 2, 3

And then if I log ids[i] after the click on the submit button it returns undefined. Shouldn’t it return the value of the clicked button?

The ordering doesn’t matter. It’s quicker that way because otherwise ids.length would be checked with each iteration.

Where exactly did you insert the console.log? Could you show me in a screenshot like mine what exactly the POST request’s params are?

Thanks again.
The console.log I inserted here:

var ids = ["<?php echo implode('","', array_keys($page->donations()->yaml())) ?>"];
  for (var i = ids.length - 1; i >= 0; i--) {
    console.log(ids[i]);  //<--- here it logs: 3,2,1,0
    $('#donation-form-submit-' + ids[i]).click(function (e) {
      var form = $('#donation-form-' + ids[i]);
      console.log(ids[i]); //<--- here it logs: undefined
      e.preventDefault();
      $.post(...

And here a screenshot of the POST request

But I’m not sure if this helps?

Maybe I can send you a link with the page? Is there something like a direct message in this forum?

That looks like a JS scoping issue. Try assigning a new variable for each ID:

for (var i = ids.length - 1; i >= 0; i--) {
   var id = ids[i];
   $('#donation-form-submit-' + id).click(function (e) {
      var form = $('#donation-form-' + id);
   // etc.

That brings it a step further… if I log console.log[id] after the click the value is 0. But it is 0 for every form I submit.

Then try moving it down like so:

for (var i = ids.length - 1; i &gt;= 0; i--) {
   $('#donation-form-submit-' + ids[i]).click(function (e) {
      var id = ids[i];
      var form = $('#donation-form-' + id);
   // etc.

then it’s again undefined

I think I found a solution to get around the problem with the IDs. Because the form is in a accordion, I can set the ID before submitting the form. My script looks now like this:

<script type="text/javascript">
window.onload = function () {
  $('.trigger').on( 'click', function() {
  var id = $(this).attr('data-id');
    
    $('#donation-form-submit-' + id).click(function (e) {
      var form = $('#donation-form-' + id);
//      console.log(form);
      e.preventDefault();
      $.post(
          '<?php echo $page->children()->find('send')->url()?>',
          form.serialize()
      )
      .then(function (response) {
          var feedback = $('#donation-feedback-' + id);
          feedback.removeClass('flash-success flash-error').text(response.message);
          $('input, textarea').removeClass('erroneous');
          if (response.success) {
              feedback.addClass('flash-success');
              $('input, textarea').prop('value', '');
//              form.prop('disabled', 'disabled');
          } else {
              feedback.addClass('flash-error');
              for (var i = response.errors.length - 1; i >= 0; i--) {
                  $('[name="' + response.errors[i] + '"]').addClass('erroneous');
              };
          }
      });
    });
  });
};
</script>

That seems to work. The only problem I have is, that the form doesn’t get cleared on successful submit. The error messages are correct. But I think they are triggered by the input field in the html (required).
The form data gets sent and I receive the email. I dosen’t get any php errors. So the controller seems to work fine.
Now the problem could be in the json object. I’ve seen that you, @mzur, have updated the json template in your blog post. There is a ARRAY_FILTER_USE_KEY. What is the function of it?

Well, what does the HTTP response look like? You can test the Uniform validation by using an email address like user@example without the TLD, which passes validation by the browsers but not by PHP.

Read the manual :wink: