Grav Modular Contact Form

Share on Facebook
Share on Twitter
Grav CMS offers an out-of-box solution for sending e-mails directly from a website's form. This solution works just fine and is sufficient most of the times. You may find a how to set up a basic contact form in this concise tutorial.
Share on Facebook
Share on Twitter

However, the things get a little bit more complicated when you wish to get the form running on a modular page. It gets even more complicated if you wish to prevent a page reload and use ajax for the task. Lastly, a small cherry on a cake is when the web form blueprint should be located in a different file (in a dedicated, yet unrouteable web form page). Additionally, one may often decide that the basic response message is not fitting in their website. Or wish to send e-mails with different structure, for example without any bold prefix.

Grav's documentation contains all the puzzle pieces we need. but spread all across. If you spend enough time, you'll find out how to put them together to get this easily replicable web form going. However, getting all the pieces for the first time could be tedious. Without further ado, let's begin.

Web Form blueprint

Firstly, we'll create a dedicated page that will contain the form. The idea is to separate this quazi page from the rest for a better maintainability and, importantly, for an easier replicability. It then only takes a moment to re-use the form on a different website.

To begin, create a regular Grav page, for instance one called contact-form. The page template is irrelevant, you could use even a non-existing one. Let's call our inquiry.md. So the structure is following:


// Folder structure
* contact-form
  * form.md

The following form's blueprint is stripped down to just two text inputs and a submit button. This page (/contact-form) is not routable, so it cannot be displayed nor indexed. The form itself contains a lot of attributes that are crucial for the correct implementation. Let's get through them one by one and build the other assets along the way.


// form.md
title: 'Online contact form'
routable: false
form:
    action: /home
    name: contact-form
    template: custom-forms/form-messages
    refresh_prevention: true

    fields:
        -
            name: email
            label: 'E-mail'
            type: email
        -
            name: message
            label: 'Your message'
            type: textarea
    buttons:
        -
            type: submit
            value: 'Send message'
    process:
         -
            email:
                to: 'test@mail.com'
                from: 'test@mail.com'
                subject: 'Email: {{ form.value.name|e }}'
                body: '{% include ''custom-forms/data.html.twig'' %}'
        -
            save:
                fileprefix: feedback-
                dateformat: Ymd-His-u
                extension: txt
                body: '{% include ''forms/data.txt.twig'' %}'
        -
            message: 'Thank you for the message'

1 Overall setup

The parent page and the modular page

The first attribute is action: /home. This means that the modular page that contains the final form is a child of the home page. So the page structure looks as follows


// Folder structure
* 01.Home
  * _inquiry
    * inquiry.md
* contact-form
  * form.md

Parent page

The parent page isn't going to contain much. Even though in our example it has only one modular child, let's use a common loop through page collections. In the home page (index.md for instance) add


// index.md
content:
    items: '@self.modular'

And in the template (index.html.twig) this


// index.html.twig
{% for module in page.collection() %}
     {{module.content}}
{% endfor %}

Modular page

In the modular page, you don't have to put anything special. However, in it's template (inquiry.html.twig) put this


// inquiry.html.twig
{% include "forms/form.html.twig" with { form: forms('contact-form') } %}

This will find and include the contact form defined on the other page. In other words, it injects the contact form called contact-form to the modular page _inquiry which resides in /home. Simply put, by this approach one never leaves home page, even though separate actions happen in three different files. The form-result is where the form's submit response message will be displayed. We'll create this message in the next section.

One additional note. This code includes form.html.twig from forms, even though we haven't created anything like it. It uses the default form template provided by form plugin in user/plugins. You can customize that if you wish, but as you are able to define custom classes (more here) in frontmatter, it usually suffices.

Custom message template

The next attribute in the form's blueprint is template: custom-forms/form-messages. This enables to use a different template for the form response. As the official documentation says, this comes particularly handy when handling submits using ajax. The form plugin provides an out-of-box template for the response. However, you may wish to add custom classes or ID's or make the response much more complex. Therefore, it is often useful to use a custom form response template. To do so, create in your themes folder a new folder called custom-forms and add form-messages.html.twig into it. At this point, your template folder should contain at the bare minimum


// Folder structure
* custom-forms
  * form-messages.html.twig
* modular
  * inquiry.html.twig
* index.html.twig

Regarding the form-messages template, put anything you like inside. For instance something like this


// form-messages.html.twig
{% if form.message %}
    {% if form.inline_errors and form.messages %}
     

{{ "FORM.VALIDATION_FAIL"|t|raw }}

{% else %}

{{ form.message|raw }}

{% endif %} {% endif %}

This is copied from the default form-messages template. If message has been sent, check if it had ended with an error. If yes, throw in form.validation_fail message. If not, display thank you message defined in the form's blueprint. Even though we haven't changed the default response at all in this example, nothing prevents you to do so if you wish now.

2 Customizing templates

Submit with AJAX

The last area to tackle is using ajax the to handle the form submit. refresh_prevention: true in the form's blueprint is the first step. The ajax handler requires some JavaScript and for the simplicity, I'll follow the documentation and use jQuery (don't forget to include script in your site). In the inquiry.html.twig template (or in the index template below the loop), add the following.


// inquiry.html.twig
<script>
$(document).ready(function(){
    var form = $('#contact-form');
    form.submit(function(e) {
        // prevent form submission
        e.preventDefault();

        // submit the form via Ajax
        $.ajax({
            url: form.attr('action'),
            type: form.attr('method'),
            dataType: 'html',
            data: form.serialize(),
            success: function(result) {
                // Inject the result in the HTML
                $('#form-result').html(result);
            }
        });
    });
});
</script>

The ID in the form variable must match the name defined in the form's blueprint. The output element's ID must match the one we defined in the modular page.

Asynchronous & without reload

Handling submit

At this point, the contact form sends messages using ajax. The last thing from the blueprint to is the process part.


// form.md
...preceding code...

  process:
      -
          email:
              to: 'test@mail.com'
              from: 'test@mail.com'
               subject: 'Email: {{ form.value.name|e }}'
              body: '{% include ''custom-forms/data.html.twig'' %}'
      -
          save:
              fileprefix: feedback-
              dateformat: Ymd-His-u
              extension: txt
              body: '{% include ''forms/data.txt.twig'' %}'
      -
          message: 'Thank you for the message'

You can define sender and recipient using frontmatter or directly in the admin interface. The subject of the message is set to contain word Email: and then the sender's e-mail address. The body of the message (blueprints' textarea field) is processed using a our customdata.html.twig template. Let's create it.


{% macro render_field(form, fields) %}
    {% for index, field in fields %}
        {% set input = attribute(field, "input@") %}
        {% if input is null or input == true %}
             {# Skips textarea input and renders it at the bottom #}
            {% if form.value(field.name) and field.type != 'textarea'  %}
                {% block field %}
                    
{% block field_label %} {{ field.label|t|e }}: {% endblock %} {% block field_value %} {% if field.type == 'checkboxes' %}
    {% for value in form.value(field.name) %}
  • {{ field.options[value]|e }}
  • {% endfor %}
{% elseif field.type == 'checkbox' %} {{ (form.value(field.name) == 1) ? "PLUGIN_FORM.YES"|t|e : "PLUGIN_FORM.NO"|t|e }} {% elseif field.type == 'select' %} {{ field.options[form.value(field.name)]|e }} {% else %} {{ string(form.value(field.name))|nl2br }} {% endif %} {% endblock %}
{% endblock %} {% endif %} {% else %} {% if field.fields %} {{ _self.render_field(form, field.fields) }} {% endif %} {% endif %} {% endfor %}

{{ string(form.value(fields['message'].name))|nl2br }} {% endmacro %} {{ _self.render_field(form, form.fields) }}

As previously, this is mostly copied from the form plugin. The only difference is that this skips the textarea field at the beginning and instead of that, it outputs the field named message as last. Lastly, every message is also saved in user/data under forms name (contact-form) as a text file (it won't exist until you send first message). That's all the magic. At the end, your page structure that relates to our effort should look like this.


// Final folder structure
* user
  * data
    * contact-form
  * pages
    * 01.Home
      * _inquiry
        * inquiry.md
    * contact-form
      * form.md
  * themes
    * your-theme
      * custom-forms
        * data.html.twig
        * form-messages.html.twig
      * modular
        * inquiry.html.twig
      * index.html.twig

3 E-mail configuration

Conclusion

Grav is truly powerful. Some concepts take longer to understand the first time, but when you dedicate some time, you are generally able to create an universal and reusable solution which will save you a tremendous amount of time in future projects. Therefore, if you think that sending messages from modular pages with custom body structure and generation a custom response message without page reload is something you could need more often, congratulate yourself. By replicating this code you're all set.

< back