4

for the life of me I can't figure out what is happening. I've been spending all day searching for the answer but can't find it anywhere. I'm practicing with writing a function that emails an abandoned form to me.

This is the index.js file I run for the local host that hosts the form

const inputSelector = document.getElementById("name");
const fieldSelector = document.querySelectorAll('.formfield');
let formData = {};


fieldSelector.forEach(field =>{

    field.addEventListener('input', (e) =>{

        let formField = e.target.id;
        formData[formField] = field.value;
        
    });
})


window.addEventListener('beforeunload', () =>{
    fetch('http://localhost:8080/', {
        method:'post',
        headers:{
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            message: formData
        })
    })
})

The function that the request is send to, is a cloud function that contains this code:

exports.testFetch = async (req, res) =>{
    console.log(req.method);
    if(req.method === 'OPTIONS'){
        console.log('method is option')
        res.set('Access-Control-Allow-Origin', "*")
        res.set('Access-Control-Allow-Methods', 'GET, POST');
        res.set('Access-Control-Allow-Headers', 'Content-Type');
    }

    else{
   
        console.log('full body: ', req.body);
        console.log('message: ', req.body.message);
    }

    res.send('response')

}

Now whenever I fill out the form fields and then browse from that page to another page it works perfectly, the cloud function console.logs the form fields. However when I just CLOSE the page by clicking X the cloud function only console.log 'OPTIONS' and 'method is option'. It looks like it only sends the options part of the post request. I've been spending all afternoon looking for a solution but I can't find it anywhere. Super frustrating. When I just add the fetch function to a button and then press the button it works perfectly aswell. It's just with closing the browser that it doesn't seem to work and gets stuck in OPTIONS. If you have any tips, please let me know!

RogerKint
  • 454
  • 5
  • 13

3 Answers3

17

As Endless's answer discusses, sendBeacon is an option. But the problem is your fetch request is not using the keepalive flag.

Normally, when a document is unloaded, all associated network requests are aborted. But the keepalive option tells the browser to perform the request in the background, even after it leaves the page. So this option is essential for our request to succeed.

window.addEventListener('beforeunload', () => {
        fetch('http://localhost:8080/', {
            method:'post',
            headers:{
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                message: formData
            }),
            keepalive: true // this is important!
        })
    })
Elliott Beach
  • 10,459
  • 9
  • 28
  • 41
  • This should've been the aceepted answer. – Arad Alvand Feb 09 '22 at 15:29
  • Changed it! Just now saw it (this answer was placed almost a year after the other) – RogerKint Aug 05 '22 at 14:25
  • Yes! Now I get page read times / time on page in Matomto tracker even if they leave by sending a ping on exit. Did get the "Stay on page?" popup so used `window.onbeforeunload = function() { matomoExit(); return; };` – zx81roadkill Oct 25 '22 at 19:48
8

Try using navigator.sendBeacon

It's meant for sending a request durning the unload

window.addEventListener('unload', () => {
  navigator.sendBeacon(url, data)
})

side note: i would recommend using FormData to serialize your data. you could send that instead of some json. but you must use the name attribute instead of id. which you should do either way.

var fd = new FormData(formElement)
// if you want json anyway
var json = JSON.stringify(Object.fromEntries(fd))

I would have removed the fieldSelector.forEach(... and just build the formdata once on the unload event

Since you can't specify any headers with sendBeacon you could send a Blob instead

var blob = new Blob([json], {type: 'application/json')
navigator.sendBeacon(url, blob)

more info here https://stackoverflow.com/a/41729668/1008999


if this dose not works for you try using the keep alive flag in fetch { keepalive: true }

Endless
  • 34,080
  • 13
  • 108
  • 131
  • 1
    Thank you so much!!! It works flawlessly now :D You helped me out big time! And I rewrote it btw so now the formdata is building on the unload event, you were right it's better! I recently started programming and I feel like I'm doing OK but sometimes I wish I had someone who could point stuff like this out! Thanks again for your help, much appreciated! – RogerKint Jul 30 '20 at 17:48
0

Please note that the Mozilla documentation of sendBeacon is advising against using sendBeacon with the unload or beforeunload event:

In the past, many websites have used the unload or beforeunload events to send analytics at the end of a session. However, this is extremely unreliable. In many situations, especially on mobile, the browser will not fire the unload, beforeunload, or pagehide events. For example, these events will not fire in the following situation:

  1. The user loads the page and interacts with it.
  2. When they are finished, they switch to a different app, instead of closing the tab.
  3. Later, they close the browser app using the phone's app manager.

Additionally, the unload event is incompatible with the back/forward cache (bfcache) implemented in modern browsers. [...]

Source

I can confirm, that sendBeacon is working in most use cases, but if you need certainty, that the request is sent, you should not be relying on those events.

Jan
  • 23
  • 4