Submit a form using kirby->email() with AJAX

I’ve been trying to find what would be the best way to handle an AJAX form without refreshing the page on submit and keep using the same structure in this doc reference by using kirby->email() with a controller.

Does someone have some experience on this?

Thanks in advance

You could use a content representation (json controller) that is called by your JS.

Example of such a setup can be found in the AJAX load more cookbook recipe:

Thank you.
So what I have is the next.

  • A controller contact.json.php
  • A form in the contact.php template
  • A JS snippet that handles the AJAX

It keeps giving me a 200 error on submitting, but I think I am getting closer.
I assume the problem recalls in the controller itself, which looks like this:

<?php
return function($kirby, $pages, $page) {

    $alert = null;

    if($kirby->request()->is('POST') && get('submit')) {

        // check the honeypot
        if(empty(get('website')) === false) {
            go($page->url());
            exit;
        }


        $data = [
            'email' => get('email'),
            'text'  => get('text'),
            'newsletter' => (!empty(get('newsletter'))) ? get('newsletter') : [],
        ];

        $rules = [
            'email' => ['required', 'email'],
            'text'  => ['required', 'minLength' => 3, 'maxLength' => 3000],
        ];

        $messages = [
            'email' => 'Please enter a valid email address',
            'text'  => 'Please enter a text between 3 and 3000 characters'
        ];


        // authenticate as almighty
        $kirby->impersonate('kirby');

        // some of the data is invalid
        if($invalid = invalid($data, $rules, $messages)) {
            $alert = $invalid;

            // the data is fine, let's send the email
        } else {
            try {
                $kirby->email([
                    'template' => 'contact',
                    'from'     => 'myemail@gmail.com',
                    'replyTo'  => $data['email'],
                    'to'       => 'myemail@gmail.com',
                    'subject' => 'New contact request',
                    'data'     => [
                        'text'   => esc($data['text']),
                        'newsletter' => implode(', ', $data['newsletter']),
                    ]
                ]);

            } catch (Exception $error) {
                if(option('debug')):
                    $alert['error'] = 'The form could not be sent: <strong>' . $error->getMessage() . '</strong>';
                else:
                    $alert['error'] = 'The form could not be sent!';
                endif;
            }

            // no exception occurred, let's send a success message
            if (empty($alert) === true) {

               $success = kirby()->email([
                    'from'    => 'myemail@gmail.com',
                    'to'      => $data['email'],
                    'subject' => 'New contact request received',
                    'body'    => 'We have received your request.',
                ])->isSent();

                $data = [];

            }
        }
    }

    return [
        'alert'   => $alert,
        'data'    => $data ?? false,
        'success' => $success ?? false,
    ];
};

Any idea how I can tweak this code to pass the data properly for the JSON and how to call it in the ‘url’ for the AJAX request?

Thanks again!

  1. Your json controller has to return json, not a PHP array.
  2. You call $kirby->email() twice
  3. As url, you have to call contact.json

1. Your json controller has to return json, not a PHP array.

Would something like this be a good way to approach this instead (despite is not generating the JSON file for some reason)?

// Prepare the JSON response
$response = [
    'alert'   => $alert,
    'data'    => $data ?? false,
    'success' => $success,
];

$jsonResponse = json_encode($data); // Encode the response as JSON

// Save the JSON response as a file in the main directory
$filename = 'contact.json';
file_put_contents($filename, $jsonResponse);

2. You call $kirby->email() twice

My idea is to send an automatic email to the user after receiving the form submit. Is in this context calling the function twice something bad?

I think I managed to make the this relation work, but I am getting an ‘{alert: null, data: false, success: false}’ on accessing the json based on this:

<?php
return function($kirby, $pages, $page) {

    $alert = null;

    if($kirby->request()->is('POST') && get('submit')) {

        // check the honeypot
        if(empty(get('website')) === false) {
            go($page->url());
            exit;
        }


        $data = [
            'email' => get('email'),
            'text'  => get('text'),
            'newsletter' => (!empty(get('newsletter'))) ? get('newsletter') : [],
        ];

        $rules = [
            'email' => ['required', 'email'],
            'text'  => ['required', 'minLength' => 3, 'maxLength' => 3000],
        ];

        $messages = [
            'email' => 'Please enter a valid email address',
            'text'  => 'Please enter a text between 3 and 3000 characters'
        ];

        // authenticate as almighty
        $kirby->impersonate('kirby');

        // some of the data is invalid
        if($invalid = invalid($data, $rules, $messages)) {
            $alert = $invalid;

            // the data is fine, let's send the email
        } else {
            try {

                $kirby->email([
                    'template' => 'contact',
                    'from'     => '...',
                    'replyTo'  => $data['email'],
                    'to'       => '...,
                    'subject' => '...,
                    'data'     => [
                        'text'   => esc($data['text']),
                        'newsletter' => implode(', ', $data['newsletter']),
                    ]
                ]);

            } 

            // no exception occurred, let's send a success message
            if (empty($alert) === true) {
                $data = [];
            }
        }
    }

    // return [
    //     'alert'   => $alert,
    //     'data'    => $data ?? false,
    //     'success' => $success ?? false
    // ];

    // Create the JSON response
    $response = json_encode([
        'alert'   => $alert,
        'data'    => $data ?? false,
        'success' => $success ?? false
    ]);


    // Write the JSON response to a file
    $file = 'contact.json';
    file_put_contents($file, $response);

};

Any idea what might be originating the data and success values to be always false?

I don’t understand the purpose of the json file.

However, I made a mistake.

Your controller has to return an array, then your template has to echo the json encoded result.

The alternative would be to handle this from a route.

I tried to reproduce a basic example of what I have, and made a mistake on renaming the JSON filename.

So my idea is to generate a JSON file from the controller (contact.php) that would be (contact.json), to then grab this information via an AJAX call.

That won’t work, is that what you meant?

Thanks again, and sorry for the insistence, this is driving me crazy.

No, because when you just call this file, it is static and the controller won’t be triggered.

OK, so what I should have is a /controllers/contact.php that returns the arrays in PHP, then a /controllers/contact.json.php and a ‘/templates/contact.json.php’ in addition to the ‘/templates/contact.php’ which stores the form. Is that correct?

One thing I don’t understand from the documentation you sent is ’ This content representation will be available at the url localhost/photography.json and is the URL we will use in the JavaScript in the next step.’ When is this json file being generated?

This is not a file, it is a combination of a contact.json.php controller and a contact.json.php template. See the docs about content representations to understand this better.

contact.json.php controller

<?php

return function ($kirby, $page) {
	
       // other code
	return [
		'alert'       => $alert,
		'success' => $success,
                // etc.
	];
};

contact.json.php template

<?php
echo json_encode([
  'success' => $success,
  'alert'       => $alert,
  // etc.
]);

OK, I think Im getting slowly there. Thanks for the patience.

So, should the url value for the AJAX call in the JS still be contact.json or something else? I am currently doing 'form.attr('action') + '.json'; which retrieves the $page->url() from the form action; but shows now the '{alert: null, data: false, success: false}' again on submitting – but at least seems it’s finding the json file.

That should work.

But are you sending any data to the url?

Sharing my full code at this point for full context (not including the form which essentially just calls to the page url on the ‘action’ attribute):

/controllers/contact.json.php:

<?php
return function($kirby, $pages, $page) {

    if($kirby->request()->is('POST') && get('submit')) {

        // check the honeypot
        if(empty(get('website')) === false) {
            go($page->url());
            exit;
        }


        $data = [
            'email' => get('email'),
            'text'  => get('text'),
        ];

        $rules = [
            'email' => ['required', 'email'],
            'text'  => ['required', 'minLength' => 3, 'maxLength' => 3000],
        ];

        $messages = [
            'email' => 'Please enter a valid email address',
            'text'  => 'Please enter a text between 3 and 3000 characters'
        ];


        // authenticate as almighty
        $kirby->impersonate('kirby');

        // some of the data is invalid
        if($invalid = invalid($data, $rules, $messages)) {

            // the data is fine, let's send the email
        } else {

            try {
                $kirby->email([
                    'template' => 'contact',
                    'from'     => 'myemail@gmail.com',
                    'replyTo'  => $data['email'],
                    'to'       => 'myemail@gmail.com',
                    'subject' => 'Testing',
                    'data'     => [
                        'text'   => esc($data['text']),
                    ]
                ]);

            }

            catch (Exception $error) {}


        }
    }

    return [
        'data'    => $data ?? false,
        'success' => $success ?? false
    ];


};

/templates/contact.json.php:

<?php

echo json_encode([
    'data'    => $data ?? false,
    'success' => $success ?? false
]);

?>

js function:

$(form').on('submit', function(e) {
  e.preventDefault();
  // define some variables
  var form = $(this);
  // var url  = form.attr('action');
  var url  = form.attr('action') + '.json';
  var data = form.serialize();

  $.ajax({
    type: 'POST',
    dataType: 'json',
    url: url,
    data: data,
    // if the ajax call is successful ...
    success: function(response) {
  
    console.log(response);

      // if registration was successful
      if(response.success) {
        console.log(response.success);
      }

      // if registration failed
      if(response.error) {
       console.log(response.error);
      }

    },
    error: function (xhr, ajaxOptions, thrownError) {
        console.log(xhr.status);
        console.log(thrownError);

      }
  });
});

@texnixe I have the impression that I am not passing any data to the form. Do you know what would be the best way to pass the kirby->email() to be triggered on the ajax call? Thanks a lot

Vanilla JS:

  let contactForm = document.querySelector('form');

  const handleForm = async (e) => {
    e.preventDefault();
    let url = `${window.location.href}.json`;
    const formData = new FormData(contactForm);
    try {
      const response       = await fetch(url, {
        method: "post",
        body: formData,
      });
      const { success, alert } = await response.json();
      
      // show error/success etc.
    } catch (error) {
      console.log('Fetch error: ', error);
    }
  }
  loginForm.addEventListener('submit', handleForm);
1 Like