angularformsform-submitangular12pardot

Is there a way to submit() an HTMLFormElement form for Pardot without a redirect/reload? If not, what is an Angular way to emulate same?


Gonna try to make this as simple as possible. I've tried a slew of things and have been researching this for some time, and I think I'm missing something small.

Using Angular 12.

Sample form:

    <form id="my-form" action="https://example.com" action="post">
        <input name="fname">
        <button (click)="mySubmitFunction()">SUBMIT</button>
    </form>

I have within mySubmitFunction() logic like so:

  const myForm = document.querySelector('#my-form') as HTMLFormElement;
  myForm.submit();

With this method, a form submission occurs, which takes every <input> with a name attribute to populate the form data. You can see the resultant http request in your Developer Tools in Chrome for example in the Network tab.

My Issue with this is that data goes out to a third-party vendor, and a redirect occurs, redirecting to whatever we tell them to (in this case, it is the same page). So the user experience is that not only this form data but EVERYTHING ELSE on the page is cleared.

Most examples I've seen of what I am asking are essentially to not use the action/method/submit approach, but to define a button click method that creates an http request from scratch (which is the very first thing I tried actually). The problem with that, is that I get CORS issues on the http.post request, no matter what environment. Here is basically what I'm doing there:

 postContactInfoToVendor(requestBody: string): Observable<any> {
   const headers = {
     'Content-Type': 'application/x-www-form-urlencoded'
    };
  // Where the requestBody is formatted like 'fname=Bob'
  return this.http.post(environment.vendorUrl, requestBody, { headers });
}

So I'm kinda stuck, wondering if the many great minds out there have solved this before and I just can't find it.

I also notice there are a LOT of additional headers sent along with the form's submit() request, but I have no clue where they are coming from - wondering if this will help with the CORS issues. Headers like "Cookie" with "visitor_id"-type entries, and others.


Possible solutions I'm thinking of:

  1. Hosting my contact form within an iframe on the page. Then on success redirect, which we are able to configure on the vendor's side, can go to a thank you page.

  2. Intercepting the form.submit()'s request somehow, cancel the request and send a new one modeled with the exact same headers. I have been unable to intercept these requests at this time though.

As always, thanks for any help guys.


UPDATE: My specific issue is with Pardot form submission. There is a workaround in Angular for this, though I'm not sure how I feel about it.

Piecing together information from Simulate a JSONP response with JavaScript URLs and How to make a simple JSONP asynchronous request in Angular 2?, I was able to import HttpJsonpModule and use that with

http.jsonp(url + urlEncodedString, 'callback');

Then, I configure the success/error URLs to statically-hosted json files that will be returned in the respective responses to be used in the success/error callbacks (to my understanding). I'll be testing that out over the next couple of days but just wanted to give a current update.

Although, generally speaking, I know my <iframe> solution would have worked as well :)

UPDATE: I've finished my solution using not HTMLFormElement.submit(), nor http.post, but an http.jsonp() request, only because JSONP is supported server-side.

I'll write up my own answer here shortly with all information.


Solution

  • The simple answer to my first question is that it can't be done. Your page location will have to change no matter what you do. Please someone correct me if this is wrong.

    The medium answer to my second question is that it possibly could be done with an http request vs. HTMLFormElement's .submit(), if you can mimic the form's request and use Angular's HttpClient's http.post or get request instead. This didn't work for me though, since I couldn't mimic all the headers or/and there were CORS issues on the form handler side (3rd party server, Pardot) that I have no control over.

    A workaround in general cases is to host the form within an <iframe>, if you can convince your team it will be OK :). Then, the redirect/reload behavior would ONLY occur within the <iframe>, not to your page at large.


    My solution, since I was told to avoid <iframe>s, is to find another method of support through our 3rd party vendor. There is a somewhat (I feel) shady protocol called JSONP that Angular and Pardot support, which gets around CORS issues. But the setup was kind of overkill for this task. The full details below:

    1. In my module, app.module.ts, I had to import HttpClientJsonModule from @angular/common/http.

    2. I created an interface that consists of all Pardot's expected fields for us.

    3. In a function I activate when the submit button is pressed, I build the form typed to that interface from the form fields, and submit from a service like so:

        postContactInfoToPardot(formData: PardotForm): Observable<any> {
          const formParams = parameterize(formData);
    
          /*
          Angular adds &callback=ng_jsonp_callback_0 or ng_jsonp_callback_1 etc. to the request, which is the name of
            the success callback (will not get hit in this case bc we are not calling it directly)
          'callback' is the name of the parameter that Pardot expects
          There doesn't seem to be a way for our defined redirected .js to know about the number in ng_jsonp_callback_0,
            so just eat the error that will say "JSONP injected script did not invoke callback" in caller's error
            callback.
           */
          return this.http.jsonp(`${environment.pardotUrl}?${formParams}`, 'callback');
        }
    
    ...
    
    // A function we have defined as a utility function
    function parameterize(body: any): string {
      return new HttpParams({fromObject: body}).toString();
    }
    
    1. I host two simple .js files that can be used as success/error redirects. Under src/assets/pardot I have pardot-response-error.js and pardot-response-success.js

    2. The contents of those files is

    // Defined in src/index.html
    pardotCallback({ 'result' : 'error' })
    

    and

    // Defined in src/index.html
    pardotCallback({ 'result' : 'success' })
    

    respectively.

    1. In src/index.html, I've got:
        <script>
            function pardotCallback(response) {
                const xhr = new XMLHttpRequest();
    
                if (response.result === 'success') {
                    xhr.open('POST', '#{loggingUrl}#?level=info');
                    xhr.send('Pardot form submission success');
                } else {
                    xhr.open('POST', '#{loggingUrl}#?level=error');
                    xhr.send('Pardot form submission error');
                }
            }
        </script>
    

    You can do anything you want there - our team just wanted a request to go to our backend server to log success or errors. (We replace #{loginUrl}# with an environment-specific value. I can't use my environment file for this, so this is done via our pipeline)

    1. The last thing on my side is the handling of errors. Where I call my service function to send form data to Pardot, if we don't handle this one error, the end users will see it in their console as an error for every form submission:
          // Due to the nature of Angular + http.jsonp + Pardot, we will have an error, every time.
          this.service.postContactInfoToPardot(pardotForm).subscribe(
            () => {},
            (err: HttpErrorResponse) => {
              // ONLY deal with an error here if it is not the one we are expecting every time
              if (err.error.toString().indexOf('JSONP injected script did not invoke callback') === -1) {
                // TODO: Replace with a call to the logging service, but this block also should never run
                console.error(err);
              }
            }
          );
    
    1. Now, finally, on the Pardot form handler config side of things, we set our success redirect (currently the label says "Success Location") to our prod success.js, and something similar for the error redirect (currently the label is "Error Location") to our error.js. For example,

    Success Location = https://<host>/assets/pardot/pardot-response-success.js


    So as you can see, this setup is very weird and I don't think I would recommend it to a future team, but it does work. I hope this is helpful to anyone looking for the same information, either Pardot-specific or regarding my original question.