DEV Community

Andrew Welch
Andrew Welch

Posted on • Originally published at nystudio107.com on

2

Using VueJS + GraphQL to make Practical Magic

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

Andrew Welch / nystudio107

Vuejs Graphql Craft Cms Magic

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.

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.

Tool­ing

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.

Craft Cms Vue Js Graphql Tooling

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:

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!

Auto-com­plete 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 Complete Search

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.

Vue Instance for Auto-com­plete 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.

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:

Craftql Bearer Rokens

And define what per­mis­sions they have:

Craftql Token Scopes

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.

HTML for Auto-com­plete 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.

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.

GraphQL for Auto-com­plete 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.

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.

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.

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.

Have A Beer

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!

Con­tact form sub­mis­sion saving

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:

Contact Form Submission Saving

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.

Vue Instance for Con­tact form sub­mis­sion 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!

HTML for Con­tact form sub­mis­sion saving

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.

GraphQL for Con­tact form sub­mis­sion saving

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!

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:

Contact Form Craft Cms Fields

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.

Wrap­ping 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.

Victory Image

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 Explorer Auto Complete

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!

Auto-com­plete 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>

Con­tact form sub­mis­sion sav­ing 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>

Further Reading

If you want to be notified about new articles, follow nystudio107 on Twitter.

Copyright ©2020 nystudio107. Designed by nystudio107

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Instrument, monitor, fix: a hands-on debugging session

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

Tune in to the full event

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. ❤️