Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: migrate IFrame component to functional component #21677

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
14 changes: 7 additions & 7 deletions apps/signup-form/src/components/Frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,14 @@ type TailwindFrameProps = ResizableFrameProps & {
/**
* Loads all the CSS styles inside an iFrame.
*/
const TailwindFrame: React.FC<TailwindFrameProps> = ({children, onResize, style, title}) => {
const head = (
<>
<style dangerouslySetInnerHTML={{__html: styles}} />
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0" name="viewport" />
</>
);
const head = (
<>
<style dangerouslySetInnerHTML={{__html: styles}} />
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0" name="viewport" />
</>
);

const TailwindFrame: React.FC<TailwindFrameProps> = ({children, onResize, style, title}) => {
return (
<IFrame head={head} style={style} title={title} onResize={onResize}>
{children}
Expand Down
116 changes: 52 additions & 64 deletions apps/signup-form/src/components/IFrame.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,58 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {Component} from 'react';
import React from 'react';
import {createPortal} from 'react-dom';

/**
* This is still a class component because it causes issues with the behaviour (DOM recreation and layout glitches) if we switch to a functional component. Feel free to refactor.
*/
export default class IFrame extends Component<any> {
node: any;
iframeHtml: any;
iframeHead: any;
iframeRoot: any;

constructor(props: {onResize?: (el: HTMLElement) => void, children: any}) {
super(props);
this.setNode = this.setNode.bind(this);
this.node = null;
}

componentDidMount() {
this.node.addEventListener('load', this.handleLoad);
}

handleLoad = () => {
this.setupFrameBaseStyle();
};

componentWillUnmount() {
this.node.removeEventListener('load', this.handleLoad);
}

setupFrameBaseStyle() {
if (this.node.contentDocument) {
this.iframeHtml = this.node.contentDocument.documentElement;
this.iframeHead = this.node.contentDocument.head;
this.iframeRoot = this.node.contentDocument.body;
this.forceUpdate();

if (this.props.onResize) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(new ResizeObserver(_ => this.props.onResize(this.iframeRoot)))?.observe?.(this.iframeRoot);
type IFrameProps = React.PropsWithChildren<{
style: React.CSSProperties
head: React.ReactNode;
title: string;
onResize?: (el: HTMLElement) => void;
} & Omit<React.ComponentProps<'iframe'>, 'onResize'>>

const IFrame: React.FC<IFrameProps> = ({children, head, title = '', onResize, style = {}, ...rest}) => {
const [iframeHead, setIframeHead] = React.useState<HTMLHeadElement>();
const [iframeRoot, setIframeRoot] = React.useState<HTMLElement>();

// TODO: Migrate to callback ref when React 19 cleanup refs are available: https://react.dev/blog/2024/04/25/react-19#cleanup-functions-for-refs
const node = React.useRef<HTMLIFrameElement>(null);
React.useEffect(() => {
function setupFrameBaseStyle() {
if (node.current?.contentDocument) {
const iframeRootLocal = node.current.contentDocument.body;
// This is safe because of batched updates, new to React 18
setIframeHead(node.current.contentDocument.head);
setIframeRoot(iframeRootLocal);

Comment on lines +22 to +23
Copy link
Author

Choose a reason for hiding this comment

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

No need to recall this.forceUpdate anymore, since the batched updates will trigger a re-render, updating the iframeHead and iframeRoot usages in the JSX

if (onResize) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(new ResizeObserver(_ => onResize(iframeRootLocal)))?.observe?.(iframeRootLocal);
}

// This is a bit hacky, but prevents us to need to attach even listeners to all the iframes we have
// because when we want to listen for keydown events, those are only send in the window of iframe that is focused
// To get around this, we pass down the keydown events to the main window
// No need to detach, because the iframe would get removed
node.current.contentWindow?.addEventListener('keydown', (e: KeyboardEvent | undefined) => {
// dispatch a new event
window.dispatchEvent(
new KeyboardEvent('keydown', e)
);
});
}

// This is a bit hacky, but prevents us to need to attach even listeners to all the iframes we have
// because when we want to listen for keydown events, those are only send in the window of iframe that is focused
// To get around this, we pass down the keydown events to the main window
// No need to detach, because the iframe would get removed
this.node.contentWindow.addEventListener('keydown', (e: KeyboardEvent | undefined) => {
// dispatch a new event
window.dispatchEvent(
new KeyboardEvent('keydown', e)
);
});
}
}

setNode(node: any) {
this.node = node;
}
node.current?.addEventListener('load', setupFrameBaseStyle);

return () => {
node.current?.removeEventListener('load', setupFrameBaseStyle);
};
}, [onResize]);

return (
<iframe srcDoc={`<!DOCTYPE html>`} {...rest} ref={node} frameBorder="0" style={style} title={title}>
{iframeHead && createPortal(head, iframeHead)}
{iframeRoot && createPortal(children, iframeRoot)}
</iframe>
);
};

render() {
const {children, head, title = '', style = {}, ...rest} = this.props;
return (
<iframe srcDoc={`<!DOCTYPE html>`} {...rest} ref={this.setNode} frameBorder="0" style={style} title={title}>
{this.iframeHead && createPortal(head, this.iframeHead)}
{this.iframeRoot && createPortal(children, this.iframeRoot)}
</iframe>
);
}
}
export default IFrame;
Loading