drunomics / nuxtjs-drupal-ce

A Nuxt module to easily connect Drupal via custom elements.
https://lupus-decoupled.org/
MIT License
24 stars 4 forks source link

Add support for JS-enabled form submissions #101

Open fago opened 1 year ago

fago commented 1 year ago

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

<!--
  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>