{{craftSnippets}}
Home Articles Plugins Starter theme Components on Github Gists About
  • Home
  • Articles
  • Building AJAX contact form with Craft CMS
Posted on Jan 08, 2020 by Piotr Pogorzelski

Building AJAX contact form with Craft CMS

Contact form on github gists
Build AJAX-based contact form using Contact Form plugin for Craft CMS.
Table of contents:
  • Preparing your form
  • Twig code
  • Twig code explained
  • Javascript code
  • Javascript code explained
  • Notifications
  • Email message template
  • Multiple contact forms on the same page
  • Preventing spam messages
  • Contact form extensions plugin
  • Form builder plugins

In this article, I will describe how to build the AJAX-based contact form for Craft CMS using Contact form plugin. Contact form is the first-party plugin, created by Pixel&Tonic. It is usually used with standard forms that reload page upon submitting.

If however data is sent to contact form controller over AJAX and with proper headers, it returns a response in JSON format. So all we need to do is to properly format AJAX request and consume JSON response using Javascript to display a notification about successfully (or not) submitted message.

Preparing your form #

Before doing anything with Contact form plugin, you need to set up to email setting in its settings page in the control panel. It is an email address that contact form messages will be sent to. Until you do that, contact form will not work.

It might also be useful to connect your Craft install to email testing service - like Mailtrap. Thanks to that, you will be able to easily analyze and test messages sent by contact form. You can learn more about it in Testing emails sent by Craft CMS using Mailtrap article.

Once your contact form goes live on the production server, remember to check if emails are actually delivered to the desired address. If you are using Sendmail (built-in server email service) it's possible that the server doesn't send anything due to some issues with configuration. This will be logged in Craft logs as [warning] but Contact form plugin will still return a success message - so it's possible for such problems to be overlooked.

Twig code #

First, let's take care of the Twig code of contact form that renders its HTML elements. Here's contact form template component.

<form class="js-contact-form">
<fieldset class="js-contact-form-fieldset">
  {{ csrfInput() }}

    <div class="field">
        {{tag('label', {
          for: 'form-email',
          class: 'label',
          text: 'Your Email'|t('contact-form'),
        }) }}
        {{tag('input', {
          type: 'email',
          name: 'fromEmail',
          id: 'form-email',
          class: 'input',
          required: true,
        }) }}
    </div>

    <div class="field">
        {{tag('label', {
          for: 'form-name',
          class: 'label',
          text: 'Your Name'|t('contact-form'),
        }) }}
        {{tag('input', {
          type: 'text',
          name: 'fromName',
          id: 'form-name',
          class: 'input',
          required: true,
        }) }}
    </div>

  <div class="field">
        {{tag('label', {
          for: 'form-text',
          class: 'label',
          text: 'Message'|t('contact-form'),
        }) }}
        {{tag('textarea', {
          type: 'text',
          name: 'message[text]',
          id: 'form-text',
          class: 'input',
          required: true,
        }) }}
  </div>

  <div class="field">
    <button class="js-contact-form-send button is-primary">
        {{'Send'|t('app')}}
    </button>    
  </div>
</fieldset>
</form>

Twig code explained #

Contact form uses Bulma CSS classes for styling and classes with -js prefix for attaching Javascript functionality to HTML elements. These prefixed classes should not be used for styling so that CSS and Javascript code is decoupled.

Textarea, input and label elements are created using Twig tag function. Thanks to that, it's easy to set their HTML attributes.

For input labels and button text, we use built-in static message translations - labels use translations from Contact form plugin, while button uses "Send" word that exists in Craft static message translations. Thanks to using already existing static message translations, form will be automatically translated to your site locale - as long as there is static message translation for your language available in the system.

The form contains three visible fields - fromName, fromEmail and message[text], representing sender e-mail, his name, and text of the message. These fields are required by plugin - if you want to add your own fields, add them as a part of message object - for example message[phoneNumber]. You can read more in plugin documentation.

There is also one Twig function that generates hidden field - csrfInput . It generates field responsible for CSRF protection of form.

Javascript code #

In order to make use of AJAX, we need some Javascript code. I assumed that such code should be placed in contact form template file, within {% js %} tags. This allows you to keep your code more modular as well as maintainable, and simplifies passing Twig variables into Javascript. You can learn more about using Javascript in Twig templates in Using Javascript in Twig templates with Craft CMS
article.

I created two versions of JS code - one using jQuery ajax function and one using native ES6 Fetch API. Both essentially work the same.

Here's jQuery version:

{% js %}
// twig to js
var submit_url = "{{alias('@web')|e('js')}}";
var message_success = "{{'Your message has been sent.'|t('contact-form')}}";
var message_fail = "{{'Message could not be sent!'|t}}";

// html elements
var form_element = $('.js-contact-form');
var form_button = form_element.find('.js-contact-form-send')
var fieldset = form_element.find('.js-contact-form-fieldset')

// ajax request on submit
form_element.submit( function(e){

    e.preventDefault()
    var formData = new FormData(form_element[0]);
    formData.append('action', 'contact-form/send');

      $.ajax({
        contentType: false,
        processData: false,
        method: "POST",
        url: submit_url+'/',
        dataType: "json",
        data: formData,
        beforeSend: function( ) {
          form_button.addClass('is-loading');
          fieldset.prop('disabled', true);
        },
      }).always(function() {
        form_button.removeClass('is-loading');
        fieldset.prop('disabled', false);
      }).done(function( data ) {
        if(data.success == true){
          form_element[0].reset();
          alert(message_success);
        }else{
          var allErrors = Array();
          for ( fieldErrors of Object.values(data.errors)){
             var concatedArray =  allErrors.concat(fieldErrors);
             allErrors = concatedArray;
          }
          var errorString = allErrors.join(' ');
          alert(errorString);
        }
      }).fail(function( data ) {
          alert(message_fail);
      });
})
{% endjs %}

And here's Fetch API version:

{% js %}
// twig to js
var submit_url = "{{alias('@web')|e('js')}}";
var message_success = "{{'Your message has been sent.'|t('contact-form')}}";
var message_fail = "{{'Message could not be sent!'|t}}";

// html elements
var form_element = document.querySelector('.js-contact-form');
var form_button = form_element.querySelector('.js-contact-form-send')
var fieldset = form_element.querySelector('.js-contact-form-fieldset')


form_element.addEventListener("submit", function(e){

      e.preventDefault();   
      var formData = new FormData(form_element);
      formData.append('action', 'contact-form/send');

      form_button.classList.add('is-loading');
      fieldset.setAttribute('disabled', true);

     fetch(submit_url+'/', {
      method: 'POST', 
      body: formData,
      headers: {
        'Accept': 'application/json',
      },
     })
     .then(response => response.json())
     .then(data => {
        form_button.classList.remove('is-loading');
        fieldset.removeAttribute('disabled');
        if(data.success == true){
          form_element.reset();
          alert(message_success);
        }else{
          var allErrors = Array();
          for ( fieldErrors of Object.values(data.errors)){
             var concatedArray =  allErrors.concat(fieldErrors);
             allErrors = concatedArray;
          }
          var errorString = allErrors.join(' ');
          alert(errorString);
        }
     })
     .catch(err => {
      alert(message_fail)
    });

});
{% endjs %}

Javascript code explained #

First, we pass few Twig variables into Javascript variables - URL that AJAX request will be sent to and notifications text. Success message is taken from Contact form plugin built-in messages, while failure message is provided by our own static messages translations. After that, we define variables with HTML elements.

Next, we attach to submit event of the form. Upon sending, form fields are serialized into FormData object. We also need to append action variable with value of contact-form/send to formData - so request sent by form is sent to plugin controller. This could also be done by just using actionInput Twig function, but we want to make this form dependant of Javascript - so spambots that ignore JS and just submit form they find in HTML code won't succedeed.

Right before sending, is-sending Bulma class is added to the button. This will replace text inside the button with an animated spinner indicating that for is being sent. By placing disabled attribute on formset element, form will be made temporarily inactive. Thanks to that, impatient users won't send duplicate requests by constantly clicking "send" button. Both is-loading class and disabled attribute are removed when the request completes.

Unfortunately, even if Form could not be submitted due to validation errors, HTTP code returned by the controller is always 200 - which means success (this will be fixed in the next versions of Craft). That's why we cannot catch these errors in a separate callback function - we need to manually check for success value. If data returned from the controller contains success key with true value, we can assume that the message was sent successfully. In such a case, form is reset and notification with success message is displayed.

If controller returns object with error messages, these messages are concated into one string and also displayed in the notification - but form is not reset, so user can fix errors and try to resend it. Such a situation however would usually not occur. Form has proper required attributes and fromEmail field is set to type email - so frontend validation should prevent form with incorrect values from being submitted. There is also a separate callback for a situation when AJAX request was not completed at all or some other backend error occurred - in that case, another generic error message is displayed.

Notifications #

For success or error messages, the native alert function is used. This is of course not the best method to notify users of something. You may replace alert with some kind of notification library, for example pnotify.

You can easily overwrite native alert like this:

function alert(text){
    someNotification(text);
}

Email message template #

By default, Contact form plugin uses its internal template to render email messages. This template just lists fields used in contact form and their values. To use your own Twig template, you can use one of these two plugins:

  • Craft Contact Form Extensions - this plugin allows you to set template for contact form. You set a template globally in plugin settings - so if you use multiple forms on your website, all of them will use the same template file.
  • Contact Form Templates - this plugin allows you to set different templates for each form, by adding a special hidden field to it. Personally, I prefer this option.

Multiple contact forms on the same page #

If you want to use multiple contact forms on same page (for example, one form in page footer on desktop and one form in popup on mobile) you need to replace js-contact-form class in Javascript and Twig code with variable that is passed into form template component. Then include each form with different class variables.

Thanks to that, if you submit one form, all rest won't be submitted along with it. The rest of classes can stay the same - Javascript code uses them to search for elements only among children of main form class, not in whole document.

Preventing spam messages #

By virtue of being dependent on Javascript, this contact form is impervious to many of spambots. These bots often just scan the HTML code of the website and try to submit forms they find without using Javascript - and Javascript is required to add action value to data sent by form so request is routed to plugin controller.

If you want to further secure your form, you can use Google reCAPTCHA. You can do this using Contact Form Extensions plugin.

Contact form extensions plugin #

Contact form extensions plugin was already mentioned in a few places in this article. It greatly expands the functionality of standard contact form, allowing you to:

  • Save submissions to the database and view them in the Control Panel
  • Add a confirmation email that is sent to the submitting email
  • Overwrite the default e-mail template
  • Add an invisible reCAPTCHA to your forms

Form builder plugins #

If you need a more advanced solution, you need to familiarize yourself with form builder plugins. These plugins allow you to set up your form fields in the control panel, store submissions in database and render ready to use forms.

Solspace offers two form builder plugins - Express forms and Freeform. Each of these plugins comes in two editions and Lite edition of Express form is free.

There is also quite popular Sprout forms plugin - also comes in two editions, one of which is free.


TAGS:
#twig component
If you want to get latest updates on Craft CMS tutorials and components, follow me on Twitter or subscribe to RSS feed.
Quick links for this article:
Contact form on github gists
Articles on blog:
  • User account management with Craft CMS
  • Frontend testing for Craft CMS websites with Codeception and Cypress
  • Building reactive Craft Commerce product page with Sprig plugin
  • Dynamically generated PDF attachments for Freeform submissions
  • Using template hooks in Craft CMS
  • Alpine JS modal component for Craft CMS
  • Using template UI elements to extend Craft CMS control panel
  • Matrix within a Matrix - possible solutions for Craft CMS
  • Universal email template for Craft CMS
  • Creating attributes table from entry fields in Craft CMS
  • Namespacing forms in Craft CMS
  • Creating map-based navigation for Craft CMS
  • Placeholder image macro for Craft CMS
  • Building AJAX contact form with Craft CMS
  • Using incognito field plugin for Craft CMS
  • Email footer creator made with Craft CMS
  • Infinite scrolling and lazy loading with Craft CMS
  • Using Javascript in Twig templates with Craft CMS
  • Twig templating tips and tricks for Craft CMS
  • Basic SEO functionality for Craft CMS
  • Working with dates in Craft CMS templates
  • Working with SVG images in Craft CMS templates
  • Responsive and lazy-loaded youtube videos with Craft CMS
  • Debugging and inspecting Twig templates in Craft CMS
  • Creating article excerpts with Twig component in Craft CMS
  • Adding favicons to Craft CMS website
  • Truncating text with Twig macros in Craft CMS
  • Universal language switcher for Craft CMS
  • Read time macro for Craft CMS
  • Using attr() function to render HTML attributes in Craft CMS
  • Building dynamic, AJAX based pagination for Craft CMS
  • How to add Disqus comments to Craft CMS website
  • Ellipsis pagination component for Craft CMS
  • Converting email addresses into links using Twig macro
  • Breadcrumb created from URL for Craft CMS
  • Best developer-oriented Craft CMS plugins
  • Search autocomplete component for Craft CMS
  • RSS feed - template component for Craft CMS
  • Testing emails sent by Craft CMS using Mailtrap
  • Quick edit link - Twig component for Craft CMS
  • Filtering entries in control panel using Searchit plugin
  • Fetching routes into Twig templates in Craft CMS


User account management with Craft CMS

Frontend testing for Craft CMS websites with Codeception and Cypress

Building reactive Craft Commerce product page with Sprig plugin

Dynamically generated PDF attachments for Freeform submissions

Using template hooks in Craft CMS

Alpine JS modal component for Craft CMS

Using template UI elements to extend Craft CMS control panel

Matrix within a Matrix - possible solutions for Craft CMS

Copyright ©2023 Piotr Pogorzelski