Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions docs/SSR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# Server-Side Rendering (SSR) Compatibility Guide

This guide explains how to safely load ELIXIR Cloud Components in projects that use SSR frameworks (e.g. Next.js, Remix).

## Quick Start

All components are **safe to import during SSR** — importing them on the server will not crash. However, Web Components are a **client-side technology**: rendering and hydration occur in the browser via the Custom Elements API.

### Next.js (App Router)

Use dynamic imports with `ssr: false` to load components only on the client:

```tsx
// app/components/MyComponent.tsx
"use client";

import dynamic from "next/dynamic";

// Dynamic import prevents SSR from trying to define custom elements
const EccComponents = dynamic(
() => import("@elixir-cloud/design").then((mod) => mod),
{ ssr: false }
);

export default function MyComponent() {
return (
<div>
<ecc-utils-design-button>Click me</ecc-utils-design-button>
</div>
);
}
```

Alternatively, mark the entire page as a Client Component:

```tsx
// app/page.tsx
"use client";

import "@elixir-cloud/design";

export default function Page() {
return <ecc-utils-design-button>Click me</ecc-utils-design-button>;
}
```

### Remix

Use `ClientOnly` wrappers or lazy loading:

```tsx
import { ClientOnly } from "remix-utils/client-only";

export default function Page() {
return (
<ClientOnly fallback={<p>Loading...</p>}>
{() => {
import("@elixir-cloud/design");
return <ecc-utils-design-button>Click me</ecc-utils-design-button>;
}}
</ClientOnly>
);
}
```

---

## How SSR Safety Works

### Environment Guards

Every `customElements.define()` call is wrapped in an environment check:

```ts
if (
typeof window !== "undefined" &&
window.customElements &&
!window.customElements.get("tag-name")
) {
window.customElements.define("tag-name", ComponentClass);
}
```

This prevents:
- **Server crashes** — `customElements` doesn't exist in Node.js
- **Duplicate registrations** — safe for HMR and multiple imports

### Deterministic IDs

Components that need unique IDs (e.g., `<ecc-utils-design-select>`) use a monotonic counter instead of `Math.random()`. This avoids mismatches if the same module is loaded on both server and client.

### Browser-Only Lifecycle

All DOM access (`document`, `MutationObserver`, `window.setInterval`) is deferred to client-only Lit lifecycle hooks:

| API | Used In | Lifecycle Hook |
|-----|---------|----------------|
| `document.addEventListener` | Select, MultiSelect | `connectedCallback()` |
| `MutationObserver` | Code editor | `firstUpdated()` |
| `document.documentElement` | Code editor | `isDarkMode()` (guarded) |
| `window.setInterval` | Select, Collapsible sub-components | `connectedCallback()` |
| `ace.edit()` | Code editor | `firstUpdated()` |

These hooks only execute in the browser, so importing during SSR never triggers them.

---

## SSR Utility API

The `@elixir-cloud/design` package exports SSR helpers:

```ts
import {
isBrowser,
isServer,
ssrSafeDefine,
generateDeterministicId,
resetIdCounter,
} from "@elixir-cloud/design";
```

| Function | Description |
|----------|-------------|
| `isBrowser()` | Returns `true` in browser environments |
| `isServer()` | Returns `true` in Node.js / SSR |
| `ssrSafeDefine(tag, ctor)` | Safely registers a custom element |
| `generateDeterministicId(prefix)` | Counter-based ID generation |
| `resetIdCounter()` | Resets the ID counter (for testing) |

---

## Declarative Shadow DOM (DSD)

Lit has experimental support for Declarative Shadow DOM via `@lit-labs/ssr`. DSD allows a server response to include shadow roots as HTML so the browser can display styled content before JavaScript loads:

```html
<ecc-utils-design-button>
<template shadowrootmode="open">
<style>/* component styles */</style>
<button><slot></slot></button>
</template>
Click me
</ecc-utils-design-button>
```

### Current Status

- **DSD-based server rendering is not yet implemented** in this library
- Components are safe to **import** on the server (no crashes)
- Rendering and hydration happen on the client via dynamic imports
- Future: integrate `@lit-labs/ssr` for server-side template rendering

> **Note:** DSD requires browser support (`template[shadowrootmode]`). Chrome, Edge, and Firefox 123+ support it natively. Safari 16.4+ supports the older `shadowroot` attribute.

---

## Constraints & Best Practices

1. **Always use `"use client"` or dynamic imports** — Web Components need the browser DOM to render
2. **Don't access `this.shadowRoot` in constructors** — defer to `connectedCallback()` or `firstUpdated()`
3. **Avoid `Math.random()` in component state** — use `generateDeterministicId()` instead
4. **Don't rely on `window` or `document` at module scope** — gate behind `isBrowser()`
5. **Test SSR compatibility** — run `node -e "import('@elixir-cloud/design')"` to verify no crashes

---

## Troubleshooting

### `ReferenceError: customElements is not defined`
You're importing a component at the module level on the server. Use dynamic imports or `"use client"`.

### Hydration mismatch warnings
Check for `Math.random()` or `Date.now()` in component state. Use deterministic values instead.

### Flash of Unstyled Content (FOUC)
Add CSS to hide undefined custom elements until they upgrade:

```css
:not(:defined) {
visibility: hidden;
}
```

<!-- TODO: Integrate @lit-labs/ssr for full server-side shadow DOM rendering -->
<!-- TODO: Add Declarative Shadow DOM template generation -->
<!-- TODO: Add framework-specific wrapper packages (React, Vue, Angular) -->
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ import ECCClientElixirCloudRegistryServiceCreate from "./service-create.js";
export * from "./service-create.js";
export default ECCClientElixirCloudRegistryServiceCreate;

window.customElements.define(
"ecc-client-elixir-cloud-registry-service-create",
ECCClientElixirCloudRegistryServiceCreate
);
// SSR guard: customElements is not available in Node.js
if (
typeof window !== "undefined" &&
window.customElements &&
!window.customElements.get("ecc-client-elixir-cloud-registry-service-create")
) {
window.customElements.define(
"ecc-client-elixir-cloud-registry-service-create",
ECCClientElixirCloudRegistryServiceCreate
);
}

declare global {
interface HTMLElementTagNameMap {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ import ECCClientElixirDrsFilerObjectCreate from "./object-create.js";

export * from "./object-create.js";

window.customElements.define(
"ecc-client-elixir-drs-filer-object-create",
ECCClientElixirDrsFilerObjectCreate
);
// SSR guard: customElements is not available in Node.js
if (
typeof window !== "undefined" &&
window.customElements &&
!window.customElements.get("ecc-client-elixir-drs-filer-object-create")
) {
window.customElements.define(
"ecc-client-elixir-drs-filer-object-create",
ECCClientElixirDrsFilerObjectCreate
);
}

declare global {
interface HTMLElementTagNameMap {
Expand Down
15 changes: 11 additions & 4 deletions packages/ecc-client-elixir-ro-crate/src/components/about/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ import ECCClientRoCrateAbout from "./about.js";
export * from "./about.js";
export default ECCClientRoCrateAbout;

window.customElements.define(
"ecc-client-elixir-ro-crate-about",
ECCClientRoCrateAbout
);
// SSR guard: customElements is not available in Node.js
if (
typeof window !== "undefined" &&
window.customElements &&
!window.customElements.get("ecc-client-elixir-ro-crate-about")
) {
window.customElements.define(
"ecc-client-elixir-ro-crate-about",
ECCClientRoCrateAbout
);
}

declare global {
interface HTMLElementTagNameMap {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ import ECCClientElixirTrsToolCreate from "./tool-create.js";
export * from "./tool-create.js";
export default ECCClientElixirTrsToolCreate;

window.customElements.define(
"ecc-client-elixir-trs-tool-create",
ECCClientElixirTrsToolCreate
);
// SSR guard: customElements is not available in Node.js
if (
typeof window !== "undefined" &&
window.customElements &&
!window.customElements.get("ecc-client-elixir-trs-tool-create")
) {
window.customElements.define(
"ecc-client-elixir-trs-tool-create",
ECCClientElixirTrsToolCreate
);
}

declare global {
interface HTMLElementTagNameMap {
Expand Down
13 changes: 11 additions & 2 deletions packages/ecc-client-ga4gh-drs/src/components/object/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@ import ECCClientGa4ghDrsObject from "./object.js";

export * from "./object.js";

// Define the custom element
customElements.define("ecc-client-ga4gh-drs-object", ECCClientGa4ghDrsObject);
// SSR guard: customElements is not available in Node.js
if (
typeof window !== "undefined" &&
window.customElements &&
!window.customElements.get("ecc-client-ga4gh-drs-object")
) {
window.customElements.define(
"ecc-client-ga4gh-drs-object",
ECCClientGa4ghDrsObject
);
}

export default ECCClientGa4ghDrsObject;

Expand Down
15 changes: 11 additions & 4 deletions packages/ecc-client-ga4gh-drs/src/components/objects-list/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ import ECCClientGa4ghDrsObjects from "./objects.js";
export * from "./objects.js";
export default ECCClientGa4ghDrsObjects;

window.customElements.define(
"ecc-client-ga4gh-drs-objects",
ECCClientGa4ghDrsObjects
);
// SSR guard: customElements is not available in Node.js
if (
typeof window !== "undefined" &&
window.customElements &&
!window.customElements.get("ecc-client-ga4gh-drs-objects")
) {
window.customElements.define(
"ecc-client-ga4gh-drs-objects",
ECCClientGa4ghDrsObjects
);
}

declare global {
interface HTMLElementTagNameMap {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ import ECCClientGa4ghServiceRegistryService from "./service.js";

export * from "./service.js";

// Define the custom element
customElements.define(
"ecc-client-ga4gh-service-registry-service",
ECCClientGa4ghServiceRegistryService
);
// SSR guard: customElements is not available in Node.js
if (
typeof window !== "undefined" &&
window.customElements &&
!window.customElements.get("ecc-client-ga4gh-service-registry-service")
) {
window.customElements.define(
"ecc-client-ga4gh-service-registry-service",
ECCClientGa4ghServiceRegistryService
);
}

export default ECCClientGa4ghServiceRegistryService;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ import { ECCClientGa4ghServiceRegistryServices } from "./services.js";
export * from "./services.js";
export default ECCClientGa4ghServiceRegistryServices;

window.customElements.define(
"ecc-client-ga4gh-service-registry-services",
ECCClientGa4ghServiceRegistryServices
);
// SSR guard: customElements is not available in Node.js
if (
typeof window !== "undefined" &&
window.customElements &&
!window.customElements.get("ecc-client-ga4gh-service-registry-services")
) {
window.customElements.define(
"ecc-client-ga4gh-service-registry-services",
ECCClientGa4ghServiceRegistryServices
);
}

declare global {
interface HTMLElementTagNameMap {
Expand Down
15 changes: 11 additions & 4 deletions packages/ecc-client-ga4gh-tes/src/components/create-run/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ import ECCCLientGa4ghTesCreateRun from "./create-run.js";
export * from "./create-run.js";
export default ECCCLientGa4ghTesCreateRun;

window.customElements.define(
"ecc-client-ga4gh-tes-create-run",
ECCCLientGa4ghTesCreateRun
);
// SSR guard: customElements is not available in Node.js
if (
typeof window !== "undefined" &&
window.customElements &&
!window.customElements.get("ecc-client-ga4gh-tes-create-run")
) {
window.customElements.define(
"ecc-client-ga4gh-tes-create-run",
ECCCLientGa4ghTesCreateRun
);
}

declare global {
interface HTMLElementTagNameMap {
Expand Down
Loading