This is a demo of a GraphQL, headless project using Craft CMS, Vue.js 3.0 and Formie. This repository comes in two parts, the Craft CMS install and the front-end headless project.
Take the demo for a spin!
We're using Vue for our front-end, but the techniques employed here can be utilised on any number of GraphQL-based projects. Even if you're not using Vue, we'd encourage you to spin up the project as-is to see it all working, and you can pick-and-choose functionality to use in your own applications.
- Vite.js is used for the build system as per Vite.js Next Generation Frontend Tooling + Craft CMS
- Vue.js 3.0 is used for the interactive bits.
- Tailwind CSS.
- ESlint to keep JS lookin' good!
- Vue Apollo Easy GraphQL handling using Apollo for queries and mutations.
- Pristine.js Simple form validation.
This repository contains the Craft CMS project files in the main root folder. Little to no changes have been made from the craftcms/craft project.
- Run
composer install
at the root of this project. - Ensure you setup your
.env
file as you normally would. - Point your favourite local server virtual host to the
web
directory. - We'll assume you'll set it up with the domain
http://formie-headless.test
- but this can be anything you like.
We've also included a database.sql
you can use to get things off the a great start. It comes pre-configured with a few forms, fields, and other mock data like entries, etc. GraphQL will also be setup.
The CP login credentials are initially set as follows:
Login: formie
Password: formie
You can also use your own Craft install - this is a headless demo after all! You'll just want to ensure Formie and Craft are enabled for GraphQL querying and mutations.
The front-end Vite project is contained in frontend
. The two projects are completely detatched (headless), but kept in the single repository for ease. Vite provides a super-fast dev server for local development. We've intentionally kept the demo as minimal as possible, but it's a full-featured implementation of Formie functionality.
There's two env settings you'll need to configure.
VITE_GQL_URL="http://formie-headless.test/api"
This is the full URL to your GraphQL endpoint. We'll use this to query forms and run mutations on submissions.
VITE_DEFAULT_FORM="contactForm"
The handle of the form you would like to show. You can also pass in a query param http://localhost:3000/?form=formHandle
to spin up an example of any form.
- Open a terminal and navigate to
frontend
, then runnpm install
. - Start up the Vite dev server by typing
npm run dev
. - Navigate to
http://localhost:3000
to see the demo.
You should see the "Contact Form"!
We've used Vue for this demo, but the concepts used here could be used in any framework. Be sure to check out the GraphQL sections below for the "good stuff".
Otherwise, the Vue components used in this demo are fully-featured, so there are some complexities that you might not need for your site. For example, being able to handle different label and instruction locations.
App
- The wrapper component that holds a form.FormieForm
- The form component, which handles all GraphQL handling, overall form-level functionalityFormiePage
- Each page in the form uses this component. Pages contain back/next page buttons.FormieField
- Each field wraps another component ininputs/
to keep things modular.inputs/
- Each input type has its own component file, as each require different implementations.mixins/FieldMixin
- Each field uses this mixin, and contains common functionality.
We decided not to include a Vue-based validation framework, to help not tie this project too deeply to Vue - in terms of functionality. VeeValidate and Vuelidate are both quite opinionated when it comes to structing components. We thought this would get in the way of understanding this demo project, and at the same time requires understanding of those frameworks to understand the form/field structure.
As such, our approach is very light-on, using Pristine.js which is entirely framework-agnostic. Of course, feel free to use your own validation options.
Formie supports querying forms via GraphQL queries, including form settings, pages, and of course fields. From this endpoint, you cna fetch everything you need about a form for rendering it in your app.
Have a look at the graphql/forms.js file, which contains the GraphQL query we use. We've split each field into a GraphQL fragment to easily re-use.
We then use Apollo to fetch the data from this query, given a handle for the form.
Formie supports creating submissions via GraphQL mutations. A mutation looks something like:
// Query
mutation saveSubmission($yourName: String) {
save_contactForm_Submission(yourName: $yourName) {
id
}
}
// Query Variables
{
"yourName": "Peter Sherman"
}
Here, you define the query which includes all field you want to create content for, and importantly their type. As such, this acts like a schema for the data you want to send. This also needs to be tailored to the handle you're using - contactForm
in this case. Then, you'll want to supply variables which contain the content.
While the above example seems simple, things quickly get complicated for large forms, or where you want clients to be able to create their own forms (rightly so!). You'd then need to setup a schema for every field you want to use.
To address this, we can use the data fetched for each field in our query to construct this schema string completely dynamically, using a few helper function. Have a look at the utils/mutations.js file for how we construct this. The getFormMutation()
will return the schema above, setup and ready to go - given a form object (received from the GraphQL query).
Also see how we construct the GraphQL schema and variables to send to the server.
A little extra work is required for captchas to be supported correctly. Ensure you add the following to your GQL form query to fetch tokens generated server-side.
{
form (handle: "contactForm") {
...
captchas {
handle
name
value
}
}
}
Here, we're fetching 3 vital bits of information, which is the handle
of the Captcha integration, the name
of the session variable used to compare the tokens between client and server, and the value
of the token.
To authenticate your enabled Captchas correctly, you'll need to include these in your mutation, sent to the server.
// Query
mutation saveSubmission($yourName:contactForm_yourName_FormieNameInput $javascriptCaptcha: FormieCaptchaInput) {
save_contactForm_Submission(yourName: $yourName, javascriptCaptcha: $javascriptCaptcha) {
title
}
}
// Query Variables
{
"yourName": {
"firstName": "Peter",
"lastName": "Sherman"
},
"javascriptCaptcha": {
"name": "__JSCHK_8403842",
"value": "1234"
}
}
The javascriptCaptcha
param above is using the handle
, name
and value
fetching during our initial query of the form. Our helper functions getMutationVariables()
and getFormMutation()
will add the correct typing to the mutation, and inject them into the variables sent alongside the mutation. But you can of course construct the mutation and variables manually as per the above if you don't use our helpers.
However, you'll also need to setup your Craft install to handle a persistent cookie session. This is because these captchas generate a unique value when the form is rendered (or in the case of GraphQL - when queried), which are then used to compare when the form is submitted. These values are stored server-side in a session, however this is not typically persisted for most requests due to CORS. Fortunately, this can be configurable through a number of different settings.
First, we'll need to modify our client-side code to include credentials
. This is to ensure your front-end code persists cookies with the server. For Apollo (which this demo uses) you can use:
const apolloProvider = createApolloProvider({
defaultClient: new ApolloClient({
// ...
// Enable sending cookies over cross-origin requests
credentials: 'include',
}),
});
You can also use credentials: 'same-origin'
if your backend server is the same domain, or credentials: 'include'
if your backend is a different domain.
Next, you'll need to configure your server (if you haven't already) to grant access via Access-Control-Allow-Origin
. Note that this must be a domain, and not the wildcard *
, as we are enforcing credentials in order to maintain a cookie session.
For Apache, this might look like the below. Of course, changing the origin to match your headless front-end site:
Header add Access-Control-Allow-Origin "https://my-headless-site.test"
Header add Access-Control-Allow-Credentials "true"
Lastly, we need to add the following to your general.php
file:
return [
'sameSiteCookieValue' => 'Lax',
'allowedGraphqlOrigins' => [
'https://my-headless-site.test',
],
];
Here, we are setting the sameSiteCookieValue to Lax
(further reading here) and setting the allowedGraphqlOrigins to our headless front-end site (essentially setting Access-Control-Allow-Origin
)
Once these have been done, maintaining a session between client and server should be possible. As such, captchas should be evaluated correctly.
reCAPTCHA support is slightly more involved, as you'll need to fetch the token from reCAPTCHA first, then attach it to your mutation. The same applies for hCaptcha. How you implement this is up to you, but typically in your form's submit handler, you'll have:
import { load } from 'recaptcha-v3';
upsert(array, element) {
const i = array.findIndex((el) => { return el.handle === element.handle; });
if (i > -1) {
array[i] = element;
} else {
array.push(element);
}
},
onSubmit(e) {
// Load reCAPTCHA and request a token. Then attach it to our mutation
load(import.meta.env.VITE_RECAPTCHA_KEY).then((recaptcha) => {
recaptcha.execute().then((token) => {
// Add it to the form variables so it can be prepped. Be sure to check if it already
// included in the form data (submitting multiple times in a single request)
this.upsert(this.form.captchas, {
handle: 'recaptchaCaptcha',
name: 'g-recaptcha-response',
value: token,
});
const formData = getMutationVariables(this.form, $form);
const formMutation = getFormMutation(this.form);
// Construct the mutation as normal
this.$apollo.mutate({
// ...
});
});
});
},
Where we first fetch the token from reCAPTCHA with our site key .env
variable, and add it manually to form.captchas
. Our helper functions getMutationVariables()
and getFormMutation()
will add the correct typing to the mutation, and inject them into the variables sent alongside the mutation.
We're also using an
upsert()
function to help when adding this captcha token to our forms object - which is completely optional. This just helps when submitting the form multiple times, and the captcha token for reCAPTCHA already exists (from the previous request). Theupsert()
function will unsure it's updated.
You can also use the framework-agnostic package to help, as we have done using npm install recaptcha-v3
.
Thanks to Dave Stockley / magicspon for their work on the mutations generator.