verbb / formie

The most user-friendly forms plugin for Craft CMS.
Other
96 stars 72 forks source link

Receive HTML from GraphQL endpoint #2087

Open aloco opened 1 month ago

aloco commented 1 month ago

What are you trying to do?

Currently working on a headless Craft CMS project with astro in front accessing data via GraphQL and trying to find the easiest way to use Formie in this setup. Since mapping all Formie data from GraphQL to actual HTML seems very complicated for simple forms I am looking for an easy solution for most cases.

What's your proposed solution?

For convenience I would like to request the actual HTML directly via GraphQL, including required scripts and styles .. Hyper for instance is providing a HTML string via GraphQL as link which is very handy when no customization is needed (besides replacing some urls)

Additional context

No response

engram-design commented 1 month ago

I think you're after the templateHtml option.

{
  formieForm(handle: "simpleForm") {
    templateHtml(options:"{\"renderJs\": true}")
  }
}

One thing you'll want to do is set a Form Template and have it set CSS and JS inline, rather than output in the usual spot of the header and footer respectively. With that enabled, you'll get a bundle of everything you need to embed a Formie form on your BYO front-end.

aloco commented 1 month ago

Nice this is exactly what I need. Thanks!

Loading the form works so far but I still have two issues:

1) it looks like the phone field is broken when embedding the form in this way, other js depended fields work (e.g. the signature field)

2) in the markup, the action base url correctly points to my cms url, but when submitting the form via ajax it gets sent to the frontend url - any idea on this?

thank you!

engram-design commented 1 month ago

I'm not sure how you're embedding your HTML, but I have just realised that using something like innerHTML isn't going to work for <script> elements. They can't be evaluated with innerHTML and need to be appended to the DOM separately.

const formHandle = 'sample';
const endpoint = 'https://formie-craft5.test/api';

const query = `
  {
    formieForm(handle: "${formHandle}") {
      templateHtml
    }
  }
`;

fetch(endpoint, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: query
  })
})
  .then(response => response.json())
  .then(response => {
    const $form = document.querySelector('#my-form');

    $form.innerHTML = response.data.formieForm.templateHtml;

    // Script tags won't work using `innerHTML`, they need to be evaluated differently
    const scripts = $form.querySelectorAll('script');

    scripts.forEach(script => {
      const newScript = document.createElement('script');
      newScript.type = 'text/javascript';

      if (script.src) {
        newScript.src = script.src;  // Handle external scripts
      } else {
        newScript.textContent = script.textContent; // Handle inline scripts
      }

      document.body.appendChild(newScript);
    });
  });

Otherwise no JS is going to work correctly. But if your signature field is working correctly, I'd say that you already have something handling this, but I was getting similar issues when submitting the form via Ajax, it was just refreshing the page.

Just wanted to clarify you're handling that first?

engram-design commented 1 month ago

In light of this, just added templateJs and templateCss for separate handling. To get this early, run composer require verbb/formie:"dev-craft-5 as 3.0.8".

const formHandle = 'sample';
const endpoint = 'https://formie-craft5.test/api';

const query = `
  {
    formieForm(handle: "${formHandle}") {
      templateHtml
      templateCss
      templateJs
    }
  }
`;

fetch(endpoint, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: query
  })
})
  .then(response => response.json())
  .then(response => {
    console.log(response)
    const $form = document.querySelector('#my-form');

    // Inject HTML
    $form.innerHTML = response.data.formieForm.templateHtml;

    // Inject CSS
    const cssContainer = document.createElement('div');
    cssContainer.innerHTML = response.data.formieForm.templateCss;
    document.head.appendChild(cssContainer);

    // Inject JS
    const scriptContainer = document.createElement('div');
    scriptContainer.innerHTML = response.data.formieForm.templateJs;

    // Extract and run each script separately to ensure execution
    const scripts = scriptContainer.querySelectorAll('script');

    scripts.forEach(script => {
      const newScript = document.createElement('script');

      if (script.src) {
        newScript.src = script.src; // External scripts
      } else {
        newScript.textContent = script.textContent; // Inline scripts
      }

      document.body.appendChild(newScript);
    });
  })
  .catch(error => {
    console.error('Error:', error); // Handle any errors
  });
aloco commented 1 month ago

Hi, thank you for the input, I am using Astros <Fragment set:html={formTemplateHtml} /> I tried your approach and the problem is the same, I digged a little further and realized that formies ajaxSubmit function does this.$form.getAttribute("action") which is not set on the form html, it fallbacks to window.location.href doing the XMLHttpRequest. Looks like the action attribute is missing when retrieving the html in this way


PS: The JS seems to work (at least partially) because the form gets a new CSRF Token via:

    // Wait until Formie has been loaded and initialized
    document.addEventListener('onFormieInit', (event: any) => {
        // Fetch the Form Factory once it's been loaded
        let Formie = event.detail.formie;

        // Refresh the necessary bits that are statically cached (CSRF inputs, captchas, etc)
        Formie.refreshForCache(event.detail.formId);
    });
engram-design commented 1 month ago

That's there if you want to override the action attribute of the form, which in this case, that's probably something you want to do, setting that to either the Craft index.php (or any link to Craft) or to the formie/submissions/submit endpoint.

But while that's easy to do in custom templates (which is what this was originally for), it's not so much for GraphQL. I can make that amendment.

aloco commented 1 month ago

Ok thank you, yes it would be great when the form action points to the cms host as default so its less prone to errors when creating new forms in the backend while using it with GraphQL.

aloco commented 1 month ago

I solved this for now by setting the action manually, this works so far. However I ran into a CSRF issue, because the withCredentials of the XMLHttpRequest is per default false and currently, there is no way to configure this in formie. It might happen that the frontend runs with another domain than the cms so I think it makes sense to make this configurable.

engram-design commented 1 month ago

In that case, it probably makes sense to disable CSRF validation using the enableCsrfValidationForGuests config setting. In this instance, everyone is technically going to be a guest with a detached front-end.