Cached Forms #

When using caching mechanisms with Formie, it's worth taking note of some caveats to ensure things work correctly.

Template Caching #

If you are using the {% cache %} Twig functions in your templates, you'll need to be mindful of CSS and JS assets will not work. For example, say you have a Twig template with the following:

<html>
<head></head>
<body>
    {% cache %}

    {{ craft.formie.renderForm('contactForm') }}

    {% endcache %}
</body>
</html>

Here, we're using the {% cache %} tag to wrap out renderForm function. What this will do is cache the HTML generated by the function. Whilst this is beneficial, the renderForm function also registers the CSS and JS that Formie uses, and this won't be cached. You'll find on subsequent page-reloads that the CSS and JS will not render.

To get around this, you'll need to call craft.formie.registerAssets() outside of your cached content. This will tell Formie and Craft to render the CSS and JS for the form.

<html>
<head></head>
<body>
    {% cache %}

    {{ craft.formie.renderForm('contactForm') }}

    {% endcache %}

    {# Register the assets used by Formie, outside of the cached tags #}
    {% do craft.formie.registerAssets('contactForm') %}
</body>
</html>

Refreshing CSRF Token and Captchas #

Whilst the form will now be cached, this will cause issues with Formie's CSRF token, which is also cached. This needs to be unique per-request, so we need a method of being able to update this. Similarly, some captchas that rely on the output for the form will fail. Notably the JavaScript captcha and the Duplicate captcha, as their content will be cached.

Continue reading the next section for a more detailed explanation and how to handle refreshing this information dynamically.

Static Caching #

It's quite commonplace to implement full-page static caching on sites. For Craft, we highly recommend the Blitz plugin, but you can use any number of methods to statically cache your pages.

However, caching the form for every visitor poses an issue for Formie's CSRF tokens and captchas used to verify the integrity of form submissions and spam submissions. Indeed, this problem will be the same for any form on your site. To get around this, you'll need to implement a way to refresh these tokens in your forms through JavaScript.

Let's take a look at some examples in action.

CSRF Token #

{% set form = craft.formie.forms.handle('contactForm').one() %}

{{ craft.formie.renderForm(form) }}

{# Ensure we load polyfills for older browsers that don't support `fetch()` #}
<script src="https://cdn.polyfill.io/v2/polyfill.js?features=fetch,Promise"></script>

<script>
    // Wait until the DOM is ready
    document.addEventListener('DOMContentLoaded', (event) => {
        // Fetch the form we want to deal with
        let $form = document.querySelector('#{{ form.formId }}');

        // Find the CSRF token hidden input, so we can replace it
        let $csrfInput = $form.querySelector('input[name="CRAFT_CSRF_TOKEN"]');

        // Fetch the new token for the form and replace the CSRF input with our new one
        fetch('/actions/formie/forms/refresh-tokens?form={{ form.handle }}')
            .then(result => { return result.json(); })
            .then(result => { $csrfInput.outerHTML = result.csrf.input; });
    });
</script>

Here, we've combined rendering the form as we normally would, with some extra JavaScript. While this entire code will be cached and served exactly the same to each visitor, the JavaScript will be executed when the page is loaded. The above script makes a GET call to our actions/formie/forms/refresh-tokens controller action, which returns a collection of useful token information - part of which is a fresh CSRF token.

We use this to inject and replace the cached CSRF token (which is completely invalid now), after which the form will submit as expected. It's a little bit of extra work to get things working with a static cached page, but it's worth it for significant performance gains!

The response from the formie/forms/refresh-tokens action would look something like:

{
    "csrf": {
        "param": "CRAFT_CSRF_TOKEN",
        "token": "MVHMpS1zZXotiEYY...",
        "input": "<input type=\"hidden\" name=\"CRAFT_CSRF_TOKEN\" value=\"MVHMpS1zZXotiEYY...\">"
    },
    "captchas": {
        "duplicate": {
            "sessionKey": "__DUP_91804410",
            "value": "617138283f857"
        },
        "javascript": {
            "sessionKey": "__JSCHK_91804410",
            "value": "617138283f878"
        }
    }
}

We'll cover the captchas portion of this shortly, but you'll notice the csrf.param, csrf.token and csrf.input are available, which we've used above in our fetch() callback. Our example shows using csrf.input for convenience, but use whatever you prefer.

fetch('/actions/formie/forms/refresh-tokens?form={{ form.handle }}')
    .then(result => { return result.json(); })
    .then(result => {
        // Use `csrf.input` for convenience
        $csrfInput.outerHTML = result.csrf.input;

        // Use `csrf.param` and `csrf.token`
        $form.querySelector('input[name="' + result.csrf.param + '"]').value = result.csrf.token;
    });

Captchas #

As shown above, the formie/forms/refresh-tokens action also contains information about captchas. Because some captchas rely on the page content being unique, we must update them dynamically now that the page is statically cached.

The response from the formie/forms/refresh-tokens contains information on captcha tokens:

{
    "captchas": {
        "duplicate": {
            "sessionKey": "__DUP_91804410",
            "value": "617138283f857"
        },
        "javascript": {
            "sessionKey": "__JSCHK_91804410",
            "value": "617138283f878"
        }
    }
}

Which we can use in our callback to find the hidden <input> elements, and update their value attributes.

{% set form = craft.formie.forms.handle('contactForm').one() %}

{{ craft.formie.renderForm(form) }}

{# Ensure we load polyfills for older browsers that don't support `fetch()` #}
<script src="https://cdn.polyfill.io/v2/polyfill.js?features=fetch,Promise"></script>

<script>
    // Wait until the DOM is ready
    document.addEventListener('DOMContentLoaded', (event) => {
        // Fetch the form we want to deal with
        let $form = document.querySelector('#{{ form.formId }}');

        // Fetch the new tokens for the form and replace the captcha inputs
        fetch('/actions/formie/forms/refresh-tokens?form={{ form.handle }}')
            .then(result => { return result.json(); })
            .then(result => {
                // JavaScript captcha
                let jsCaptcha = result.captchas.javascript;

                // Find the JavaScript captcha hidden input, so we can update it
                if (jsCaptcha) {
                    $form.querySelector('input[name="' + jsCaptcha.sessionKey + '"]').value = jsCaptcha.value;
                }

                // Duplicate captcha
                let duplicateCaptcha = result.captchas.duplicate;

                // Find the Duplicate captcha hidden input, so we can update it
                if (duplicateCaptcha) {
                    $form.querySelector('input[name="' + duplicateCaptcha.sessionKey + '"]').value = duplicateCaptcha.value;
                }
            });
    });
</script>

Here we're implementing the same approach as the CSRF token, by getting fresh information for each captcha, querying for the hidden <input> elements in the form, and updating those values. The result.captchas will only contain token information for the captchas you have enabled, so if you aren't using all of them, you need not include the respective captchas - if if you're not using the JavaScript or Duplicate captcha, this can be omitted altogether.

Get started with Formie

Available for Craft 3. Get it from the plugin store.