Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/dual-editor-vanilla-componet-example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@openworkflowspec/vanilla-web-component-example": minor
---

Integrate the Dual Editor in the Vanilla Web Component example.
25 changes: 19 additions & 6 deletions examples/vanilla-web-component/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

# @openworkflowspec/vanilla-web-component-example

This example demonstrates how to embed the `@openworkflowspec/diagram-editor` React component in a **non-React** host application using a Web Component.
This example demonstrates how to embed the `@openworkflowspec/dual-editor` React component in a **non-React** host application using a Web Component. It displays the text and diagram editors side by side.

## Quick Start

Expand All @@ -31,10 +31,10 @@ Browse http://localhost:6007

## How It Works

The wrapper in `src/diagram-editor-element.ts` defines a custom element `<openworkflowspec-diagram-editor>` that:
The wrapper in `src/dual-editor-element.ts` defines a custom element `<openworkflowspec-dual-editor>` that:

1. Creates a React root inside itself
2. Renders the `DiagramEditor` React component
2. Renders the `DualEditor` React component and creates its language-service worker
3. Re-renders when attributes or properties change
4. Unmounts the React root on disconnect

Expand All @@ -45,17 +45,18 @@ The wrapper in `src/diagram-editor-element.ts` defines a custom element `<openwo
| `content` (property) | `string` | - | Open Workflow specification in YAML or JSON format |
| `locale` | `string` | - | Language locale for the editor UI |
| `color-mode` | `'light' \| 'dark' \| 'system'` | `"system"` | Color theme for the editor |
| `language` | `'json' \| 'yaml'` | `"json"` | Text syntax and diagram output format |
| `read-only` | boolean | - | Enable read-only mode to prevent editing |

### Usage

```html
<openworkflowspec-diagram-editor locale="en" read-only></openworkflowspec-diagram-editor>
<openworkflowspec-dual-editor locale="en" language="yaml"></openworkflowspec-dual-editor>

<script type="module">
import "./src/index.ts";

const editor = document.querySelector("openworkflowspec-diagram-editor");
const editor = document.querySelector("openworkflowspec-dual-editor");
editor.content = `document:
dsl: '1.0.3'
namespace: examples
Expand All @@ -73,4 +74,16 @@ do:

### CSS

The entry point (`src/index.ts`) imports `@openworkflowspec/diagram-editor/styles.css`. All editor styles are scoped under `.dec-root` with prefixed Tailwind classes.
The entry point (`src/index.ts`) imports `@openworkflowspec/dual-editor/styles.css`, which includes the text and diagram editor styles.

## Shadow DOM integration

Monaco Editor has a known issue when embedded inside Shadow DOM: diagnostic underlines appear, but their hover messages may not be visible.

As a temporary workaround, stop `mousemove` propagation on the editor's mount container:

```ts
container.addEventListener("mousemove", (event) => event.stopPropagation());
```

The container must be inside the Shadow Root and wrap the Text Editor.
10 changes: 5 additions & 5 deletions examples/vanilla-web-component/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Diagram Editor — Web Component Example</title>
<title>Dual Editor — Web Component Example</title>
<style>
body {
height: 100vh;
Expand All @@ -31,7 +31,7 @@
text-align: center;
}

openworkflowspec-diagram-editor {
openworkflowspec-dual-editor {
display: block;
width: 80%;
height: 80%;
Expand All @@ -43,14 +43,14 @@
</style>
</head>
<body>
<h1>Diagram Editor — Web Component Example</h1>
<openworkflowspec-diagram-editor locale="en" read-only></openworkflowspec-diagram-editor>
<h1>Dual Editor — Web Component Example</h1>
<openworkflowspec-dual-editor locale="en" language="yaml"></openworkflowspec-dual-editor>

<script type="module">
import "./src/index.ts";
import { sampleWorkflow } from "./src/sample-workflow.ts";

const editor = document.querySelector("openworkflowspec-diagram-editor");
const editor = document.querySelector("openworkflowspec-dual-editor");
editor.content = sampleWorkflow;
</script>
</body>
Expand Down
5 changes: 3 additions & 2 deletions examples/vanilla-web-component/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@openworkflowspec/vanilla-web-component-example",
"version": "1.1.0",
"private": true,
"description": "non-React example embedding the open workflow diagram component",
"description": "non-React example embedding the Open Workflow dual editor",
"keywords": [],
"homepage": "https://github.com/open-workflow-specification/editor",
"bugs": {
Expand Down Expand Up @@ -30,7 +30,8 @@
},
"dependencies": {
"@openworkflowspec/core": "workspace:*",
"@openworkflowspec/diagram-editor": "workspace:*",
"@openworkflowspec/dual-editor": "workspace:*",
"@openworkflowspec/text-editor": "workspace:*",
"react": "catalog:",
"react-dom": "catalog:"
},
Expand Down
1 change: 1 addition & 0 deletions examples/vanilla-web-component/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default defineConfig({

use: {
baseURL: "http://localhost:6007",
permissions: ["clipboard-read", "clipboard-write"],
},

webServer: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,42 +14,49 @@
* limitations under the License.
*/

// React is required internally by @openworkflowspec/diagram-editor.
// React is required internally by @openworkflowspec/dual-editor.
// This wrapper encapsulates it behind a standard Custom Element API,
// so host applications don't need to interact with React directly.
import { createElement } from "react";
import { createRoot, type Root } from "react-dom/client";
import { DiagramEditor } from "@openworkflowspec/diagram-editor";
import type { DiagramEditorProps } from "@openworkflowspec/diagram-editor";
import { DualEditor } from "@openworkflowspec/dual-editor";
import type { DualEditorProps } from "@openworkflowspec/dual-editor";
// Vite transforms this module import into a Worker constructor.
// oxlint-disable-next-line import/default
import LanguageServiceWorker from "@openworkflowspec/text-editor/worker?worker";
import type { ColorMode } from "@openworkflowspec/core";
export class DiagramEditorElement extends HTMLElement {
static observedAttributes = ["locale", "color-mode", "read-only"];

export class DualEditorElement extends HTMLElement {
static observedAttributes = ["locale", "color-mode", "read-only", "language"];

private _root: Root | null = null;
private _mountPoint: HTMLDivElement | null = null;
private _content = "";
private _contentResetKey = 0;

get content(): string {
return this._content;
}

set content(value: string) {
this._content = value;
this._contentResetKey += 1;
this.render();
}

connectedCallback(): void {
this._mountPoint = document.createElement("div");
this._mountPoint.style.height = "100%";
this._mountPoint.dataset.testid = "diagram-editor-mount-point";
this._mountPoint.dataset.testid = "dual-editor-mount-point";
this.appendChild(this._mountPoint);
this._root = createRoot(this._mountPoint);
this.render();
}

disconnectedCallback(): void {
this._root?.unmount();
const root = this._root;
this._root = null;
root?.unmount();
this._mountPoint?.remove();
this._mountPoint = null;
}
Expand All @@ -61,13 +68,22 @@ export class DiagramEditorElement extends HTMLElement {
private render(): void {
if (!this._root) return;

const props: DiagramEditorProps = {
const contentResetKey = this._contentResetKey;
const props: DualEditorProps = {
content: this._content,
contentResetKey,
locale: this.getAttribute("locale") ?? "en",
isReadOnly: this.hasAttribute("read-only"),
colorMode: (this.getAttribute("color-mode") as ColorMode) ?? "system",
language: this.getAttribute("language") === "yaml" ? "yaml" : "json",
createLanguageServiceWorker: () => new LanguageServiceWorker(),
onContentChange: (content) => {
if (contentResetKey !== this._contentResetKey) return;
this._content = content;
this.render();
},
};

this._root.render(createElement(DiagramEditor, props));
this._root.render(createElement(DualEditor, props));
}
}
8 changes: 4 additions & 4 deletions examples/vanilla-web-component/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
* limitations under the License.
*/

import "@openworkflowspec/diagram-editor/styles.css";
import { DiagramEditorElement } from "./diagram-editor-element";
import "@openworkflowspec/dual-editor/styles.css";
import { DualEditorElement } from "./dual-editor-element";

if (!customElements.get("openworkflowspec-diagram-editor")) {
customElements.define("openworkflowspec-diagram-editor", DiagramEditorElement);
if (!customElements.get("openworkflowspec-dual-editor")) {
customElements.define("openworkflowspec-dual-editor", DualEditorElement);
}
78 changes: 64 additions & 14 deletions examples/vanilla-web-component/tests-e2e/diagram-editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,85 @@

import { test, expect } from "@playwright/test";

test("web component renders the diagram editor", async ({ page }) => {
test("web component renders both dual editor panes", async ({ page }) => {
await page.goto("/");

await expect(page.locator(".monaco-editor").first()).toBeVisible();
await expect(page.locator(".monaco-editor").first()).toContainText("accumulate-room-readings");
await expect(page.getByRole("region", { name: "Diagram editor" })).toBeVisible();
await expect(page.getByTestId("diagram-container")).toBeVisible();

await expect(page.getByTestId("rf__node-/do/consumeReading")).toContainText("consumeReading");
});

test("web component re-renders when content is replaced", async ({ page }) => {
test("text edits update the content property and diagram, and external content resets both", async ({
page,
}) => {
await page.goto("/");

await expect(page.getByTestId("rf__node-/do/consumeReading")).toContainText("consumeReading");

const newWorkflow = `document:
const editor = page.locator(".monaco-editor").first();
const originalWorkflow = `document:
dsl: '1.0.3'
namespace: examples
name: original
do:
- firstTask:
call: http
with:
method: get
endpoint: https://example.com/first`;
const editedWorkflow = `document:
dsl: '1.0.3'
namespace: examples
name: greet
name: edited
do:
- sayHello:
- updatedTask:
call: http
with:
method: get
endpoint: https://example.com/hello`;
endpoint: https://example.com/updated`;

await editor.click();
await page.keyboard.press("ControlOrMeta+a");
await page.evaluate((content) => navigator.clipboard.writeText(content), editedWorkflow);
await page.keyboard.press("ControlOrMeta+v");

await expect(page.getByTestId("rf__node-/do/updatedTask")).toContainText("updatedTask");
await expect
.poll(() =>
page
.locator("openworkflowspec-dual-editor")
.evaluate((element) => (element as HTMLElement & { content: string }).content),
)
.toBe(editedWorkflow);

await page.evaluate((content) => {
const customEditor = document.querySelector("openworkflowspec-dual-editor") as
| (HTMLElement & { content: string })
| null;
if (!customEditor) throw new Error("Dual Editor custom element was not found");
customEditor.content = content;
}, originalWorkflow);

await expect(page.locator(".monaco-editor").first()).toContainText("firstTask");
await expect(page.getByTestId("rf__node-/do/firstTask")).toContainText("firstTask");
await expect(page.getByTestId("rf__node-/do/updatedTask")).not.toBeVisible();
});

test("JSON mode provides Open Workflow schema diagnostics", async ({ page }) => {
await page.goto("/");

await page.evaluate((yaml) => {
const editor = document.querySelector("openworkflowspec-diagram-editor") as any;
editor.content = yaml;
}, newWorkflow);
const monacoEditor = page.locator(".monaco-editor").first();
await page.evaluate(() => {
const editor = document.querySelector("openworkflowspec-dual-editor") as
| (HTMLElement & { content: string })
| null;
if (!editor) throw new Error("Dual Editor custom element was not found");
editor.setAttribute("language", "json");
editor.content = JSON.stringify({
document: { dsl: "1.0.3", namespace: "examples", name: "missing-do" },
});
});

await expect(page.getByTestId("rf__node-/do/sayHello")).toContainText("sayHello");
await expect(page.getByTestId("rf__node-/do/consumeReading")).not.toBeVisible();
await expect(monacoEditor.locator(".squiggly-warning").first()).toBeVisible();
});
Loading
Loading