Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fbdeb22
[jer/preview-ng-5-hooks] [jer/preview-hooks] Add usePreviewHost and u…
jeremywiebe Mar 30, 2026
0ae5eb2
[jer/preview-ng-5-hooks] Rename usePreviewHost/Client to usePreviewCo…
jeremywiebe Apr 16, 2026
3ab4eb0
[jer/preview-ng-5-hooks] Unify duplicate createQuestionPreview helper…
jeremywiebe Apr 16, 2026
f991b55
[jer/preview-ng-5-hooks] Formatting
jeremywiebe Apr 20, 2026
96830c3
[jer/preview-ng-5-hooks] Inline iframe (only used once)
jeremywiebe Apr 27, 2026
c5a587a
[jer/preview-ng-5-hooks] Use '/' for targetOrigin for all postMessage…
jeremywiebe Apr 27, 2026
dd854c3
[jer/preview-ng-5-hooks] Remove ESLint suppression and just add ifram…
jeremywiebe Apr 27, 2026
f39c505
[jer/preview-ng-5-hooks] Fix naming of UsePreviewHostResult to UsePre…
jeremywiebe Apr 27, 2026
a0304bc
[jer/preview-ng-5-hooks] Remove unused 'lint report' message
jeremywiebe Apr 28, 2026
4123c9e
[jer/preview-ng-5-hooks] Fix naming of UsePreviewClientResult to UseP…
jeremywiebe Apr 28, 2026
39b9d17
[jer/preview-ng-5-hooks] Inline iframe var
jeremywiebe Apr 28, 2026
e9e3183
[jer/preview-ng-5-hooks] Fix up example in docs
jeremywiebe Apr 28, 2026
f7ac1dd
[jer/preview-ng-5-hooks] Remove message id: not actually useful/needed
jeremywiebe Apr 28, 2026
9b16cfb
[jer/preview-ng-5-hooks] Make each hook's message handler the same (u…
jeremywiebe Apr 28, 2026
2097499
[jer/preview-ng-5-hooks] Fixups
jeremywiebe Apr 28, 2026
aebd01b
[jer/preview-ng-5-hooks] Remove unneeded Log mock
jeremywiebe Apr 29, 2026
23c4d8b
[jer/preview-ng-5-hooks] Use interfaces for message types (and 'inher…
jeremywiebe May 1, 2026
4688928
[jer/preview-ng-5-hooks] Rename request-data event to iframe-ready
jeremywiebe May 1, 2026
d79d1f9
[jer/preview-ng-5-hooks] Ensure iframe's contentWindow is non-null wh…
jeremywiebe May 1, 2026
ace7e36
[jer/preview-ng-5-hooks] Make sure we capture pending data if iframe …
jeremywiebe May 1, 2026
ec89ac8
[jer/preview-ng-5-hooks] Rename event type and migrate to constructor…
jeremywiebe May 1, 2026
3992713
[jer/preview-ng-5-hooks] Remove 'paths' from linterContext test data …
jeremywiebe May 4, 2026
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/quiet-stars-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus-editor": minor
---

Add usePreviewController and usePreviewPresenter hooks for typed iframe preview communication
47 changes: 17 additions & 30 deletions packages/perseus-editor/src/preview/message-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,6 @@ interface PreviewMessageBase {
source: typeof PREVIEW_MESSAGE_SOURCE;
}

/**
* Base type for messages with iframe ID
*
* Note: The ID is primarily used for debugging/logging purposes.
* Message routing is handled by event.source filtering in the hooks,
* not by comparing ID strings.
*/
interface PreviewMessageWithId extends PreviewMessageBase {
id: string;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on Ben's question about why the ID shouldn't be used for routing, I realized that the "gating" by message source that we do in the message handlers is sufficient. The id is just an extra value that doesn't add any further safety and so is just extra work for no benefit.

}

/**
* Data for question preview (full item with question, answer area, and hints)
*/
Expand Down Expand Up @@ -82,10 +71,10 @@ export type PreviewContent =
/**
* Message from parent sending content data to iframe
*/
type PreviewDataMessage = PreviewMessageWithId & {
interface PreviewDataMessage extends PreviewMessageBase {
type: "content-data";
content: PreviewContent;
};
}

/**
* Union of all messages sent from parent to iframe
Expand All @@ -95,32 +84,30 @@ export type ParentToIframeMessage = PreviewDataMessage;
// ---- Iframe → Parent messages ----

/**
* Message from iframe requesting data from parent
* Message from iframe to parent telling it the iframe is ready
*/
type PreviewDataRequestMessage = PreviewMessageWithId & {
type: "request-data";
};
interface PreviewIframeReadyMessage extends PreviewMessageBase {
type: "iframe-ready";
}

/**
* Message from iframe reporting its content height
*/
type PreviewHeightUpdateMessage = PreviewMessageWithId & {
interface PreviewHeightUpdateMessage extends PreviewMessageBase {
type: "height-update";
height: number;
};

/**
* Message from iframe reporting lint warnings
*/
type PreviewLintReportMessage = PreviewMessageWithId & {
type: "lint-report";
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature exists today, but it just logs the lint report to the JS console, so effectively, it does nothing! Also, the frontend doesn't actually send this data so we're waiting for a message that'll never come!

We'll bring it back if/when we actually want to implement it.

lintWarnings: ReadonlyArray<any>;
};
}

/**
* Union of all messages sent from iframe to parent
*/
export type IframeToParentMessage =
| PreviewDataRequestMessage
| PreviewHeightUpdateMessage
| PreviewLintReportMessage;
| PreviewIframeReadyMessage
| PreviewHeightUpdateMessage;

export function createPreviewIframeReadyMessage(): PreviewIframeReadyMessage {
return {
source: PREVIEW_MESSAGE_SOURCE,
type: "iframe-ready",
};
}
12 changes: 0 additions & 12 deletions packages/perseus-editor/src/preview/message-validators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ describe("message-validators", () => {
const message = {
source: PREVIEW_MESSAGE_SOURCE,
type: "request-data" as const,
id: "test-id",
};

expect(isIframeToParentMessage(message)).toBe(true);
Expand All @@ -20,7 +19,6 @@ describe("message-validators", () => {
const message = {
source: PREVIEW_MESSAGE_SOURCE,
type: "height-update" as const,
id: "test-id",
height: 500,
};

Expand All @@ -31,7 +29,6 @@ describe("message-validators", () => {
const message = {
source: PREVIEW_MESSAGE_SOURCE,
type: "lint-report" as const,
id: "test-id",
lintWarnings: [],
};

Expand All @@ -55,7 +52,6 @@ describe("message-validators", () => {
it("returns false for object without source property", () => {
const message = {
type: "request-data",
id: "test-id",
};

expect(isIframeToParentMessage(message)).toBe(false);
Expand All @@ -65,7 +61,6 @@ describe("message-validators", () => {
const message = {
source: 123,
type: "request-data",
id: "test-id",
};

expect(isIframeToParentMessage(message)).toBe(false);
Expand All @@ -75,7 +70,6 @@ describe("message-validators", () => {
const message = {
source: "wrong-source",
type: "request-data",
id: "test-id",
};

expect(isIframeToParentMessage(message)).toBe(false);
Expand Down Expand Up @@ -105,7 +99,6 @@ describe("message-validators", () => {
const message = {
source: PREVIEW_MESSAGE_SOURCE,
type: "content-data" as const,
id: "test-id",
content: {
type: "question" as const,
data: {
Expand Down Expand Up @@ -138,7 +131,6 @@ describe("message-validators", () => {
it("returns false for object without source property", () => {
const message = {
type: "content-data",
id: "test-id",
content: {},
};

Expand All @@ -149,7 +141,6 @@ describe("message-validators", () => {
const message = {
source: {nested: "object"},
type: "content-data",
id: "test-id",
};

expect(isParentToIframeMessage(message)).toBe(false);
Expand All @@ -159,7 +150,6 @@ describe("message-validators", () => {
const message = {
source: "different-source",
type: "content-data",
id: "test-id",
};

expect(isParentToIframeMessage(message)).toBe(false);
Expand All @@ -186,7 +176,6 @@ describe("message-validators", () => {
const message = {
source: PREVIEW_MESSAGE_SOURCE,
type: "content-data",
id: "test-id",
unexpectedProperty: "should-not-break",
};

Expand All @@ -199,7 +188,6 @@ describe("message-validators", () => {
const message = {
source: PREVIEW_MESSAGE_SOURCE,
type: "some-type",
id: "test-id",
};

// Both type guards only check source, so this passes both
Expand Down
Loading