CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-single-spa

The router for easy microfrontends that enables multiple frameworks to coexist on the same page.

Pending
Overview
Eval results
Files

parcels-system.mddocs/

Parcels System

Manual component management system for mounting components outside of the standard application lifecycle. Parcels provide a way to manually control when and where components are mounted.

Capabilities

Mount Root Parcel

Mounts a parcel at the root level, not within a specific application. This is useful for shared components like modals, tooltips, or notifications that need to be managed independently.

/**
 * Mount a parcel at the root level
 * @param parcelConfig - Parcel configuration object or loading function
 * @param parcelProps - Properties including DOM element and custom props
 * @returns Parcel instance with lifecycle methods
 */
function mountRootParcel(
  parcelConfig: ParcelConfig,
  parcelProps: ParcelProps & CustomProps
): Parcel;

type ParcelConfig = ParcelConfigObject | (() => Promise<ParcelConfigObject>);

interface ParcelConfigObject {
  name?: string;
  bootstrap: LifeCycleFn | Array<LifeCycleFn>;
  mount: LifeCycleFn | Array<LifeCycleFn>;
  unmount: LifeCycleFn | Array<LifeCycleFn>;
  update?: LifeCycleFn | Array<LifeCycleFn>;
}

interface ParcelProps {
  domElement: HTMLElement;
}

interface Parcel {
  mount(): Promise<null>;
  unmount(): Promise<null>;
  update?(customProps: CustomProps): Promise<any>;
  getStatus(): ParcelStatus;
  loadPromise: Promise<null>;
  bootstrapPromise: Promise<null>;
  mountPromise: Promise<null>;
  unmountPromise: Promise<null>;
}

type ParcelStatus = 
  | "NOT_LOADED"
  | "LOADING_SOURCE_CODE" 
  | "NOT_BOOTSTRAPPED"
  | "BOOTSTRAPPING"
  | "NOT_MOUNTED"
  | "MOUNTING" 
  | "MOUNTED"
  | "UNMOUNTING"
  | "UNLOADING"
  | "SKIP_BECAUSE_BROKEN"
  | "LOAD_ERROR";

Usage Examples:

import { mountRootParcel } from "single-spa";

// Mount a simple parcel
const modalContainer = document.getElementById("modal-container");
const modalParcel = mountRootParcel(
  () => import("./modal/modal.parcel.js"),
  { 
    domElement: modalContainer,
    title: "User Settings",
    onClose: () => modalParcel.unmount()
  }
);

// Mount parcel with configuration object
const tooltipParcel = mountRootParcel(
  {
    name: "tooltip",
    bootstrap: () => Promise.resolve(),
    mount: (props) => {
      props.domElement.innerHTML = `<div class="tooltip">${props.text}</div>`;
      return Promise.resolve();
    },
    unmount: (props) => {
      props.domElement.innerHTML = "";
      return Promise.resolve();
    }
  },
  {
    domElement: document.getElementById("tooltip"),
    text: "This is a tooltip"
  }
);

// Mount parcel with lifecycle management
async function createNotificationParcel(message, type = "info") {
  const container = document.createElement("div");
  document.body.appendChild(container);
  
  const parcel = mountRootParcel(
    () => import("./notification/notification.parcel.js"),
    {
      domElement: container,
      message,
      type,
      onDismiss: async () => {
        await parcel.unmount();
        document.body.removeChild(container);
      }
    }
  );
  
  // Auto-dismiss after 5 seconds
  setTimeout(async () => {
    if (parcel.getStatus() === "MOUNTED") {
      await parcel.unmount();
      document.body.removeChild(container);
    }
  }, 5000);
  
  return parcel;
}

Parcel Lifecycle Management

Parcels follow a similar lifecycle to applications but are manually controlled:

import { mountRootParcel } from "single-spa";

async function manageParcels() {
  const container = document.getElementById("dynamic-content");
  
  // Create parcel
  const parcel = mountRootParcel(
    () => import("./widget/widget.parcel.js"),
    { 
      domElement: container,
      initialData: { userId: 123 }
    }
  );
  
  // Wait for parcel to be mounted
  await parcel.mountPromise;
  console.log("Parcel mounted successfully");
  
  // Update parcel with new data
  if (parcel.update) {
    await parcel.update({ userId: 456, theme: "dark" });
    console.log("Parcel updated");
  }
  
  // Check parcel status
  const status = parcel.getStatus();
  if (status === "MOUNTED") {
    console.log("Parcel is ready");
  }
  
  // Unmount when done
  await parcel.unmount();
  console.log("Parcel unmounted");
}

Application-Scoped Parcels

Applications can also mount parcels using the mountParcel function provided in their props:

// Within an application's mount lifecycle
export async function mount(props) {
  const { domElement, mountParcel } = props;
  
  // Mount a parcel within this application
  const widgetContainer = domElement.querySelector("#widget-container");
  const widgetParcel = mountParcel(
    () => import("./widget/widget.parcel.js"),
    {
      domElement: widgetContainer,
      parentApp: props.name
    }
  );
  
  // Store parcel reference for cleanup
  domElement.widgetParcel = widgetParcel;
  
  return Promise.resolve();
}

export async function unmount(props) {
  const { domElement } = props;
  
  // Clean up parcel
  if (domElement.widgetParcel) {
    await domElement.widgetParcel.unmount();
    delete domElement.widgetParcel;
  }
  
  return Promise.resolve();
}

Advanced Parcel Patterns

Parcel Factory

Create a factory for commonly used parcels:

import { mountRootParcel } from "single-spa";

class ParcelFactory {
  static async createModal(config) {
    const container = document.createElement("div");
    container.className = "modal-backdrop";
    document.body.appendChild(container);
    
    const parcel = mountRootParcel(
      () => import("./modal/modal.parcel.js"),
      {
        domElement: container,
        ...config,
        onClose: async () => {
          await parcel.unmount();
          document.body.removeChild(container);
          if (config.onClose) config.onClose();
        }
      }
    );
    
    return parcel;
  }
  
  static async createToast(message, type = "info", duration = 3000) {
    const container = document.createElement("div");
    container.className = "toast-container";
    document.body.appendChild(container);
    
    const parcel = mountRootParcel(
      () => import("./toast/toast.parcel.js"),
      {
        domElement: container,
        message,
        type
      }
    );
    
    // Auto-remove toast
    setTimeout(async () => {
      await parcel.unmount();
      document.body.removeChild(container);
    }, duration);
    
    return parcel;
  }
}

// Usage
const confirmModal = await ParcelFactory.createModal({
  title: "Confirm Action",
  message: "Are you sure you want to delete this item?",
  onConfirm: () => console.log("Confirmed"),
  onClose: () => console.log("Modal closed")
});

const successToast = await ParcelFactory.createToast(
  "Item saved successfully!", 
  "success", 
  2000
);

Parcel Registry

Manage multiple parcels with a registry pattern:

import { mountRootParcel } from "single-spa";

class ParcelRegistry {
  constructor() {
    this.parcels = new Map();
  }
  
  async register(id, parcelConfig, parcelProps) {
    if (this.parcels.has(id)) {
      throw new Error(`Parcel with id "${id}" already exists`);
    }
    
    const parcel = mountRootParcel(parcelConfig, parcelProps);
    this.parcels.set(id, parcel);
    
    return parcel;
  }
  
  async unregister(id) {
    const parcel = this.parcels.get(id);
    if (!parcel) {
      console.warn(`Parcel with id "${id}" not found`);
      return;
    }
    
    await parcel.unmount();
    this.parcels.delete(id);
  }
  
  get(id) {
    return this.parcels.get(id);
  }
  
  async unregisterAll() {
    const promises = Array.from(this.parcels.values()).map(parcel => 
      parcel.unmount().catch(console.error)
    );
    await Promise.all(promises);
    this.parcels.clear();
  }
  
  getStatus(id) {
    const parcel = this.parcels.get(id);
    return parcel ? parcel.getStatus() : null;
  }
  
  getAllStatuses() {
    const statuses = {};
    this.parcels.forEach((parcel, id) => {
      statuses[id] = parcel.getStatus();
    });
    return statuses;
  }
}

// Usage
const parcelRegistry = new ParcelRegistry();

// Register parcels
await parcelRegistry.register(
  "notification",
  () => import("./notification/notification.parcel.js"),
  { domElement: document.getElementById("notifications") }
);

await parcelRegistry.register(
  "sidebar",
  () => import("./sidebar/sidebar.parcel.js"),
  { domElement: document.getElementById("sidebar") }
);

// Later, clean up specific parcel
await parcelRegistry.unregister("notification");

// Or clean up all parcels
await parcelRegistry.unregisterAll();

Error Handling

import { mountRootParcel, addErrorHandler } from "single-spa";

// Handle parcel errors
addErrorHandler((error) => {
  if (error.appOrParcelName && error.appOrParcelName.includes("parcel")) {
    console.error("Parcel error:", error);
    // Handle parcel-specific errors
  }
});

// Safe parcel mounting with error handling
async function safeMountParcel(config, props) {
  try {
    const parcel = mountRootParcel(config, props);
    await parcel.mountPromise;
    return parcel;
  } catch (error) {
    console.error("Failed to mount parcel:", error);
    // Clean up DOM element if needed
    if (props.domElement && props.domElement.parentNode) {
      props.domElement.parentNode.removeChild(props.domElement);
    }
    throw error;
  }
}

Types

interface CustomProps {
  [key: string]: any;
  [key: number]: any;
}

type LifeCycleFn = (props: ParcelProps & CustomProps) => Promise<any>;

Install with Tessl CLI

npx tessl i tessl/npm-single-spa

docs

application-management.md

application-status-lifecycle.md

configuration-timeouts.md

error-handling.md

index.md

navigation-routing.md

parcels-system.md

tile.json