I have a html page that opens a modal using window.showModalDialog with a React application as the uri:
var retValue = window.showModalDialog ("http://localhost:8080/myreactapp, "dialogWidth:500px; dialogHeight:500px; dialogLeft:300px;");
This react app displays a form and has a data object to contain the form values. When I submit the form
I would the the updated data object to be sent to the parent html page of the modal .
I've been using the [postmessage api] (https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) for communicating between the parent and the child windows.
In the modal I have:
window.top.postMessage( message, 'http://localhost:3000/postMessage.html' );
I've also tried:
window.parent.postMessage( message, 'http://localhost:3000/postMessage.html' );
I can communicate from the react app to the parent when I use an iframe but not when using a modal.
The postmessage simply does nothing in modal, there is no error message and no message sent.
window.showModalDialog is deprecated in most browsers, so I would suggest using a modal component from a library such as react-bootstrap to achieve the same effect.
Given that, you can check the origin of your target windows to see if they match the origin you're passing in as the target. If they don't match, the message will not be sent.
For example
window.top.location.origin should match 'http://localhost:3000/postMessage.html'
Come to think of it, I've never seen origin specified with the actual leaf page, so maybe your target origin should be 'http://localhost:3000.
I was able to get this working.
In the html client I use two test methods :
function createWindow()
{
var win = window.open('http://localhost:8080/myreactapp', 'popup',
'status=
no,toolbar=no,location=no,
directories=no,
resisable=no,
srollbars=yes,
width=1050,height=600');
}
function createWindowV2() {
if (window.showModalDialog) {
showModalDialog ("http://localhost:8080/myreactapp", window, "dialogWidth:1000px; dialogHeight:800px; dialogLeft:300px;");
}
}
Then in the popup/modal window :
const [IE, setIE] = useState(false);
const [chrome, setChrome] = useState(false);
const [openerWindow, setOpenerWindow] = useState(null);
useEffect(() => {
const handler = event => {
if (typeof event.data !== 'undefined') {
document.getElementById('received-message').innerHTML = event.data;
}
};
window.addEventListener('message', handler);
setIE(/*#cc_on!#*/ false || !!document.documentMode);
setChrome(
!!window.chrome && (!!window.chrome.webstore || !!window.chrome.runtime)
);
setOpenerWindow(window.dialogArguments);
// clean up
return () => window.removeEventListener('message', handler);
}, []);
const handleSubmit = evt => {
evt.preventDefault();
if (IE) {
openerWindow.postMessage(
message,
'http://localhost:3000/postMessage.html'
);
} else {
window.opener.postMessage(
message,
'http://localhost:3000/postMessage.html' );
}
};
Related
I'm currently working on a project where I want to show a custom Dialogue box with my Own Content ("Save your data into drafts before leaving"). I have tried different methods but can't find a proper way to do it. I explore all the previous questions on StackOverflow but they didn't work properly in my case.
useEffect(() => {
return () => {
window.onbeforeunload = function() {
return "do you want save your data into drafts before leave?";
};
}
},[])
Currently, I've written the above code in Plain JavaScript to do, but it's just showing the dialogue box on tab close and reload while not showing on custom click events to navigate to other pages or window back button.
React can't help me in this because they remove useBlocker, usePrompt from new releases. How can I achieve it?
One way of doing this is :
import { Prompt } from 'react-router'
const MyComponent = () => (
<>
<Prompt
when={shouldBlockNavigation}
message='Do you want ot save data before leave?'
/>
{/* Component JSX */}
</>
)
If wants on page refresh or browser closing then add:
useEffect(() => {
if (shouldBlockNavigation) {
window.onbeforeunload = () => true
} else {
window.onbeforeunload = undefined
}
},[]);
Second way is to use history if using react-router
useEffect(() => {
let unblock = history.block((tx) => {
// Navigation was blocked! Let's show a confirmation dialog
// so the user can decide if they actually want to navigate
// away and discard changes they've made in the current page.
let url = tx.location.pathname;
if (window.confirm(`Are you sure you want leave the page without saving?`)) {
// Unblock the navigation.
unblock();
// Retry the transition.
tx.retry();
}
})
},[]);
useEffect(() => {
const unloadCallback = (event) => {
event.preventDefault();
event.returnValue = "";
return "";
};
window.addEventListener("beforeunload", unloadCallback);
return () => {
window.addEventListener("popstate", confirmation());
window.removeEventListener("beforeunload", unloadCallback);
}
}, []);
I just did it with this code sample (actually I combine two events to show dialogue whenever users leave a page) and it's working fine for me. Thanks to all of you guys ... Especially #DrewReese for the help
I am trying to Differentiate Between Page Refresh, Browser Close and New tab events.
So, I want some handling on page close V/s page refresh/new tab
I came across below workaround using sessionStorage. However the issue with sessionStorage is that it gets reset or is not read even on opening link in new tab. But I want both page refresh/new tab to behave in same way V/s refresh of the page.
if (sessionStorage.getItem('reloaded') != null) {
console.log('page was reloaded');
} else {
console.log('page was not reloaded');
}
sessionStorage.setItem('reloaded', 'yes');
You'll have to use a combination of sessionStorage and localStorage to persist the data and rely on beforeunload event to handle the data removal.
The thing is beforeunload fires on both tab/window close and page refresh so we have to work around that.
localStorage will handle persistence across tabs and windows and sessionStorage will sync the data on page refresh.
const readFromStorage = (storageKey) => {
const localStorageItem = localStorage.getItem(storageKey);
const sessionStorageItem = sessionStorage.getItem(storageKey);
// You can optimize this by doing more checks but you get the idea
const itemValue = localStorageItem ?? sessionStorageItem;
if (localStorageItem !== sessionStorageItem) {
writeToStorage(storageKey, itemValue);
}
return itemValue;
};
const writeToStorage = (storageKey, value) => {
localStorage.setItem(storageKey, value);
sessionStorage.setItem(storageKey, value);
};
Event handler:
window.addEventListener('beforeunload', (e) => {
localStorage.removeItem(STORAGE_KEY);
});
Usage:
const STORAGE_KEY = '<storage_key>';
const item = readFromStorage(STORAGE_KEY);
If item is null - a tab/windows was closed. Otherwise, the data will persist across refreshes and new tabs/windows.
I would like to show the popup only one time with React Hooks.
Access for the first time to example.com/campaign/1234
Show popup
Close or refresh the page.
Access again to example.com/campaign/1234 and don't show popup
Access for the first time to example.com/campaign/0000 (is a different URL)
Show popup
Close or refresh the page
Access again to example.com/campaign/0000 or example.com/campaign/1234 and the popup is not being displayed
Any idea of how to do it? I know that I need to use local storage but how can I trigger the event when the user closes or refreshes the page?
Here is a sandbox.
I also read this thread but it doesn't mention how to do it with Hooks
If you never use the setStickyState callback from the custom hook, the state will just remain at its initial value.
It seems like setStickyState also has a bug in it, where it won't update if the key has changed. Here's an enhanced version that I've called useLocalStorage, which should work more reliably:
export function useLocalStorage(key, initialDefault) {
const [val, setVal] = useState(() => {
const localStorageVal = localStorage.getItem(key);
return localStorageVal !== null
? JSON.parse(localStorageVal)
: initialDefault;
});
useEffect(() => {
if (localStorage.getItem(key) === null) {
setVal(initialDefault);
}
}, [key, initialDefault]);
useEffect(() => {
localStorage.setItem(key, JSON.stringify(val));
}, [val, key]);
return [val, setVal];
}
You can then use it like this:
const [visited, setVisited] = useLocalStorage(pageId, false);
const navigateAway = useCallback(() => {
setVisited(true)
}, [setVisited])
useEffect(() => {
// if user navigates away to a completely different site
// or refreshes the page etc
window.addEventListener("beforeunload", navigateAway);
// if user navigates to another page on the same site
return () => {
navigateAway();
window.removeEventListener("beforeunload", navigateAway);
};
}, [pageId, navigateAway]);
// ...
<dialog open={!visited}>
<p>Welcome to page {pageId}!</p>
<button onClick={() => setVisited(true)}>
Don't show again on this page
</button>
</dialog>
Here's a demo (with TypeScript):
useLocalStorage demo
i have troubles detecting a closing window after the build is done.
const newWindow = window.open(url, '_blank', options);
newWindow.onbeforeunload = () => null;
newWindow.addEventListener('beforeunload', (evt: BeforeUnloadEvent) =>
{
console.log(evt)
}
);
it works great until i do the build, there the beforeunload event does not get triggered. i also tried placing a host listener in the new window's component:
#HostListener('window:beforeunload', [ '$event' ])
beforeUnloadHander(event: BeforeUnloadEvent): void {
debugger;
}
but the same problem here. after the build is done, we don't arrive at the debugger anymore
anybody any idea what i am doing wrong? thanks for your help!
Edit Workaround
const heartBeatNewWindow = setInterval(() => {
if (newWindow.closed) {
this.canvasSettings.displayInNewWindow = false;
clearTimeout(heartBeatNewWindow);
}
}, 1500);
I had to do something similar and my approach was the following:
I created a generic catch from close event windows in the constructor of my service, them call method what handle this event. Inside this method I validate the origin of this event is the correct to execute the logic I needed. Look this example:
Inside the constructor:
if(window.addEventListener){
window.addEventListener("message", this.authService.handleMessage.bind(this), false);
}else{
(<any>window).attachEvent('onmessage', this.authService.handleMessage.bind(this));
}
And my method to handle that event:
handleMessage(event: Event) {
event.preventDefault();
const message = event as MessageEvent;
// Only trust messages from the below origin.
//
if ((message.origin !== environment.BASE_URL)) return;
const result = JSON.parse(message.data);
//Add your logic here
I Hope be helpfull.
The website in the iframe isn't located in the same domain, but both are mine, and I would like to communicate between the iframe and the parent site. Is it possible?
With different domains, it is not possible to call methods or access the iframe's content document directly.
You have to use cross-document messaging.
parent -> iframe
For example in the top window:
myIframe.contentWindow.postMessage('hello', '*');
and in the iframe:
window.onmessage = function(e) {
if (e.data == 'hello') {
alert('It works!');
}
};
iframe -> parent
For example in the top window:
window.onmessage = function(e) {
if (e.data == 'hello') {
alert('It works!');
}
};
and in the iframe:
window.top.postMessage('hello', '*')
In 2018 and modern browsers you can send a custom event from iframe to parent window.
iframe:
var data = { foo: 'bar' }
var event = new CustomEvent('myCustomEvent', { detail: data })
window.parent.document.dispatchEvent(event)
parent:
window.document.addEventListener('myCustomEvent', handleEvent, false)
function handleEvent(e) {
console.log(e.detail) // outputs: {foo: 'bar'}
}
PS: Of course, you can send events in opposite direction same way.
document.querySelector('#iframe_id').contentDocument.dispatchEvent(event)
This library supports HTML5 postMessage and legacy browsers with resize+hash https://github.com/ternarylabs/porthole
Edit: Now in 2014, IE6/7 usage is quite low, IE8 and above all support postMessage so I now suggest to just use that.
https://developer.mozilla.org/en-US/docs/Web/API/Window.postMessage
Use event.source.window.postMessage to send back to sender.
From Iframe
window.top.postMessage('I am Iframe', '*')
window.onmessage = (event) => {
if (event.data === 'GOT_YOU_IFRAME') {
console.log('Parent received successfully.')
}
}
Then from parent say back.
window.onmessage = (event) => {
event.source.window.postMessage('GOT_YOU_IFRAME', '*')
}
Updated:
postMessage should not work on cross domain, so the solution like this:
For example your website is: customer.com and your domain is my.com
You need to do like this
Create a js file (upload to CDN or your server) - my.com
Embed js file above to customer.com
Now from my.com, you can postMessage and above embed script can be received data from you.
the window.top property should be able to give what you need.
E.g.
alert(top.location.href)
See
http://cross-browser.com/talk/inter-frame_comm.html
After spending 2 days trying to get an iFrame posting messages back to the parent, a Vue application in my situation, I came across this excellent reference:
https://dev-bay.com/iframe-and-parent-window-postmessage-communication/
From the iframe to parent:
const parentWindow = window.parent;
class Message {
constructor(type, body) {
this.type = type;
this.body = body;
}
};
function sendMessage (windowObj, payload) {
if(windowObj) {
windowObj.postMessage(payload, "*");
}
};
//Then call appropriately:
sendMessage(parentWindow, new Message("button-click", "Show Stats Overlay"));
In the parent, my Vue application mounted life cycle event, but reference the link for your own requirement:
window.addEventListener("message", (e) => {
var data = e.data;
console.log("RECEIVED message from CHILD TO PARENT", data);
var type = data.type;
var body = data.body;
if(type === "button-click" && body) {
console.log("button-click RECEIVED FROM CHILD")
//Additional functionality ...
} else if (type === "text-msg" && body) {
console.log("TEXT MESSAGE RECEIVED FROM CHILD");
//Additional functionality ...
}
});
Please see reference for examples of communication from Parent to iFrame.
Hope this helps someone else.
You can also use
postMessage(message, '*');