<!--Companion article: https://css-tricks.com/headless-form-submission-with-the-wordpress-rest-api/-->
<p class="bg-secondary text-center text-primary p-1">
    There's no reply. Submissions are deleted automatically and immediately.
</p>

<div class="container">

    <div class="columns col-gapless">

        <div class="col-sm-12 col-md-8 col-4 col-mx-auto">

            <div x-data="wpForm()">

                <h4>Contact Form 7</h4>
                <div class="divider"></div>

                <div class="toast" :class="{'toast-success': isSuccess, 'toast-error': !isSuccess}" x-show="message" x-text="message" x-cloak></div>

                <form action="https://cssformsrestapi.tastewp.com/wp-json/contact-form-7/v1/contact-forms/5/feedback" method="post" autocomplete="off" x-ref="form" @submit.prevent="submit">

                    <!--The "required" attributes are not used on purpose.-->

                    <div class="form-group" :class="{'has-error': validationError['somebodys-name']}">
                        <label class="form-label" for="somebodys-name">Somebody's name</label>
                        <input class="form-input" id="somebodys-name" type="text" name="somebodys-name">
                        <div class="form-input-hint" x-show="validationError['somebodys-name']" x-text="validationError['somebodys-name']" x-cloak></div>
                    </div>

                    <div class="form-group" :class="{'has-error': validationError['any-email']}">
                        <label class="form-label" for="any-email">Any valid email address</label>
                        <!--The type="email" is not used on purpose.-->
                        <input class="form-input" id="any-email" type="text" name="any-email">
                        <div class="form-input-hint" x-show="validationError['any-email']" x-text="validationError['any-email']" x-cloak></div>
                    </div>

                    <div class="form-group" :class="{'has-error': validationError['before-space-age']}">
                        <!--Space Age began at October 4, 1957-->
                        <label class="form-label" for="before-space-age">A date before the Space Age</label>
                        <!--The "max" attribute is not used on purpose.-->
                        <input class="form-input col-8" id="before-space-age" type="date" placeholder="yyyy-mm-dd" name="before-space-age">
                        <div class="form-input-hint" x-show="validationError['before-space-age']" x-text="validationError['before-space-age']" x-cloak></div>
                    </div>

                    <div class="form-group" :class="{'has-error': validationError['optional-message']}">
                        <label class="form-label" for="optional-message">Optional message to the world</label>
                        <textarea class="form-input" id="optional-message" name="optional-message"></textarea>
                        <div class="form-input-hint" x-show="validationError['optional-message']" x-text="validationError['optional-message']" x-cloak></div>
                    </div>

                    <div class="form-group" :class="{'has-error': validationError['fake-terms']}">
                        <label class="form-switch">
                            <input name="fake-terms" type="checkbox" value="1">
                            <i class="form-icon"></i>
                            Fake and obligatory terms and conditions checkbox
                        </label>
                        <div class="form-input-hint" x-show="validationError['fake-terms']" x-text="validationError['fake-terms']" x-cloak></div>
                    </div>

                    <div class="form-group">
                        <button type="submit" class="btn btn-primary">
                            Submit
                        </button>
                    </div>

                </form>

            </div>

            <div class="mt-12 mt-6" x-data="wpForm()">

                <h4>Gravity Forms</h4>
                <div class="divider"></div>

                <div class="toast" :class="{'toast-success': isSuccess, 'toast-error': !isSuccess}" x-show="message" x-text="message" x-cloak></div>

                <form action="https://cssformsrestapi.tastewp.com/wp-json/gf/v2/forms/1/submissions" method="post" autocomplete="off" x-ref="form" @submit.prevent="submit">

                    <!--The "required" attributes are not used on purpose.-->

                    <div class="form-group" :class="{'has-error': validationError['input_1']}">
                        <label class="form-label" for="input_1">Somebody's name</label>
                        <input class="form-input" id="input_1" type="text" name="input_1">
                        <div class="form-input-hint" x-show="validationError['input_1']" x-text="validationError['input_1']" x-cloak></div>
                    </div>

                    <div class="form-group" :class="{'has-error': validationError['input_2']}">
                        <label class="form-label" for="input_2">Any valid email address</label>
                        <!--The type="email" is not used on purpose.-->
                        <input class="form-input" id="input_2" type="text" name="input_2">
                        <div class="form-input-hint" x-show="validationError['input_2']" x-text="validationError['input_2']" x-cloak></div>
                    </div>

                    <div class="form-group" :class="{'has-error': validationError['input_3']}">
                        <!--Space Age began at October 4, 1957-->
                        <label class="form-label" for="input_3">A date before the Space Age</label>
                        <!--The "max" attribute is not used on purpose.-->
                        <input class="form-input col-8" id="input_3" type="date" placeholder="yyyy-mm-dd" name="input_3">
                        <div class="form-input-hint" x-show="validationError['input_3']" x-text="validationError['input_3']" x-cloak></div>
                    </div>

                    <div class="form-group" :class="{'has-error': validationError['input_4']}">
                        <label class="form-label" for="input_4">Optional message to the world</label>
                        <textarea class="form-input" id="input_4" name="input_4"></textarea>
                        <div class="form-input-hint" x-show="validationError['input_4']" x-text="validationError['input_4']" x-cloak></div>
                    </div>

                    <div class="form-group" :class="{'has-error': validationError['input_5']}">
                        <label class="form-switch">
                            <input name="input_5_1" type="checkbox" value="1">
                            <i class="form-icon"></i>
                            Fake and obligatory terms and conditions checkbox
                        </label>
                        <div class="form-input-hint" x-show="validationError['input_5']" x-text="validationError['input_5']" x-cloak></div>
                    </div>

                    <div class="form-group">
                        <button type="submit" class="btn btn-primary">
                            Submit
                        </button>
                    </div>

                </form>

            </div>

        </div>

    </div>

</div>
.mt-12 {
    margin-top: calc(0.2rem * 12);
}

.mt-6 {
    margin-bottom: calc(0.2rem * 6);
}

[x-cloak] {
    display: none;
}
// https://css-tricks.com/snippets/javascript/strip-html-tags-in-javascript/
const stripHtml = (string) => string.replace(/(<([^>]+)>)/gi, "");

const initialState = {
    isSuccess: false,
    message: "",
    validationError: {}
};

const normalizeResponse = (url, response) => {
    if (
        url.match(/wp-json\/contact-form-7\/v1\/contact-forms\/\d+\/feedback/)
    ) {
        return normalizeContactForm7Response(response);
    }

    if (url.match(/wp-json\/gf\/v2\/forms\/\d+\/submissions/)) {
        return normalizeGravityFormsResponse(response);
    }

    return {
        ...initialState,
        ...{
            message: "Are you submitting to the right URL?"
        }
    };
};

const normalizeGravityFormsResponse = (response) => {
    const isSuccess = response.is_valid;
    const message = isSuccess
        ? stripHtml(response.confirmation_message)
        : "There was a problem with your submission.";
    const validationError = isSuccess
        ? {}
        : Object.fromEntries(
              Object.entries(
                  response.validation_messages
              ).map(([key, value]) => [`input_${key}`, value])
          );

    return {
        isSuccess,
        message,
        validationError
    };
};

const normalizeContactForm7Response = (response) => {
    const isSuccess = response.status === "mail_sent";
    const message = response.message;
    const validationError = isSuccess
        ? {}
        : Object.fromEntries(
              response.invalid_fields.map((error) => {
                  const key = /cf7[-a-z]*.(.*)/.exec(error.into)[1];

                  return [key, error.message];
              })
          );

    return {
        isSuccess,
        message,
        validationError
    };
};

const wpForm = () => {
    return {
        ...initialState,
        submit() {
            const formElement = this.$refs.form,
                { action, method } = formElement,
                body = new FormData(formElement);

            fetch(action, {
                method,
                body
            })
                .then((response) => response.json())
                .then((response) => normalizeResponse(action, response))
                .then((response) => {
                    this.updateState(response);

                    if (this.isSuccess) {
                        formElement.reset();
                    }
                })
                .catch((error) => {
                    this.updateState({
                        ...initialState,
                        ...{
                            message: "Check the console for the error details."
                        }
                    });
                    console.log(error);
                });
        },
        updateState(newState) {
            Object.keys(newState).forEach((key) => (this[key] = newState[key]));
        }
    };
};

window.wpForm = wpForm;
View Compiled

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/spectre.css/0.5.9/spectre.min.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/alpinejs/2.8.1/alpine.js