User Tools

Site Tools


en:outskirts:mages-peaks:detach-windows

This is an old revision of the document!


Goals

As demonstrated in the laelith-adventure project, for the Editor, the goal is to enable a (Svelte) component sitting in a page to be detached into a popup window, that can be moved around independently, and notably make better use of multi-screen setups.
However, communicating with a local Svelte component and communicating with a component in a popup window do not involve the same techniques.
We introduce here the intercoms utility, that helps managing the communication with detachable components. The main design goal here is to have the same way to communicate with the component, be it local or in a detached window.

Attaching/Detaching a View

Properties of the detacheable component

A detachable Svelte component must have a detached svelte property, to become aware of its attached/detached status. It may have callbacks such as the onClose and onDetach shown below if the container in which it is embedded (in non-detached mode) needs to be made aware of its status change to adapt the layout.

  export let detached:boolean;
  
  // onClose and onDetach only active for the non-detached "anchor" instance.
  export let onClose: () => void = () => {};
  export let onDetach: (detached: boolean) => void = (_detached) => {};

A container page for the detached version will pass detached=true, the embedded version will receive detached=false.

Attach/Detach management code within the Component

Within the Svelte component, the following example code manages the operation:

  const intercom = detached
    ? intercoms.newDetachedIntercom("MyViewId")
    : intercoms.newIntercom("MyViewId");
  let fullyDetached = false;
  
  // The Embedded Component requests its container to destroy it.
  const closeButton = () => {
    onClose();
  };
  
  // Closing the detached popup window reattaches the component
  const reattach = () => {
    window.close();
  };

  const detach = () => {
    const ghostId = intercoms.detachMe("MyViewId");
    const detachedWindow = window.open(
      "/?p=detached-page-url",
      "Target",
      "popup,left:0, bottom= 300px, width=500px, height=600px",
    );
    if (detachedWindow) {
      intercoms.declareChild(ghostId, "MyViewId", detachedWindow).then(() => {
        fullyDetached = true;
        onDetach(true);    // Notify the container if needed
      });
    } else {
      intercoms.undetachMe("MyViewId");  // cancel detach
    }
  };
  
  onMount(() => {
    intercom.on(Intercom.RX_MESSAGE,
      (msg: IntercomMessage) => {
        switch (msg.msgType) {
          case "wake-up":
            fullyDetached = false;
            onDetach(false);
            // If needed, re-init code for the reattached component
            break;
        }
      },
      "myview",
    );
    intercom.whenReady().then(() => {
      // If needed, init code for the component
    });
  });
  onDestroy(() => {
    intercoms.destroyIntercom("MyViewId");
  });
  
  </script>

{#if !fullyDetached}
  // Here the Svelte code of the component
{/if}

The const intercom = detached ? newDetachedIntercom… code declares this Component to the Intercoms system.

The const ghostId = intercoms.detachMe(“MyViewId”); line prepares the detachment, that wil “ghost” the embedded component.

Then the const detachedWindow = window.open(…) really creates the detached popup window. The page opened at this URL should embedd our component, with the detached=true property.

The intercoms.declareChild(ghostId, “MyViewId”, detachedWindow).then(() ⇒ { gives the reference of the (child) detached window to the Intercoms system, that will handshake with it. When the detached window is known to be present and operational, the returned Promise returns, allowing to complete the detachment operation.

When the view is mounted, it listens to Intercom messages (with intercom.on(Intercom.RX_MESSAGE) and must handle the wake-up signal – this signal notifies the embedded “ghosted” view that the detached window has closed.

Using the Intercoms to communicate

Readiness signals

Attaching / Detaching views involves a number of non-instantaneous, asynchronous operations.
The Intercoms system notifies the application code when the transition is complete, to make sure that the target view can be reached and is ready to process messages.

This is signalled using the whenReady callback. When in the embedded view, the returned Promise will immediately resolved.
When in the detached view, it will resolve when the handshake with the parent window is complete, and, most importantly, when the internal communication system is ready to deliver messages sent to components living in the parent window.

Typically, this is where you will have the code to fetch the data of the component, using Intercom messages (see below).

intercom.whenReady().then(() => {
      intercom.sendMessage("DataProviderId", "request-my-data");
    });

Communication between components

The Intercoms system provides communication between intercom instances (“views”). Each is identified by its viewId, that is the parameter given in newIntercom.

The key property of this communication system is that a message addressed to a viewId will reach its destination component regardless of its attached/detached status. This enables the application code to communicate with the detacheable view using the same API call, without having to know if it is detached or not.

Sending a message to the view identified by target-view-id is done using:

  intercom.sendMessage("target-view-id", "message-type", payload);

The message-type is fully managed by the application, the Intercoms system just reserves a few values for its own operation (wake-up, me-parent, me-child and child-up).

For message reception, attach a Listener to the intercom:

  intercom.on(Intercom.RX_MESSAGE,
      (msg: IntercomMessage) => {
        switch (msg.msgType) {
          case 'adv-current-adventure: ... // process received Adventure
          [...]
        }
      },"myview",
    );
    
    intercom.whenReady().then(() => {
      intercom.sendMessage('adv-store', 'adv-request-current-adventure');
    });

The code above illustrates a common pattern: the View registers a message listener on the intercom and requests the data it needs from the adv-store (when the intercom is Ready). The response data is received by the Message Listener and can be used by the Component.

The Network Prefix

All the ViewIds should include a dash (-), with the part before the dash termed as the networkPrefix.
The special value prefix-all broadcasts a message to all Views with the same Prefix.

Intercom messages can only be exchanged between Views of the same networkPrefix, it is thus required to use as IDs of the views that need to communicate together only strings sharing the same prefix.

  • This disposition allows to define several independent “communication networks” using Intercoms.
  • It allows to scope the Broadcasts to the Views of the same network.
  • Other unrelated applications communicate using also postMessage between windows and Intercoms will not forward or deliver these messages.

Ensuring consistency between definitions

To allow ViewIds, and Definitions .d.ts file

en/outskirts/mages-peaks/detach-windows.1712700479.txt.gz · Last modified: 2024/10/28 08:00 (external edit)