verbb / formie

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

Receive HTML from GraphQL endpoint #2087

Open aloco opened 4 days ago

aloco commented 4 days 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 4 days 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 3 days 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 2 days 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 2 days 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 15 hours 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);
    });