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

Phoenix LiveView & React integration #2988

Draft
wants to merge 11 commits into
base: add-eslint
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ and this project adheres to

### Changed

- Changed the way Monaco workers are loaded, using ESM modules instead.
- Do not include `v` param in workflow links when it is latest
[#2941](https://github.com/OpenFn/lightning/issues/2941)

Expand Down
2 changes: 1 addition & 1 deletion assets/css/app.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import './monaco-style-overrides.css';
@import '../js/monaco/styles.css';
@import 'reactflow/dist/style.css';
@import '../../deps/petal_components/assets/default.css';
/* This file is for your main application CSS */
Expand Down
28 changes: 0 additions & 28 deletions assets/css/monaco-style-overrides.css

This file was deleted.

4 changes: 4 additions & 0 deletions assets/dev-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const ctx = await esbuild.context({
target: ['es2020'],
tsconfig: 'tsconfig.browser.json',
jsx: 'automatic',
loader: {
'.woff2': 'file',
'.ttf': 'copy',
},
plugins: [
postcss(),
copy({
Expand Down
2 changes: 2 additions & 0 deletions assets/js/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export {
CloseNodePanelViaEscape,
};

export { ReactComponent } from '#/react/hooks';

export const TabIndent = {
mounted() {
this.el.addEventListener('keydown', e => {
Expand Down
40 changes: 37 additions & 3 deletions assets/js/monaco/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,43 @@
import { useRef, useCallback } from 'react';
import ResizeObserver from 'rc-resize-observer';
import Editor, { type Monaco, loader } from '@monaco-editor/react';
import type { editor } from 'monaco-editor';
import Editor, { loader, type Monaco } from '@monaco-editor/react';
import * as monaco from 'monaco-editor';

loader.config({ paths: { vs: '/assets/monaco-editor/vs' } });
// Configure Monaco with ESM workers
// This needs to be outside of the component to ensure it's only set once
if (typeof window !== 'undefined') {
// @ts-ignore - Monaco global configuration
window.MonacoEnvironment = {
getWorker(_moduleId: string, label: string) {
switch (label) {
case 'json':
return new Worker(new URL('json.worker.js', import.meta.url), {
type: 'module',
});
case 'css':
return new Worker(new URL('css.worker.js', import.meta.url), {
type: 'module',
});
case 'html':
return new Worker(new URL('html.worker.js', import.meta.url), {
type: 'module',
});
case 'typescript':
case 'javascript':
return new Worker(new URL('typescript.worker.js', import.meta.url), {
type: 'module',
});
default:
return new Worker(new URL('editor.worker.js', import.meta.url), {
type: 'module',
});
}
},
};
}

loader.config({ monaco });

export function setTheme(monaco: Monaco) {
monaco.editor.defineTheme('default', {
Expand Down Expand Up @@ -36,7 +70,7 @@ export const MonacoEditor = ({
const handleOnMount = useCallback((editor: any, monaco: Monaco) => {
monacoRef.current = monaco;
editorRef.current = editor;
if (!props.options.enableCommandPalette) {
if (!props['options']?.enableCommandPalette) {
const ctxKey = editor.createContextKey('command-palette-override', true);

editor.addCommand(
Expand Down
30 changes: 30 additions & 0 deletions assets/js/monaco/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@import 'monaco-editor/min/vs/editor/editor.main.css';

/* Fixes for curved borders (what a pain!)*/
.monaco-editor,
.monaco-editor .overflow-guard {
background-color: transparent !important;
@apply rounded-md;
}
.monaco-editor .margin {
@apply rounded-l-md;
}
.monaco-editor .monaco-scrollable-element {
@apply rounded-r-md;
border-top-right-radius: 0.375rem;
border-bottom-right-radius: 0.375rem;
}

/* Fix ugly right border */
.monaco-editor .view-overlays .current-line {
border-left: none !important;
border-right: none !important;
}

.monaco-editor .line-numbers {
color: #7c7c7c !important;
}

.monaco-editor .line-numbers.active-line-number {
color: white !important;
}
14 changes: 14 additions & 0 deletions assets/js/react/components/Bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type BarProps = {
before: React.ReactNode;
after: React.ReactNode;
children: React.ReactNode;
};

export const Bar = ({ before, after, children }: BarProps) => (
<>
{before}
<p>Bar:</p>
{children}
{after}
</>
);
11 changes: 11 additions & 0 deletions assets/js/react/components/Baz.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type BazProps = {
baz: number;
children: React.ReactNode;
};

export const Baz = ({ baz, children }: BazProps) => (
<>
<p>Baz: {baz}</p>
{children}
</>
);
33 changes: 33 additions & 0 deletions assets/js/react/components/Boundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, { Suspense } from 'react';

import { ErrorBoundary } from 'react-error-boundary';

import { Fallback } from './Fallback';

export type BoundaryProps = {
children?: React.ReactNode;
};

/**
* Some best practices:
*
* - Catch rendering errors and recover gracefully with an [error boundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)
* TODO: Configure [`ErrorBoundary`](https://github.com/bvaughn/react-error-boundary#readme)
* - Use a [suspense boundary](https://react.dev/reference/react/Suspense) to allow suspense features (such as `React.lazy`) to work.
* TODO: show a loading ("fallback") UI?
*/
export const Boundary = React.forwardRef<HTMLDivElement, BoundaryProps>(
function Boundary({ children }, ref) {
return (
<ErrorBoundary FallbackComponent={Fallback}>
<Suspense>
<div ref={ref} style={{ display: 'contents' }}>
{children}
</div>
</Suspense>
</ErrorBoundary>
);
}
);
// ESBuild renames the inner component to `Boundary2`
Boundary.displayName = 'Boundary';
12 changes: 12 additions & 0 deletions assets/js/react/components/Fallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { FallbackProps } from 'react-error-boundary';

export type { FallbackProps };

export const Fallback = ({ error }: FallbackProps) => (
<div role="alert">
<p>Something went wrong:</p>
<pre style={{ color: 'red' }}>
{error instanceof Error ? error.message : String(error)}
</pre>
</div>
);
11 changes: 11 additions & 0 deletions assets/js/react/components/Foo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type FooProps = {
foo: number;
children: React.ReactNode;
};

export const Foo = ({ foo, children }: FooProps) => (
<>
<p>Foo: {foo}</p>
{children}
</>
);
55 changes: 55 additions & 0 deletions assets/js/react/components/Slot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { View } from 'phoenix_live_view';

import { useLayoutEffect, useRef, useState } from 'react';

import { performPatch } from '#/react/lib';

const style: React.CSSProperties = {
// Don't create a box for this element
// https://developer.mozilla.org/en-US/docs/Web/CSS/display#contents
display: 'contents',
};

export type SlotProps = {
view: View;
name: string;
html: string;
cID?: number | null;
};

export const Slot = ({ view, name, html, cID = null }: SlotProps) => {
const [el, setEl] = useState<HTMLDivElement | null>(null);
const unpatchedRef = useRef(true);

// Intentionally referentially stable value, should never change so that React
// never re-renders the child. We want Phoenix to do it for us instead.
const [innerHTML] = useState(() => ({ __html: html }));

useLayoutEffect(() => {
if (
el == null ||
!el.isConnected ||
(innerHTML.__html === html && unpatchedRef.current)
) {
return;
}

performPatch(
view,
el,
`<div data-react-slot=${name} style="display: contents">${html}</div>`,
cID
);

unpatchedRef.current = false;
}, [name, el, innerHTML.__html, view, html, cID]);

return useState(() => (
<div
ref={setEl}
data-react-slot={name}
dangerouslySetInnerHTML={innerHTML}
style={style}
/>
))[0];
};
7 changes: 7 additions & 0 deletions assets/js/react/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { Foo, type FooProps } from './Foo';
export { Bar, type BarProps } from './Bar';
export { Baz, type BazProps } from './Baz';

export { Fallback, type FallbackProps } from './Fallback';
export { Boundary, type BoundaryProps } from './Boundary';
export { Slot, type SlotProps } from './Slot';
1 change: 1 addition & 0 deletions assets/js/react/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ReactComponent } from './react-component';
Loading