Multiple Layouts Per Window

Using the original one-layout-per-window approach, OpenFin handles most of the tasks to create, display, remove layouts, and update local state when a layout is added or removed. This makes for faster and easier development of your apps and features.

However, when replacing one layout with another, there is a performance cost.
The old layout is destroyed and a new layout is created, which requires significant CPU cycles. For most applications, the cost is not great.
Often, it's unnoticeable.

There are some occasions, however, where the performance cost is intrusive.
For example, switching between two tabs in a tabbed interface, both with complex layouts, can make for a noticeable delay.

The solution is to have multiple layouts in play, and show one layout while hiding all the other layouts.
To switch layouts, hide the current layout and show another.
This makes for very rapid updates. In the case of switching between tabs with complex layouts, the tabs can be switched with almost no delay.

This performance improvement comes with a cost during the development cycle.
With the new multi-layouts approach, the burden is on you to create, display, and remove layouts, and to update state for your application.
OpenFin will call your code at the right time using functions and overrides, but you must write your own code to perform those functions for your applications.

📘

Note

Use of the multi-layouts functionality in this API requires that your company has signed a fee-based, customer agreement with OpenFin that includes the appropriate multi-layouts usage rights. To verify that you have appropriate permission, please contact [email protected].

Implementation

Starting in Runtime version 34, multiple layouts per window, or multi-layouts, is implemented as a set of overrides, new API calls, and new entries in the snapshot section of the manifest.
All of these changes must be implemented to activate the multi-layouts feature.

Snapshots

Snapshots changed for multi-layouts.
The original snapshot structure defined a window.layout key, and this still remains for backward compatibility.

The original snapshot structure looks like this:

{
  "snapshot": {
    "windows": [{
      "name": "my-application",
      "layout": {
        "content": [{
          "type": "stack",
          "content": [{
            "type": "component",
            "componentName": "view",
            "componentState": {
              "url": "http://example.com/"
            }
          }]
        }]
      }
    }]
  }
}

The new snapshot structure adds the new key of snapshot.windows.layoutSnapshot.
The layouts themselves are defined as a key/value pair to avoid name collisions.
The value is the same object as the original snapshot.windows.layout value.

In general, the window snapshot has the same input options as when calling createWindow.

The new snapshot structure looks like this:

{
  "snapshot": {
    "windows": [{
      "name": "manual-test-platform-default",
      "layout" : { ... },  // <-- Key remains for backward compatibility
     
      // When `layoutSnapshot` exists, takes precedence.
      // Parsed as Record<string,CUSTOM> of named layouts to support custom metadata.
      "layoutSnapshot": {
        // "panels": {} // <-- Custom metadata example.
        "layouts": {
          "tab1": { // <-- Custom layout key given by implementer, aka layoutName.
            "content": [{ // <-- Same layout data structure as before, aka OpenFin.LayoutOptions.
              "type": "stack",
              "content": [{
                "type": "component",
                "componentName": "view",
                "componentState": {
                  "url": "http://localhost:3000/"
                }
              }]
            }]
          },
          "tab2": {
            "content": [{
              "type": "stack",
              "content": [{
                "type": "component",
                "componentName": "view",
                "componentState": {
                  "url": "http://localhost:3000/"
                }
              }]
            }]
          }
        }
      }
    }]
  }
}

The new snapshot process stores the entire contents of snapshot.windows.layoutSnapshot.
This allows you to store custom data.
For example, the tab order data for a multi-tabbed interface could be stored in snapshot.windows.layoutSnapshot.tabOrder, and would be stored in the snapshot and retrieved from it because it exists under the snapshot.windows.layoutSnapshot property.

Core multi-layout overrides

These are the essential pieces to using multi-layouts.
These must be implemented to enable multi-layout functionality.

fin.Platform.Layout.create and fin.Platform.Layout.destroy

The parameters to create and destroy are almost identical to the single-layout counterpart, fin.Platform.Layout.init().
The difference is in the CreateLayoutOptions.container member of the options parameter.

In the single-layout paradigm, container holds the container or container ID. The Platform will find the HTMLElement that is the HTML container.

In the multi-layout paradigm, you must pass the HTML container to the function in the container element. Using a container ID is not supported for multi-layouts.

layoutManagerOverride

The layoutManagerOverride takes a base class as an input and returns a class that extends the base class.

// From within the platform window that will own the logic for multi-layouts, we must initialize the layout with fin.Platform.Layout.init().

// Prepare the layoutManagerOverride input key, it expects a function which returns a class definition. The call to fin.Platform.Layout.init() call will create a new instance of your class automatically.
const layoutManagerOverride =
  (BaseLayoutManager) => class CustomLayoutManager extends BaseLayoutManager {
    // Implement overrides.
  };

// Initialize the layout with our CustomLayoutManager override.
fin.Platform.Layout.init({ layoutManagerOverride });

applyLayoutSnapshot

When using multi-layouts, you are responsible for creating the HTML div elements that will be the container for each layout and also creating the layout via fin.Platform.Layout.create() API.
Single layouts handle much of that for you since there is only one div, but in multi-layouts, you must dynamically add (or remove) div elements for when layouts are created or removed as well as calling Layout.create or Layout.destroy appropriately.

During initial window creation, OpenFin checks the layoutManagerOverride, instantiates the class, and then calls the override applyLayoutSnapshot.
This is where you begin creating your div containers and calling Layout.create().

// platform-window.html:
<!DOCTYPE html>
<html>

  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <title>Multi Layout Platform Window</title>
    <meta name="description" content="" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script>
      const setupLayout = () => {
        const snapshotState = [];

        const layoutsDiv = document.getElementById('layouts');

        const layoutManagerOverride = (BaseLayoutManager) =>
          class CustomLayoutManager extends BaseLayoutManager {
            async applyLayoutSnapshot(snapshot) {
              // The snapshot object here matches `layoutSnapshot` from the manifest:
              // {
              //   "layouts": {
              //     "tab1": { ... }
              //   }
              // }
              // So we will be using the snapshot.layouts object from here on out.
              // Note any custom keys supplied as siblings of
              // "layouts" would be present in this snapshot object as well.

              const len = Object.keys(snapshot.layouts).length;
              console.log(`Creating ${len} layouts`);

              // Enumerate snapshot.layouts keys, create div containers and
              // call fin.Platform.Layout.create() for each layout.
              Object.entries(snapshot.layouts).forEach(async ([layoutName, layout], index) => {
                // Step 1: create a new div.
                const container = document.createElement('div');
                // Store the layoutName in the `id` field for easier lookup.
                container.id = layoutName;
                container.style.width = '100%';
                container.style.height = '100%';
                container.style.padding = '2px';
                // Show the first layout, hide all the others.
                container.style.display = index === 0 ? 'display' : 'none';

                // Here we would do other things like show the
                // first layout, hide the others.
                // Maybe if we are displaying a tabbed interface,
                // we would add a tab div container.

                // Add it to our layouts top-level div.
                layoutsDiv.appendChild(container);

                // Step 2: call Layout.create() tying together this layout to the container.
                await fin.Platform.Layout.create({ container, layoutName, layout });
                console.log(`Created layout ${layoutName}`);

                // Step 3: save the entire state for later use.
                snapshotState.push({ layoutName, layout, container });
              });
            }
          };
      };

      // Add the DOMContentLoaded event listener to call our setupLayout function
      window.addEventListener('DOMContentLoaded', setupLayout);
    </script>
  </head>

  <body>
    <h2>Multi Layout Example</h2>
    <div id="layouts"></div>
  </body>

</html>

When the applyLayoutSnapshot override is called, you must create one HTML div element per layout.
To do this, enumerate the snapshot keys, and then call fin.Platform.Layout.create once per layout with each of the inputs: container, layoutName and layout.

Destroying layouts is covered in the How to switch between layouts section.

How to create the windows, views, and layouts

Once you define your snapshot JSON with the layoutSnapshot key with individual layouts, we are ready to launch an app with Multi-Layouts.

Putting it all together, the 4 interactions must take place:

  1. Have a manifest json, or window options, with a layoutSnapshot key defining multiple layout options.
  2. fin.Platform.Init() from the provider window.
  3. fin.Platform.Layout.init({ layoutManagerOverride }) from the platform window.
  4. fin.Platform.Layout.create() once per layout in the snapshot, from within the applyLayoutSnapshot override.
// From the Provider Window: Initialize the Platform.
fin.Platform.init();

// From the Platform Window: Initialize the layout with your custom layout manager override, and within applyLayoutSnapshot Initialize each layout individually.

const layoutManagerOverride =
  (BaseLayoutManager) => class CustomLayoutManager extends BaseLayoutManager {
    async applyLayoutSnapshot(snapshot) {
    // Enumerate snapshot.layout keys, create div containers and
    // call fin.Platform.Layout.create() for each layout.
    // See example for applyLayoutSnapshot above
  }
}

fin.Platform.Layout.init({ layoutManagerOverride });

How to handle layout clean up

In single layout mode, you are not responsible for creating or destroying layouts.
The Platform takes care of that responsibility.

In multi-layouts, the Platform no longer calls fin.Platform.Layout.create or fin.Platform.Layout.destroy for you.

In a tabbed interface, the user clicking the "x" to close a tab would be an example where a layout would need to be destroyed and cleaned up.

When you destroy the layout that contains your tab, that is when you would call fin.Platform.Layout.destroy as well as remove the corresponding div element.

// Window javascript. 
const snapshotState = [];

const layoutsDiv = document.getElementById("layouts");

const layoutManagerOverride =
  (BaseLayoutManager) => class CustomLayoutManager extends BaseLayoutManager {

    // ... other overrides.

    async removeLayout(layoutIdentity) {
      // Step 1: destroy the layout.
      await fin.Platform.Layout.destroy(layoutIdentity);

      // Step 2: Cleanup our UI.
      const layoutToRemove = snapshotState.find(s => s.layoutName === layoutIdentity.layoutName;
      layoutsDiv.removeChild(layoutToRemove.container);

      // Step 3: Cleanup our local state.
      snapshotState = snapshotState.filter(s => s.layoutName !== layoutToRemove.layoutName); 
    }
  };

// Window html.
<div id="layouts">
</div>

Additional multi-layout APIs

While the main APIs and overrides are covered above, there are a few more APIs and overrides that are helpful to know when using multi-layouts per window.

showLayout

The showLayout override is called when a hidden layout receives a focus event or a notification event.

This informs your code that now is the time to show the layout.
You can set {display: block;} on the active layout, and set {display: none;} on all inactive layouts, or create a new layout.

removeLayout

The removeLayout override is called when it's time to destroy the layout and update state.
This can occur when the final view for a window has been closed and this layout is ready to be removed.

After running cleanup logic, you should call destroy(layoutIdentity) to remove the layout.

resolveLayoutIdentity

Prior to the development of multi-layouts, layout functions assumed there would be only one layout per window.
Now there can be several layouts for a single window.

Internally, the Platform makes the following checks on the layouts for a window to determine which layout to use:

  1. If the setting of the layout is something other than {display: none;}, use that layout.
  2. If the inner height is within the bounds of the window, use that layout.

This works for most conditions.
There are some conditions where those criteria aren't enough.
You might set {display: none;} on all of your layouts so that they always show a "Do you want to save changes before exiting" dialog box; in that case, those criteria won't identify a specific layout to use.

When the default behavior doesn't work, override the resolveLayoutIdentity override and then OpenFin will call it for every API call.
In the "save changes before exiting" case, because the default resolution fails, your implementation would return the last active LayoutIdentity.
This ensures functions like fin.Platform.getCurrentSync().getSnapshot() will still work when the default layout resolution cannot identify the right layout.

If the resolveLayoutIdentity function can't identify the right layout to use, it will return undefined, so you can include your own fallback logic.

layoutIdentity

The layoutIdentity is a new type that is similar to an Identity, as it contains a uuid field and a name field. layoutIdentity also has a key, layoutName.
The layoutName maps to the layoutSnapshot.layouts heading in the manifest.

When you call the applyLayoutSnapshot function and users start creating layouts, you must pass in the same key from the creation options.
That key must match in the create function and it must match the layoutIdentity key.

The key must also match in the destroy function.

getLayoutSnapshot

When you call Platforms.getSnapshot, the platform returns a snapshot with each window containing the entire window.layoutSnapshot, including any custom data you placed under window.layoutSnapshot.

The getLayoutSnapshot override allows you to do additional processing when the getSnapshot function is called:

  1. The getSnapshot function is called.
  2. The Platform enumerates each window and calls the getLayoutSnapshot function on each window.
  3. The getlayoutSnapshot function enumerates each layout and calls the getSnapshot function on the layout.
  4. At this point, you can intercept the outbound JSON output and modify it.

getLayoutIdentityForView

The getLayoutIdentityForView function identifies the layout in the window and returns the layout applied to that view.

isLayoutVisible

The isLayoutVisible function performs the smart layout resolution to determine if that layout is visible.

size

The size function returns the count of layouts.

Override methods must be implemented

To get multi-layouts to work in OpenFin windows, you must implement specific multi-layouts override methods. This is different behavior from other OpenFin overrides that typically contain default implementations for the overridden methods.

If you do not implement applyLayoutSnapshot, OpenFin will only apply the snapshot of the first layout in layoutSnapshot.layouts. You will have only one layout available in each window.

If you do not implement showLayout, focus events on views in hidden layouts will be ignored. The window will not switch layouts.