Here are the components we have been using so far:
LupusForm.vue
<!--
Handles form submission via a XHR request and rendering custom-elements formatted responses.
-->
<template>
<div class="lupus-form flex px-6">
<client-only v-if="$nuxt.isOffline">
<div class="text-state-error font-bold font-display text-3xl text-center m-2 p-1 mb-3">
You are offline!
</div>
</client-only>
<!-- Note: lupus-form-element must be re-initialized when form content changes after submission, so that event
handlers are attached again. v-if actually ensures this.
-->
<client-only>
<lupus-form-element v-if="!isSubmitting" ref="formContent" @submit="submit">
<!-- Show initial content from the slot until the form is reloaded. -->
<slot v-if="!contentComponent" />
<component :is="contentComponent" v-else />
</lupus-form-element>
</client-only>
<loading-indicator v-if="isSubmitting" />
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'lupus-form',
data: () => ({
isSubmitting: false,
contentComponent: null,
}),
computed: {
...mapState('drupalCe', ['page']),
},
mounted() {
// If the site is static and the form has form_build_id hidden input,
// we fetch the form_build_id from the backend and update the hidden input
// (this fixes an error when submitting a form on SSG site with incorrect/old form_build_id)
if (process.client && process.static && this.page.content.includes('name="form_build_id"')) {
this.$nextTick(async () => {
try {
const { data } = await this.$axios.get(this.$refs.formContent.$el.getAttribute('action'))
const formBuildId = data?.content?.match(/name="form_build_id".value="(.*)"/)?.[1]
if (!formBuildId) {
return
}
const formBuildIdInputElement = this.$refs.formContent.$el.querySelector(
'input[name="form_build_id"]'
)
formBuildIdInputElement?.setAttribute('value', formBuildId)
formBuildIdInputElement?.setAttribute('data-drupal-selector', formBuildId)
} catch (e) {
// Error fetching page data
}
})
}
},
methods: {
/**
* Asynchronous client-side submission of the form data.
*/
submit(formElement, formData) {
this.isSubmitting = true
this.$axios
.request({
method: formElement.method,
// Take the verbatim value which is a relative URL, so that the axios base URL applies.
url: formElement.getAttribute('action'),
// We transfer the form values using the browser's formData API - axios supports sending form data correctly.
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
})
.then((response) => {
this.$store.commit('drupalCe/messages', response.data.messages)
const isRedirect = response.data.redirect
// For external URLs we use window.location.href to redirect.
if (isRedirect) {
this.$lupusModal.close()
if (isRedirect.external) {
window.location.href = isRedirect.url
} else {
this.$store.$router.push(isRedirect.url)
}
} else {
this.setFormContent(response.data.content)
}
this.isSubmitting = false
})
.catch((error) => {
this.$store.dispatch('drupalCe/addMessage', {
type: 'error',
message:
'There was a problem submitting the form. Try again later or contact the site administrator. ',
})
// eslint-disable-next-line no-console
console.error(error)
this.isSubmitting = false
})
},
/**
* @param {content}
* A helper to get the form element from the content markup.
*
* @returns {String}
*/
getFormElementHelper(content) {
// Using regex to format the markup to only contain the form.
content = content.match(/<form([^]*)<\/form>/gim)[0]
// The LupusFormElement component expects only a <form> element, thus checking for it.
if (content.startsWith('<form')) {
return content
} else {
throw new Error('Missing form DOM element in lupus-form content.')
}
},
/**
* Renders the form based on the content string returned from the server.
*
* Using this helper to only send the <form> tag and its elements,
* excluding any unwanted elements(example <webform> or other custom elements) from the markup.
*
* @param {String} content
* The markup of the form as a string.
*/
setFormContent(content) {
const formContent = this.getFormElementHelper(content)
this.contentComponent = { name: 'template', template: formContent }
},
},
}
</script>
<style lang="postcss">
.visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
// some more default styling...
</style>
and LupusFormElement.vue
<!--
Helper to make handling the form DOM element from the parent vue component (lupus-form) easy.
-->
<script>
export default {
name: 'lupus-form-element',
data() {
return {
/**
* @type {HTMLFormElement}
*/
form: null,
}
},
mounted() {
/**
* Polyfill for the submitter to work in Safari
*/
require('event-submitter-polyfill')
if (this.$el.tagName !== 'FORM') {
this.form = null
throw new Error('Missing form DOM element in lupus-form content.')
}
this.form = this.$el
this.form.addEventListener('submit', (event) => {
if (!event.submitter) {
throw new Error('Missing form submitter - is the polyfill loaded?')
}
event.preventDefault()
if (this.form.reportValidity()) {
// Prepare formData with the value of the submit button used, so the backend can identify it.
const formData = new FormData(this.form)
formData.append(event.submitter.getAttribute('name'), event.submitter.getAttribute('value'))
this.$emit('submit', this.form, formData)
}
})
this.form.querySelectorAll('input, button, textarea, select').forEach((item) => {
item.addEventListener('change', () => {
this.$emit('change', this.form)
})
})
},
render() {
return this.$scopedSlots.default({})
},
}
</script>
See https://www.drupal.org/project/lupus_decoupled/issues/3336148 For Drupal form support we need some frontend components handling the form submission.
Here are the components we have been using so far:
LupusForm.vue
and LupusFormElement.vue