Andrew Welch · Insights · #vuejs #graphql #craft-3

Published , updated · 5 min read ·


Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.

Using VueJS + GraphQL to make Practical Magic

Make some prac­ti­cal mag­ic with Vue­JS + GraphQL to solve every­day prob­lems like auto-com­plete search­ing and form sub­mis­sion sav­ing with a head­less Craft CMS server

The advance of new tech­nolo­gies can be daunt­ing. We hear about shiny new things like Vue­JS and GraphQL, but there’s only so much time in the day to learn every new thing that comes along.

So I think a more prac­ti­cal approach is to fig­ure out how these tech­nolo­gies can help us solve the real world prob­lems we face every day.

This article presents two practical examples using VueJS + GraphQL that will feel like magic

Here are the two prob­lems we’re going to solve:

  • Auto-com­plete search — dynam­i­cal­ly present a list of results as the user types
  • Con­tact form sub­mis­sion sav­ing — The abil­i­ty to take form sub­mis­sion data, and save it to a back­end database

So let’s get down to it, and talk about the tool­ing we need to get the job done.

Link Tooling

Every project needs at least a lit­tle bit of tool­ing; I’ve tried to keep it to a min­i­mum here, so we can focus on the exam­ples. But we still need some.

Here’s what we’ll be using for our tool­ing on the frontend:

  • Vue­JS — a fron­tend JavaScript frame­work that is approach­able, ver­sa­tile, and performant
  • Axios — a JavaScript library for doing http requests
  • Boot­strap 4 — a pop­u­lar CSS frame­work, just so our exam­ples don’t look ugly

For sim­plic­i­ty’s sake, all of these fron­tend resources will just be grabbed from a CDN. I used Boot­strap because much as I love Tail­wind CSS, I did­n’t want to get dis­tract­ed by util­i­ty-first CSS in the examples.

If you’re not famil­iar with Vue­JS, that’s okay. You could do the same thing with jQuery, vanil­la JS, or what­ev­er you like. It’d just be more work; we’re just using Vue­JS here to make the GraphQL exam­ples eas­i­er to do.

A full expla­na­tion of Vue­JS is beyond the scope of this arti­cle, but you can check out the fol­low­ing resources if you want to learn more:

Hey, where’s the GraphQL?

You might be look­ing at the list, and won­der­ing to your­self Hey, where’s the GraphQL?” There’s a good rea­son it isn’t list­ed there; GraphQL is a spec­i­fi­ca­tion, not an imple­men­ta­tion. So there’s no JavaScript to include at all!

Here’s what we’ll be using for our tool­ing on the backend:

  • Craft CMS — a won­der­ful CMS that offers a rich con­tent author­ing experience
  • CraftQL — Mark Huot’s excel­lent plu­g­in sim­ply pro­vides a GraphQL lay­er on top of Craft CMS

The exam­ples will be using Craft CMS as the back­end, but the glo­ry of JAM­stack tech­nolo­gies like Vue­JS + GraphQL is that the back­end does­n’t real­ly mat­ter. You could swap out what­ev­er you want­ed to use on the back­end! We’re using Craft CMS as a head­less” CMS just to serve up our con­tent data via API.

Even if you’re not using Craft CMS, almost every­thing in this arti­cle will apply. So read on!

Link Auto-complete search

It’s pret­ty com­mon that we might want to pro­vide the abil­i­ty for peo­ple to type in a search field, and have it dynam­i­cal­ly list a series of results.

For this exam­ple, we have a blog sec­tion in Craft CMS that has some sam­ple data in it. We want to let peo­ple type in a field to find blog entries that match what they are typing.

The end result looks like this on the frontend:

Auto-com­plete search result

At the top we have a Search field, and below it we present a dynam­ic list of match­es to blog entries as they type. Below that is just some debug­ging infor­ma­tion that may help you under­stand what’s going on under the hood.

I’m going to jump around a bit in this expla­na­tion, but the full source will be at the end of the article.

Link Vue Instance for Auto-complete search

So… how do we accom­plish this? Let’s start with defin­ing the data we need to make this hap­pen, and cre­ate our Vue instance around it.

With VueJS, the DOM is a side-effect of your data, not the other way around

This is what I love about Vue­JS. You define the data as the source of truth for your appli­ca­tion, and the HTML result is just a byprod­uct of it. 

Let’s have a look:

// Instantiate our Vue instance
    new Vue({
        el: '#demo',
        data: {
            searchApi: axios.create(configureApi(apiUrl, apiToken)),
            searchQuery: '',
            searchResults: {}
        },
        methods: {
            // Perform a search
            performSearch() {
                // If they haven't entered anything to search for, return nothing
                if (this.searchQuery === '') {
                    this.searchResults = {};
                    return;
                }
                // Set the variables we will pass in to our query
                const variables = {
                    sections: searchSections,
                    needle: searchPrefix + this.searchQuery,
                    limit: 5
                };
                // Execute the query
                executeQuery(this.searchApi, searchQuery, variables, (data) => {
                    this.searchResults = data.data.entries;
                });
            }
        }
    })

Our data is pret­ty sim­ple, and con­sists of just:

  • searchApi — the Axios instance we’ll use to send & receive GraphQL via http (more on this later)
  • searchQuery — the search string the user is look­ing for
  • searchResults — and object with the results (if any) of their search

The configureApi() func­tion looks like this:

// Configure the api endpoint
    const configureApi = (url, token) => {
        return {
            baseURL: url,
            headers: {
                'Authorization': `Bearer ${token}`,
                'X-Requested-With': 'XMLHttpRequest'
            }
        };
    };

It’s return­ing a con­fig object that we can pass to axios.create() so that all of our http requests have the same basic set­tings. We’re just cre­at­ing our own Axios instance that is pre-con­fig­ured with the set­tings we want. 

Here are the set­tings we pass in:

// Information needed for connecting to our CraftQL endpoint
    const apiToken = 'wwYfgLejf27AxoSmR0K3wUzFoj9Y96QSNTICvpPslO2l2JcNsjfRY9y5eIec5KhN';
    const apiUrl = '/api';

While this might seem over­ly com­pli­cat­ed, what if we had mul­ti­ple API URLs? Or what if we had dif­fer­ent per­mis­sions for each type of API call? This makes it eas­i­er to set up our API end­points in a reusable way.

apiUrl is set to the default /api URL that CraftQL lis­tens to for GraphQL requests. apiToken is a Bear­er Token that CraftQL uses to grant per­mis­sion to read and write data in Craft CMS.

In the Craft AdminCP, you cre­ate these bear­er tokens:

And define what per­mis­sions they have:

None of this is unique to Craft CMS or CraftQL; what­ev­er you end up using on the back­end, there will be a URL to access the API, and a bear­er token to define permissions.

Link HTML for Auto-complete search

So that’s our Vue instance; before we get to the performSearch() method and our GraphQL, let’s have a look at the HTML tem­plate we’re using:

<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>

<body>
<div class="container p-5">
    {% verbatim %}
    <form id="demo" autocomplete="off">
        <div class="form-group">
            <label for="searchQuery">Search:</label>
            <input v-model="searchQuery" v-on:keyup="performSearch()" id="searchQuery" class="form-control" type="text" />
        </div>

        <div class="form-group">
            <ul class="list-group">
                <li v-for="(searchResult, index) in searchResults" class="list-group-item">
                    <a v-bind:href="searchResult.url">{{ searchResult.title }}</a>
                </li>
            </ul>
        </div>

        <div class="form-group">
            <pre>data: {{ $data }}</pre>
        </div>
    </form>
    {% endverbatim %}
</div>

So noth­ing too excit­ing here; we have our JavaScript and Boot­strap CSS com­ing from CDNs.

Then we have the rather strange look­ing {% verbatim %} state­ment. This is just a Twig tag that tells Craft CMS not to process any­thing inside of it. We have to do this because both Twig and Vue­JS uses the same mus­tache {{ }} delim­iters, and we want to be using Vue­JS here, not Twig.

Then we have an input that is bound to our searchQuery data in Vue­JS via the v-model attribute. This means that any time the val­ue of the searchQuery data changes, so does our input… and vice ver­sa, any time the user types some­thing into the input, the val­ue in our searchQuery data is updated.

There is also a v-on:keyup attribute set on it that caus­es Vue­JS to call the performSearch() method any time there’s a keyup event. This is what caus­es our API call to GraphQL to hap­pen dynam­i­cal­ly as the user types.

This is the reactive magic of VueJS at work

After that we have a list item that has the v-for attribute set on it. This caus­es Vue­JS to ren­der a <li> for every object in our searchResults data.

So when­ev­er data is returned from our GraphQL API end­point, the searchResults data gets updat­ed, which caus­es the DOM on the fron­tend to mag­i­cal­ly update with all of the results.

If there are no results, then noth­ings renders!

The {{ $data }} at the bot­tom just dumps all of the data in our Vue instance as JSON, so we can see what’s going on under the hood.

Link GraphQL for Auto-complete Search

Now let’s have a look in more detail at our performSearch() method. While this is tech­ni­cal­ly still part of our Vue instance, it’s rel­e­vant to the GraphQL query we’ll be doing:

// Perform a search
            performSearch() {
                // If they haven't entered anything to search for, return nothing
                if (this.searchQuery === '') {
                    this.searchResults = {};
                    return;
                }
                // Set the variables we will pass in to our query
                const variables = {
                    sections: searchSections,
                    needle: searchPrefix + this.searchQuery,
                    limit: 5
                };
                // Execute the query
                executeQuery(this.searchApi, searchQuery, variables, (data) => {
                    this.searchResults = data.data.entries;
                });
            }

First it just checks to see if the searchQuery is an emp­ty string, and if so sets searchResults to an emp­ty object, and returns.

We do this because if we pass an emp­ty search string into our Craft CMS back­end, it’s going to return all results. We want it to return none.

Then it sets the variables we’re going to pass in to our GraphQL query. If you’re famil­iar with Craft CMS, this should seem fair­ly sim­i­lar to what we might pass in to craft.entries to look up data:

  • sections — the Sec­tions to search in Craft CMS
  • needle — the search string to look for; this is what­ev­er the user typed pre­fixed with searchPrefix
  • limit — the num­ber of results we want returned

To make things easy to change, we’ve defined the fol­low­ing constants:

// What to search for
    const searchSections = ['blog'];
    const searchPrefix = 'title:';

The searchSections tells it we only want to search the blog sec­tion. The searchPrefix is used to lim­it the search to just the title field, and it all works just the same as Search­ing in Craft CMS. If we want­ed it to search every­thing in an entry, we could just set this to be an emp­ty string ('').

Final­ly we get to some GraphQL! Next it calls executeQuery(), pass­ing in our Axiois API we cre­at­ed, the query we want to exe­cute, our variables, and then a call­back function.

Here’s what the searchQuery GraphQL query looks like:

// The query to search for entries in Craft
    const searchQuery =
        `
        query searchQuery($sections: [SectionsEnum], $needle: String!, $limit: Int)
        {
            entries(section: $sections, search: $needle, limit: $limit) {
                title
                url
            }
        }
        `;

While the syn­tax may look a lit­tle funky to you, it should be pret­ty clear what’s going on here. We’re defin­ing a GraphQL query called searchQuery and we’re defin­ing the names of the incom­ing vari­ables as well as their type. The ! after a type def­i­n­i­tion means that the vari­able is required, and [] is array syn­tax in GraphQL.

This is an impor­tant con­cept in GraphQL; it has a strict type sys­tem to ensure the puri­ty & cor­rect­ness of the data being passed into it. See the GraphQL doc­u­men­ta­tion on Schemas & Types for more infor­ma­tion, if you’re curious.

You must tell GraphQL not only what variables you’re passing in, but what type of data they expect

GraphQL uses the query we pass in along with the vari­ables to deter­mine what data to select. Then the title and url are telling GraphQL what data we want back.

This is anoth­er impor­tant con­cept in GraphQL: it will only return to you the data you ask for! So even though these blog entries may con­tain a huge amount of data, it’s only going to return to us the title and url that we’re ask­ing for.

GraphQL returns only what you ask for, which means it can be super lightweight

Even if the syn­tax of the query does­n’t make 100% sense to you, that’s okay. You can see that it’s send­ing in some data to look for in the query, and defin­ing what it’s returning.

When the query is com­plete, it will call our call­back function:

(data) => {
    this.searchResults = data.data.entries;
}

It only calls our call­back if the result­ing query is suc­cess­ful; and we just set our searchResults to a sub­set of the data (just the entries) that was returned.

So good enough, let’s look at the guts of the executeQuery() func­tion to see what exact­ly it’s doing:

// Execute a GraphQL query by sending an XHR to our api endpoint
    const executeQuery = (api, query, variables, callback) => {
        api.post('', {
            query: query,
            variables: variables
        }).then((result) => {
            if (callback) {
                callback(result.data);
            }
            console.log(result.data);
        }).catch((error) => {
            console.log(error);
        })
    };

It’s actu­al­ly real­ly sim­ple! We’re not using any heavy GraphQL-spe­cif­ic JavaScript, we’re just using our Axios instance that we cre­at­ed to send a POST to our API URL with our data!

The first para­me­ter to the .post() method is the URL which gets append­ed to the baseURL we spec­i­fied ear­li­er when we cre­at­ed our Axios instance. Since we’re just using one URL for all of our API, we pass in an emp­ty string ('').

The sec­ond para­me­ter to the .post() method is the data object we want to POST to the API end­point; all we need here is the query and variables for our GraphQL query.

Then since the .post() method returns a Promise, then we call our callback when the data suc­cess­ful­ly returns, or we catch any errors, and log them to the console.

Link Have a Beer!

Phew! Are you tired? I’m tired! But I think the actu­al con­cepts here are not so bad, there is just some new nomen­cla­ture to learn.

We cov­ered most of the impor­tant con­cepts that you need to under­stand how every­thing works already, so have a beer to cel­e­brate, then let’s dive in to Con­tact form sub­mis­sion saving.

It won’t be that bad, since the major­i­ty of it is the same!

Link Con­tact form sub­mis­sion sav­ing

Anoth­er com­mon thing that needs doing is the user enters some data on the fron­tend, and you want to save it on the back­end in a database.

In our case, we want to save peo­ple’s name, email address, and mes­sage from a con­tact form into our data­base on the back­end so that our CRM folks can get back in touch with them.

On the fron­tend, it looks like this:

Con­tact form sub­mis­sion sav­ing result

So, pret­ty stan­dard. The user fills in a Name, Email, and Mes­sage, then clicks on the Sub­mit but­ton… and we save the infor­ma­tion in the data­base on the backend.

We also dis­play a nice lit­tle mes­sage to the user telling them that the sub­mis­sion was suc­cess­ful­ly sub­mit­ted. It’s the lit­tle things.

Link Vue Instance for Contact form submission saving

Our Vue instance for the con­tact form is going to look pret­ty familiar:

// Instantiate our Vue instance
    new Vue({
        el: '#demo',
        data: {
            contactApi: axios.create(configureApi(apiUrl, apiToken)),
            contactName: '',
            contactEmail: '',
            contactMessage: '',
            submitted: false
        },
        methods: {
            // Submit the contact form
            submitContactForm() {
                // Set the variables we will pass in to our mutation
                const variables = {
                    contactName: this.contactName,
                    contactEmail: this.contactEmail,
                    contactMessage: this.contactMessage,
                };
                // Execute the query
                executeQuery(this.contactApi, contactFormMutation, variables, (data) => {
                    this.submitted = true;
                });
            }
        }
    })

We have our data as follows:

  • contactApi — the Axios instance we’ll use to send & receive GraphQL via http
  • contactName — the name the user enters into the con­tact form
  • contactEmail — the email address the user enters into the con­tact form
  • contactMessage — the mes­sage the user enters into the con­tact form
  • submitted — whether or not the con­tact form was suc­cess­ful­ly submitted

The configureApi() func­tion looks… well, dang, it’s exact­ly the same as we used in on Auto-com­plete Search exam­ple. Yay, code re-use!

The only thing that is dif­fer­ent are the set­tings we pass in, because we have a sep­a­rate bear­er token for the con­tact form that has per­mis­sions that allow it to save data to our Con­tact Form channel:

// Information needed for connecting to our CraftQL endpoint
    const apiToken = 'DxOES1XTDtnFVILEp0kNcOpvJpRXOmjFQci4lz6jLrrUqan6zTJ02ZkZyM_VTXlH';
    const apiUrl = '/api';

This is great, it’s lever­ag­ing every­thing we’ve done already, so let’s move right along to the HTML for the con­tact form!

Link HTML for Con­tact form sub­mis­sion sav­ing

Before we get into what the submitContactForm() method does, let’s have a look at the HTML tem­plate for our con­tact form:

<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>

<body>
<div class="container p-5">
    {% verbatim %}
    <form id="demo" autocomplete="off">
        <div class="form-group">
            <label for="contactName">Name:</label>
            <input v-model="contactName" id="contactName" class="form-control" type="text" />
        </div>

        <div class="form-group">
            <label for="contactEmail">Email:</label>
            <input v-model="contactEmail" id="contactEmail" class="form-control" type="text" />
        </div>

        <div class="form-group">
            <label for="contactMessage">Message:</label>
            <textarea v-model="contactMessage" id="contactMessage" class="form-control"></textarea>
        </div>

        <div class="form-group">
            <button v-on:click="submitContactForm()" type="button" class="btn btn-primary">Submit</button>
        </div>

        <div v-if="submitted" class="alert alert-primary" role="alert">
            Message submitted!
        </div>

        <div class="form-group">
            <pre>data: {{ $data }}</pre>
        </div>
    </form>
    {% endverbatim %}
</div>

Again we have the same JavaScripts and Boot­strap CSS at the top, and then we have a pret­ty stan­dard look­ing con­tact form HTML, with inputs for each piece of data that we want.

We again use the v-model attribute to bind the inputs to the appro­pri­ate data in our Vue instance, so we get that love­ly reac­tiv­i­ty when­ev­er data is entered.

Then we have a but­ton with the v-on:click attribute set, so that it’ll call our submitContactForm() method when­ev­er the user clicks on the button.

Final­ly, we have a <div> with the v-if attribute set to dis­play only if submitted is true, to dis­play a nice mes­sage to the user to let them know their sub­mis­sion worked. Because we care.

Link GraphQL for Con­tact form sub­mis­sion sav­ing

Now let’s get back to our submitContactForm() method to see what it’s doing:

// Submit the contact form
            submitContactForm() {
                // Set the variables we will pass in to our mutation
                const variables = {
                    contactName: this.contactName,
                    contactEmail: this.contactEmail,
                    contactMessage: this.contactMessage,
                };
                // Execute the query
                executeQuery(this.contactApi, contactFormMutation, variables, (data) => {
                    this.submitted = true;
                });
            }

So pret­ty sim­ple, we’re extract­ing out the variables we want to pass along to GraphQL, and we’re call­ing executeQuery() again to exe­cute our query.

The nifty thing here is that executeQuery() is once again exact­ly the same code! Even though we’re doing some­thing dif­fer­ent (sav­ing data instead of read­ing data), we can use the exact same executeQuery() method!

Everything in GraphQL is a query

When we want to change or add new data in GraphQL, that’s called a muta­tion. Muta­tions are just anoth­er query that hap­pen to also change or add data.

So here’s what our contactFormMutation looks like:

// The mutation to write contact form data to Craft
    const contactFormMutation =
        `
        mutation contactFormMutation($contactName: String!, $contactEmail: String!, $contactMessage: String!)
        {
            upsertContactForm(
                authorId: 1
                title: $contactName
                contactName: $contactName
                contactEmail: $contactEmail
                contactMessage: $contactMessage
            ) {
            id
            }
        }
        `;

So it looks pret­ty sim­i­lar to what we were doing before, but instead of query it’s now mutation. We’re still telling GraphQL what vari­ables we’re pass­ing in, and also the types of those variables.

But we’ve added upsertContactForm() that has a list of the data we want to upsert into the data­base. Upsert just means add or update data,” and the Con­tact­Form part is the name of the Sec­tion we want to upsert into.

Then since a muta­tion is just a type of query, we have to tell GraphQL what data we want returned; in this case we just ask for the id of the new­ly cre­at­ed entry back.

The fields we’re upsert’ing into the Con­tact Form chan­nel match what we have defined in Craft CMS:

Con­tact form feilds

The only thing slight­ly unusu­al about this is what we’re pass­ing in a hard-cod­ed authorId; this is because all Entries need to be owned by some­one in Craft CMS.

That’s it! We’re sav­ing entries in the Craft CMS backend.

Obvi­ous­ly there’s more we could do here, such as val­i­dat­ing the form input with vee-val­i­date, hid­ing the form after it’s been sub­mit­ted, etc. But that’s left as an exer­cise for you, dear reader.

Link Wrapping Up

While this may seem like a good bit to take in, once you get famil­iar with how GraphQL works, it’s infi­nite­ly eas­i­er to use than rolling your own” cus­tom API with the Ele­ment API, and you’ll have learned a skill that trans­lates to many dif­fer­ent platforms.

The best part is… you’ve sep­a­rat­ed your API from the sys­tem that imple­ments it. So if you decide to move to a dif­fer­ent CMS or plat­form, it makes it infi­nite­ly eas­i­er to do so!

One of the most fun and enjoy­able ways you can learn GraphQL is by sim­ply play­ing around with the in-brows­er GraphiQL IDE that is includ­ed with the CraftQL plu­g­in:

GraphiQL IDE

You can play around with your queries & muta­tions with an auto-com­plete edi­tor that knows the schema of your entry Craft CMS back­end. It’s so fun!

If you just can’t get enough GraphQL, the GraphQL basics and prac­ti­cal exam­ples with Vue arti­cle is a great place to go next. Also check out the GraphQL: Bet­ter than all the REST? pod­cast on dev​Mode​.fm!

Enjoy your day!

Link Auto-complete search full source

Here’s the full source to the Auto-com­plete search example:

<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>

<body>
<div class="container p-5">
    {% verbatim %}
    <form id="demo" autocomplete="off">
        <div class="form-group">
            <label for="searchQuery">Search:</label>
            <input v-model="searchQuery" v-on:keyup="performSearch()" id="searchQuery" class="form-control" type="text" />
        </div>

        <div class="form-group">
            <ul class="list-group">
                <li v-for="(searchResult, index) in searchResults" class="list-group-item">
                    <a v-bind:href="searchResult.url">{{ searchResult.title }}</a>
                </li>
            </ul>
        </div>

        <div class="form-group">
            <pre>data: {{ $data }}</pre>
        </div>
    </form>
    {% endverbatim %}
</div>

<script>
    // Information needed for connecting to our CraftQL endpoint
    const apiToken = 'wwYfgLejf27AxoSmR0K3wUzFoj9Y96QSNTICvpPslO2l2JcNsjfRY9y5eIec5KhN';
    const apiUrl = '/api';
    // What to search for
    const searchSections = ['blog'];
    const searchPrefix = 'title:';
    // The query to search for entries in Craft
    const searchQuery =
        `
        query searchQuery($sections: [SectionsEnum], $needle: String!, $limit: Int)
        {
            entries(section: $sections, search: $needle, limit: $limit) {
                title
                url
            }
        }
        `;
    // Configure the api endpoint
    const configureApi = (url, token) => {
        return {
            baseURL: url,
            headers: {
                'Authorization': `Bearer ${token}`,
                'X-Requested-With': 'XMLHttpRequest'
            }
        };
    };
    // Execute a GraphQL query by sending an XHR to our api endpoint
    const executeQuery = (api, query, variables, callback) => {
        api.post('', {
            query: query,
            variables: variables
        }).then((result) => {
            if (callback) {
                callback(result.data);
            }
            console.log(result.data);
        }).catch((error) => {
            console.log(error);
        })
    };
    // Instantiate our Vue instance
    new Vue({
        el: '#demo',
        data: {
            searchApi: axios.create(configureApi(apiUrl, apiToken)),
            searchQuery: '',
            searchResults: {}
        },
        methods: {
            // Perform a search
            performSearch() {
                // If they haven't entered anything to search for, return nothing
                if (this.searchQuery === '') {
                    this.searchResults = {};
                    return;
                }
                // Set the variables we will pass in to our query
                const variables = {
                    sections: searchSections,
                    needle: searchPrefix + this.searchQuery,
                    limit: 5
                };
                // Execute the query
                executeQuery(this.searchApi, searchQuery, variables, (data) => {
                    this.searchResults = data.data.entries;
                });
            }
        }
    })
</script>
</body>
</html>

Link Contact form submission saving full source

Here’s the full source for the Con­tact Form Sub­mis­sion Saving:

<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>

<body>
<div class="container p-5">
    {% verbatim %}
    <form id="demo" autocomplete="off">
        <div class="form-group">
            <label for="contactName">Name:</label>
            <input v-model="contactName" id="contactName" class="form-control" type="text" />
        </div>

        <div class="form-group">
            <label for="contactEmail">Email:</label>
            <input v-model="contactEmail" id="contactEmail" class="form-control" type="text" />
        </div>

        <div class="form-group">
            <label for="contactMessage">Message:</label>
            <textarea v-model="contactMessage" id="contactMessage" class="form-control"></textarea>
        </div>

        <div class="form-group">
            <button v-on:click="submitContactForm()" type="button" class="btn btn-primary">Submit</button>
        </div>

        <div v-if="submitted" class="alert alert-primary" role="alert">
            Message submitted!
        </div>

        <div class="form-group">
            <pre>data: {{ $data }}</pre>
        </div>
    </form>
    {% endverbatim %}
</div>

<script>
    // Information needed for connecting to our CraftQL endpoint
    const apiToken = 'DxOES1XTDtnFVILEp0kNcOpvJpRXOmjFQci4lz6jLrrUqan6zTJ02ZkZyM_VTXlH';
    const apiUrl = '/api';
    // The mutation to write contact form data to Craft
    const contactFormMutation =
        `
        mutation contactFormMutation($contactName: String!, $contactEmail: String!, $contactMessage: String!)
        {
            upsertContactForm(
                authorId: 1
                title: $contactName
                contactName: $contactName
                contactEmail: $contactEmail
                contactMessage: $contactMessage
            ) {
            id
            }
        }
        `;
    // Configure the api endpoint
    const configureApi = (url, token) => {
        return {
            baseURL: url,
            headers: {
                'Authorization': `Bearer ${token}`,
                'X-Requested-With': 'XMLHttpRequest'
            }
        };
    };
    // Execute a GraphQL query by sending an XHR to our api endpoint
    const executeQuery = (api, query, variables, callback) => {
        api.post('', {
            query: query,
            variables: variables
        }).then((result) => {
            if (callback) {
                callback(result.data);
            }
            console.log(result.data);
        }).catch((error) => {
            console.log(error);
        })
    };
    // Instantiate our Vue instance
    new Vue({
        el: '#demo',
        data: {
            contactApi: axios.create(configureApi(apiUrl, apiToken)),
            contactName: '',
            contactEmail: '',
            contactMessage: '',
            submitted: false
        },
        methods: {
            // Submit the contact form
            submitContactForm() {
                // Set the variables we will pass in to our mutation
                const variables = {
                    contactName: this.contactName,
                    contactEmail: this.contactEmail,
                    contactMessage: this.contactMessage,
                };
                // Execute the query
                executeQuery(this.contactApi, contactFormMutation, variables, (data) => {
                    this.submitted = true;
                });
            }
        }
    })
</script>
</body>
</html>