User Tools

Site Tools


en:outskirts:mages-peaks:detach-windows

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
en:outskirts:mages-peaks:detach-windows [2024/04/09 15:56] – created Philippe Men:outskirts:mages-peaks:detach-windows [2024/10/28 08:00] (current) – external edit 127.0.0.1
Line 1: Line 1:
 +====== Detachable Windows and Intercom ======
 +
 +
 ===== Goals ===== ===== 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.\\  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.\\ 
Line 4: Line 7:
 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. 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.
  
-===== Using the Intercoms =====+===== 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. 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.
  
Line 17: Line 21:
  
 A container page for the detached version will pass ''detached=true'', the embedded version will receive ''detached=false''.\\  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:  
 +
 +<code>
 +  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}
 +</code>
 +
 +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.
 +
 +The "container" that mounts the embedded (attached) Svelte component should not destroy it when detached -- only with this implementation, as the code listens for the wake-up signal. Other setups are absolutely possible to manage the re-attachment of a detached component.
 +===== 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).
 +
 +<code>
 +intercom.whenReady().then(() => {
 +      intercom.sendMessage("DataProviderId", "request-my-data");
 +    });
 +</code>
 +
 +=== 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:
 +<code>
 +  intercom.sendMessage("target-view-id", "message-type", payload);
 +</code>
 +
 +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'':
 +<code>
 +  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');
 +    });
 +</code>
 +
 +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 prevent misspelling bugs and provide a central place for the common definitions of communicating entities, it is suggested to create a specific ''.d.ts'' definition File with:
 +
 +  * All the ViewIDs (same network prefix) in an //Enum//.
 +  * The ''msgType'' Message Types, possibly segregated between //Requests// and //Indications//
 +  * The type definition of the Payloads.
 +
 +Create one such file for each "network" of communicating entities. As an example, for the Adventure Editor feature, the ''AdvEditIntercom.d.ts'' file plays this role:
 +
 +<code>
 +/**
 + * Participants to the Adventure Editor group. Used as Source and Target of messages, with the NetworkPrefix.
 + */
 +export enum AdvEditViews {
 +  /** For broadcasting messages, to all except the emitter. */
 +  All = "adv-all",
 +
 +  /** The Adventure Editor Store. */
 +  EditStore = "adv-edit-store",
 +
 +  /** The Main Editor View */
 +  MainEditor = "adv-maineditor",
 +[...]
 +}
 +
 +/** Messages of these msgTypes are requests adressed to a View. */
 +export enum AdvEditRequests {
 +  /** 
 +   * The message requests the Adventure currently being edited.
 +   * The Owner of the Adventure Edit Store should respond with a CurrentAdventure indication to the requester.
 +   * Target: AdvEditViews.EditStore
 +   * No Payload.
 +   */
 +  RequestCurrentAdventure = "adv-req-adventure",
 +[...]
 +}
 +
 +/** Messages of these msgTypes are event-like messages that can interest different Views. */
 +export enum AdvEditIndications {
 +
 +  /** The message payload contains the Adventure newly selected. */
 +  CurrentAdventure = "adv-ind-adventure",
 +[...]
 +}
 +
 +/**
 + * Payload of AdventureAssetCount and UpdateAssetCount
 + */
 +export interface AdventureAssetCountUpdate {
 +  assetId: string,
 +  value: number
 +}
 +
 +
 +
 +
 +
 +
 +</code>
  
en/outskirts/mages-peaks/detach-windows.1712678169.txt.gz · Last modified: 2024/10/28 08:00 (external edit)