Could I establish a deeper level of communication between pop-out windows and their parent pages? The standard ways of communicating additional information to users are either modal dialogues–dialogues residing on the same page, or pop-up windows–pages rendered in a new browser window. Most implementations of pop-up dialogues involve having a separate page with logic to render the whole body alongside business logic.
Recently, I determined that there is no need for a separate page. In a nutshell, the strategy I developed involves housing the logic of the main page and pop-out window in the same browser window. In this blog entry, I reveal how I did it.
The nuts and bolts
In part to avoid the pitfalls of using a rigid bundling and hosting framework (I’m talking about you, webpack), I used the below stack:
http-server for a lightweight serving of my static files
Rollup.js for "bundling"
Babel for transpiling (as I typically write in ES6)
React as a rendering engine
I initialized an npm project, installed and configured the tools, and created two react components:
A button to be rendered on the main page with all of the logic
A component with a form, to be displayed in the pop-out
I then used tutorials on React to:
Define a component link
Determine the state link
Create a components link for communicating among components
Here's the main component of the starting source:
import React from "react";
import ReactDOM from "react-dom";
class Main extends React.Component {
constructor(props) {
super(props);
this.modalWindow;
this.handleClick = this.handleClick.bind(this);
}
handleClick(e) {
const modalTitle = "Awesome Modal";
this.modalWindow = window.open("about:blank", modalTitle,
"width=420,height=250,menubar=no,resizable=no,scrollbars=no,status=no,location=no");
} render() {
return (
<div>
<button onClick={this.handleClick}>Init Modal</button>
</div>);
}
}
ReactDOM.render(<Main/>, document.body);
Note that I added a div element with a button, which opens a window with about:blank (it’s basically empty). This window returns an opened handle. To manipulate the window at will, I stored an internal reference to it. To determine if I could render something meaningful in that window from the parent, I added a function to render the PopOut form controller and updated the button click handler to use that rendering:
import PopOut from "./components/form.js";
//...
renderPopOut() {
if (this.modalWindow) {
let root = this.modalWindow.document.body;
ReactDOM.render(<PopOut name={this.state.name}
onFormSubmit={this.handleForm}/>, root);
}
}
handleForm(state) {
this.setState({ ...state });
}
//...
handleClick(e) {
const modalTitle = "Awesome Modal";
this.modalWindow = window.open("about:blank", modalTitle, width=420,height=250,menubar=no,resizable=no,scrollbars=no,status=no,location=no");
this.renderPopOut();
}
Here is the form component:
import React from "react";
import ReactDOM from "react-dom";
export default class PopOut extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.setName = this.setName.bind(this);
this.setEmail = this.setEmail.bind(this);
this.setBirthDay = this.setBirthDay.bind(this);
this.state = {
name: props.name || "",
email: props.email || "",
birthDate: props.birthDate || ""
};
}
capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
handleSubmit(e) {
e.preventDefault();
this.props.onFormSubmit(this.state);
}
setName(e) {
this.setState({ name: e.target.value });
}
setEmail(e) {
this.setState({ email: e.target.value });
}
setBirthDay(e) {
this.setState({ birthDate: e.target.value });
}
render() {
return (
<div>
<h1>Hello, {this.capitalizeFirstLetter(this.props.name)}</h1>
<form>
<fieldset>
<legend>Input some data:</legend>
Name: <input type="text" onChange={this.setName}/><br/>
Email: <input type="text" onChange={this.setEmail}/><br/>
Date of birth: <input type="text" onChange={this.setBirthDay}/>
</fieldset>
<button onClick={this.handleSubmit}>Save</button>
</form>
</div>
);
}
}
This component can handle input changes and render a simple form. It also exposes an event on FormSubmit to be used by the parent, as would be the case if both components resided on the same page. Here's what it looks like:
Note that, miraculously, the address displays! Further, with no additional requests to the server, all of the rendering and business logic has already been downloaded and initialized and resides in the parent window. Still, the name of the window is not set (you can see about:blank in the address). After all, for security reasons, the address bar cannot be hidden in most browsers. So, I decided to add a dummyWindow.html to lend the feel of a real page and avoid the caveat of same origin policy breach. The page contents are:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<!-- For all those "copy and paste the address" people -->
<p>Browse along. Nothing to see here!</p>
</body>
</html>
Realizing that this would result in a post-back and some downloads, I updated the handleClick to begin the rendering process after requests were successful:
handleClick(e) {
const modalTitle = "Awesome Modal";
this.modalWindow = window.open("/dummyWindow.html", modalTitle, "width=420,height=250,menubar=no,resizable=no,scrollbars=no,status=no,location=no");
this.modalWindow.addEventListener('load', ()=>{
this.modalWindow.document.title = modalTitle;
this.renderPopOut();
}, false);
}
Here's the result:
The final flourishes
Next, I initialized a counter in the parent (along with an interval function) to check if the allocated memory would still remain after the parent closed. The result: after closing the parent, an orphan child does not get re-rendered, as there are no more triggers for it in the form.
To establish back-and-forth communication, wire the components so that logic for updating counters would reside in the child component. Verify this by entering the name into the form and clicking Save. You will see that the actual functionality of the PopOut does not end when it is orphaned. This means that if I bind the name state property to the display, it will get updated even if the parent is closed.
With this in mind, if creating a progress modal window for a long-running process, the initialization of that process plus hooking to the update should occur in the boundary of the child element.
Another logical conclusion: Since the parent is responsible for everything that happens in the PopOut, all console.log messages and debugging happen in the parent.
Improved functionality in less time
Altering the placement of logic truly delivers powerful results. In this case, my strategy simplifies Document Object Model (DOM) construction, manipulation, and access, reduces pop-up windows load times to none, unites pop-up window and main page. No more writing boilerplate javascript and creating separate pages for these purposes!