DEV Community

Fred Christianson
Fred Christianson

Posted on • Originally published at devrelief.net

Multi-window JavaScript App

If you want to see this in action before continuing there are 2 examples fredchristianson.github.io deployed from my GitHub examples repository. Use the "Simple Child Windows" or "Child Window" links on that page.

I always knew window.open() could do more than I used it for. But I never know how great it is.

Multi-window example app

Multi-window JavaScript App

I'm working on a project where I want to see multiple things at the same time

  • Test controller

  • Results

  • Visual test

  • Manual accept/fail of (3)

With a multi-monitor system, I didn't want to squeeze all that into one browser window. Separation can be nice even with one monitor. So I decided to look more carefully at window.open(). I've used apps where a popup window form is obviously working with the main form so I knew it was possible. Turns out it's extremely easy and powerful.

Terminology

These are the terms I will use in this document

Parent - the window that called window.open(). Most documents call this "opener", which is the property name in the opened window. "Primary window" is also used.

Child - the window created by window.open(). I have seen this called a "secondary window".

Name - the 2nd window.open() parameter is called "target". I will use the term "name" for child windows. In my implementation name is used

  • to create the target (whitespace removed),

  • as the new window title,

  • can be used to manage multiple open windows (e.g. in a Map).

  • used to save/load window position in localStorage (.e.g. child-window-pos-${name})

Quick Summary

Parent and child windows have access to each other's global "window" object

Access includes window.document.body which allows usage of most common DOM properties, manipulation functions, and event listening on any element in the other window's body.

window.postMessage() is an easy way to pass data/requests between windows. The other window needs to listen to "message" events.

Window Creation

window.open() takes up to 3 parameters, and returns a Window object (actually a proxy to a Window object)

const child = window.open(url, target, features)

If popups are blocked in the domain, this will fail. It's not always obvious in the browser UI so it's good to alert("New window failed. Are popups enabled?");

url

The first parameter can be an absolute or relative URL or an empty string. An empty string opens a window with a blank page. If the URL is not an empty string, the browser attempts to load it.

target

This optional parameter can be null or any of the target values allowed in an <a> element: _self, _blank, _parent, and _top. It can also be the name of a window to open (or reuse if a window of that name is already opened). Whitespace is not allowed in the name.

features

The last parameter is also optional or can be a comma-separated string of "name=value" pairs. If no parameters are specified, the new window will probably open as a tab in the same browser window as the parent. MDN has full details of this parameter.

If the feature "popup=true" exists, the browser creates a "minimal popup window". The browser decides what "minimal" is.

One or more of "width", "height", "left", "top" can be specified. The values are in pixels, and the browser selects values for any that are not specified. For example

"width=500,height=600,top=100"
Enter fullscreen mode Exit fullscreen mode

creates a window 500x600 pixels at a location with a top of 100 and a left selected by the browser. The browser will ignore position values if they would place the window off screen. See "Multi-monitors on Chrome" section below for details of using multi-display systems on Chrome.

You can also specify "noopener" if the child should not have access to the parent's DOM and "noreferrer" if the URL request should not include the HTTP referrer header. These are not usually useful in a multi-window application.

Google returns many sites referring to other features like "resizable" and "memubar". They are not in the current MDN docs and I do not see any effect in chrome or firefox.

window.open() return

If window.open() succeeds, it will return a Window object for the window where it was opened. This object is the same class as the global "window" value, but a different instance. If open failed it returns null. The main cause of failure is having popups blocked.

The returned child window has not loaded the URL at this point. It does not have a document (so no body or elements to work with) You need to wait for a "load" event. A simple way to do that is with an async function like this

async function openChild(url,target){
    const child = window.open(url, target);

    return new Promise((resolve, reject) => {
        if (child == null) {
            alert("open failed.  are popups blocked?");
            reject();
        }
        child.addEventListener('load', async (_event) => {
            resolve(child);
        });
}
Enter fullscreen mode Exit fullscreen mode

One problem is that the HTTP request for the URL may fail. There is no good way to tell if the request failed. The "load" event only indicates the HTTP request is complete. But not if it completed with a 200 status, or 404, or 500, or anything else.

If you are loading a page with known content, you can query the child document body after "load" to determine if it has expected text or elements. Or you can include javascript in the child page that uses window.opener.postMessage() to notify the parent that it is loaded and running. (You can also use custom events, or manipulate the parent DOM instead of postMessage if that's more appropriate for your application).

There is an "error" event the parent window can listen to. Unfortunately, that is triggered by errors processing the returned URL, not by HTTP errors. It may be useful, for example, to detect javascript initialization errors in the child windo.

Assuming this is a coordinated multi-window application, it's probably best for the child window to include javascript that notifies the parent window when it is ready (see postMessage below).

If the request can timeout you may want to use setTimeout to give up after some amount of time.

return new Promise((resolve, reject) => {
    let timeout = setTimeout(reject,5000); // 5 seconds
    child.addEventListener('load', async (_event) => {
        clearTimeout(timeout);
        resolve(child);
    });
});
Enter fullscreen mode Exit fullscreen mode

Closing

The parent window can easily close the child window

childWindow.close()

But a multi-window app needs to consider many complications

The user may close the child window unexpectedly while the parent expects to still access its elements.

The user may close the parent window while child windows are left.

The child window may listen for "beforeunload" events and cancel the close

The parent window may listen for "beforeunload" events and cancel the close

None of these are difficult to solve, just things that should be considered when developing a multi-window application.

1) "beforeunload" events are unreliable when it comes to canceling a close. It is not a good idea for either the parent or child to use the event for that purpose.

2) When the application's main window is closed, it is usually a good idea to close all child windows. This closes 2 child windows if they are not null

// close the child windows when the main window is closed.
window.addEventListener('beforeunload', () => {
    controlWindow?.close();
    drawWindow?.close();
});
Enter fullscreen mode Exit fullscreen mode

3) A parent can listen to a child's 'beforeunload' event and create a new child if the child window should not be closed. This isn't as good as canceling the close, but is reliable.

child.addEventListener('beforeunload', recreateChild);
Enter fullscreen mode Exit fullscreen mode

recreateChild() would probably

  • save the child's position

  • create a new child

  • set the new child's position to replace the closing child

  • restore the new child's state (input value, text, etc)

  • listen for child events - including 'beforeunload'

4) A parent can use the child window's "closed" property to recreate a closed child when needed rather than during 'beforeunload'

if (child.closed) {
    recreateChild();
}
Enter fullscreen mode Exit fullscreen mode

I think it is useful to save the window's location in "beforeunload"

const location = {
  left: window.screenX
  top: window.screenY
  width: window.innerWidth
  height: window.innerHeight
};

localStorage.setItem('child-window-location',
                     JSON.stringify(location)
Enter fullscreen mode Exit fullscreen mode

Then use that to generate features when opening the window again

const location = localStorage.getItem('child-window-location');
let features = 'popup=true';
if (location != null) {
   features = `left:${location.left},...`
}
window.open(url,name,features); 
Enter fullscreen mode Exit fullscreen mode

Chrome needs permission for this to work with multiple monitors.

document

A loaded child window has a document just like any other javascript window.

const childDocument = childWindow.document;
const childBody = childDocument.body;

These can be used used to inspect, change, and listen to the child DOM the same as the main (parent) DOM

const mainBody = window.document.body;

mainBody.style.backgroundColor = '#00f';
childBody.style.backgroundColor = '#00f';

mainBody.querySelector('.message').append('new message');
childBody.querySelector('.message').append('new message');

mainBody.querySelector('input.name').value;
childBody.querySelector('input.name').value;

mainBody.addEventListener('mousemove',mainMouseMoved);
childBody.addEventListener('mousemove',childMouseMoved);

Enter fullscreen mode Exit fullscreen mode

The child has similar access to the parent's DOM

const parentBody = window.opener.document.body;

parentBody.style.backgroundColor = '#00f';

parentBody.querySelector('input.name').value;
parentBody.querySelector('.message').append('new message');

parentBody.addEventListener('mousemove',mainMouseMoved);
Enter fullscreen mode Exit fullscreen mode

While it is possible for JavaScript in multiple windows to use multiple DOMs, it will likely cause problems with debugging and maintaining the code. The most maintainable solutions will be either

The parent window handles all DOM management (modifications and events)

The child window handles of its DOM management and the parent does nothing with childWindow.document. In this case, the 2 parts of the application communicate with postMessage or CustomEvents.

Global Classes

This doesn't come up often, but classes such as HTMLElement and HTMLDivElement are properties of Window. And they are not the same in the parent and child window objects.

const parentDiv = window.document.createElement('div')
const childDiv = childWindow.document.createElement('div')

parentDiv instanceof HTMLElement;             // is true
childDiv instanceof HTMLElement;              // is false
childDiv instanceof childWindow.HTMLElement;  // true
Enter fullscreen mode Exit fullscreen mode

In my testing, even though parentDiv and childDiv are not created by the same HTMLElement constructor, they may be inserted into with DOM. It is probably safest for the future to use the correct window's document.createElement to create new elements. Different window objects also have different DOMParsers so use the correct one of those also.

Global Values

Classes are just one type of property a Window object contains. They are just functions and any other function or data can be added to a parent or child window and accessed by the other

parent JavaScript:
    window.callParent = function(...args) {...};
    window.parentData = { a:1,b:2};

child JavaScript:
    window.opener.callParent(1,2,['a','b']);
    const parentData = window.opener.parentData;
    parentData.a = 5;
Enter fullscreen mode Exit fullscreen mode

Similarly, the child can add properties to its window and the parent can call or access them.

This is probably a bad idea in most cases. JavaScript debugger and console logs are not shared between the windows. So it can be more difficult to track down problems if multiple windows are modifying the same data.

Child and parent JavaScript can communicate with postMessage and CustomEvents in a way that is likely to be much more maintainable.

Inter-window Events

The simplest multi-window application runs all JavaScript in a single window. It has access to the DOM in every window and takes care of everything. There are many cases where there are advantages to developing the windows independently (JavaScript, HTML, CSS) and use inter-window communication. The communication may be modifying DOM, calling each other's functions, accessing window properties, or events.

An architecture where windows communicate with each other through events is likely to be

  • Most maintainable

  • Most extensible

  • Most testable

In terms of maintainability, the JavaScript, HTML, and CSS for each window can be changed without concern for the implementation of other windows. The events make an API and as long as the API is followed problems are unlikely.

For extensibility, a window implementation for one application may be more easily added to other applications. The event API must be followed, but the DOM, data, and functions are independent.

Testing a child window implementation is fairly straightforward with an event interface. And the test harness will not need to change as the child implementation changes.

There are 2 (very similar) ways for windows to communicate with events. The first is custom events.

Custom Events

JavaScript CustomEvents are listened to like any other event. There are a few things to manage

The sender and receiver need to use the same name ("myevent" below)

The second parameter of the CustomEvent constructor must be an object with a "detail" property to send data to the listener

dispatchEvent is used to send the event to the listener (dispatchEvent can be used on elements as well as on the Window object. event.target has the window or element as with any other event)

Parent: 
    window.addEventListener('myevent',(event)=>{
        const data = event.detail;
        // data is the detail object send by the Child
        // {"name":"fred","age":57} in this example
    });

Child:
    const data = {
        "name": "fred",
        "age": 57
    };
    const event = new CustomEvent("myevent",{detail: data});
    window.opener.dispatchEvent(event);
Enter fullscreen mode Exit fullscreen mode

It is fairly simple and works the same if the child listens to its window and the parent dispatches the event. This works great for one-way events (i.e. notifications). It needs more if the child needs a response. In my opinion, the easiest is to use a Promise

Parent:
    window.addEventListener('myevent',(event)=>{
        const data = event.detail;
        if (isValid(data)) {
            const response = {"message":"got it", value:42};
            data.resolve(response);
        } else {
            const error = new Error("something went wrong");
            data.reject(error);
        }
    });

Child:
    let resolveEvent = null;
    let rejectEvent = null;
    let promise = new Promise((resolve,reject)=>{
        resolveEvent = resolve;
        rejectEvent = reject;
    })

    const data = {
        "name": "fred",
        "age": 57,
        resolve: resolveEvent,
        reject: rejectEvent
    };
    try {
        const event = new CustomEvent("myevent",{detail: data});
        window.opener.dispatchEvent(event);
        const response = await promise;
        // got response
    } catch(error) {
        // something failed
    }
Enter fullscreen mode Exit fullscreen mode

There is much more code for a single event with a response than a one-way communication. But most of it is common and easily encapsulated in a module and would not need to be repeated if there are many events passed between windows.

postMessage

In a multi-window application on the same domain, postMessage has no advantage over custom events. postMessage has checks for the "same-origin policy" so it would be preferred if that is a concern. You can read more about it on mdn web docs.

Multi-monitor Windows on Chrome

Firefox, and most browsers support features with left and top on any monitor. Chrome requires user permission or it always moves the new window to the same monitor.

It has a global function to ask the user for permission: getScreenDetails.

I created a function to ask the user for permission and wait for a response if Chrome's function exists:

async _checkMultipleScreenPermission() {
    if (!('getScreenDetails' in window)) {
        return true;
    }
    const promise = new Promise((resolve, reject) => {

        window.getScreenDetails()
            .then(() => {
                //user allowed multiscreen
                resolve(true);
            })
            .catch((ex) => {
                // user did not allow multiscreen.  not a problem.
                resolve(false);
            });
    });
    return await promise;
}
Enter fullscreen mode Exit fullscreen mode

Before using window.open with features that specify a left or top, call this function

await checkMultipleScreenPermission();
window.open(url,target,"left:2000,top:50");
Enter fullscreen mode Exit fullscreen mode

If getScreenDetails doesn't exist, the await will return immediately and the window will be opened. It will also return immediately if the user has already granted or denied permission, so will not prompt the user every time.

Examples and Discussion

There are 2 examples of child windows on GitHub if you want to see any of this work

If you want to just see them in action they are deployed to fredchristianson.github.io

The best place to contact me if you have corrections or questions is Twitter.

Top comments (0)