Skip to content

Commit b8805d4

Browse files
authored
feat: add react-quill example (#118)
1 parent 34c6c61 commit b8805d4

16 files changed

+3447
-3084
lines changed

react-quill/.eslintrc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/** @type {import('eslint').Linter.Config} */
2+
module.exports = {
3+
extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
4+
};

react-quill/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
3+
/.cache
4+
/build
5+
/public/build
6+
.env

react-quill/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Using react-quill (WYSIWYG) in Remix application
2+
3+
In this example we are implementing a WYSIWYG editor with Quill.
4+
5+
We have used react-quill as our library of choice to implement it.
6+
7+
## Preview
8+
9+
Open this example on [CodeSandbox](https://codesandbox.com):
10+
11+
[![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/remix-run/examples/tree/main/react-quill)
12+
13+
## Example
14+
15+
WYSIWYG editor with react-quill
16+
17+
Because `react-quill` uses `window` features under the hood, you should create a wrapper with [`remix-utils`](https://github.com/sergiodxa/remix-utils)' [`ClientOnly` component](https://github.com/sergiodxa/remix-utils#clientonly).
18+
19+
## Related Links
20+
21+
[Quill official documentation](https://quilljs.com/)
22+
[react-quill](https://zenoamaro.github.io/react-quill/)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function FallbackComponent() {
2+
return <p>Fallback component ...</p>;
3+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { ComponentProps } from "react";
2+
import ReactQuill from "react-quill";
3+
4+
type ReactQuillProps = ComponentProps<typeof ReactQuill>;
5+
type Props = Pick<ReactQuillProps, 'name' | 'onChange' | 'placeholder' | 'theme' | 'value'>;
6+
7+
const toolBarOptions = {
8+
toolbar: [
9+
[{ header: [1, 2, 3, 4, 5, 6, false] }],
10+
[{ size: [] }],
11+
["bold", "italic", "underline", "strike", "blockquote"],
12+
[
13+
{ list: "ordered" },
14+
{ list: "bullet" },
15+
{ indent: "-1" },
16+
{ indent: "+1" },
17+
],
18+
["link", "image", "video"],
19+
["clean"],
20+
],
21+
};
22+
23+
export function TextEditor(props: Props) {
24+
return (
25+
<ReactQuill {...props} modules={toolBarOptions} />
26+
);
27+
}

react-quill/app/entry.client.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { RemixBrowser } from "@remix-run/react";
2+
import { startTransition, StrictMode } from "react";
3+
import { hydrateRoot } from "react-dom/client";
4+
5+
const hydrate = () =>
6+
startTransition(() => {
7+
hydrateRoot(
8+
document,
9+
<StrictMode>
10+
<RemixBrowser />
11+
</StrictMode>
12+
);
13+
});
14+
15+
if (window.requestIdleCallback) {
16+
window.requestIdleCallback(hydrate);
17+
} else {
18+
// Safari doesn't support requestIdleCallback
19+
// https://caniuse.com/requestidlecallback
20+
window.setTimeout(hydrate, 1);
21+
}

react-quill/app/entry.server.tsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { PassThrough } from "stream";
2+
3+
import type { EntryContext } from "@remix-run/node";
4+
import { Response } from "@remix-run/node";
5+
import { RemixServer } from "@remix-run/react";
6+
import isbot from "isbot";
7+
import { renderToPipeableStream } from "react-dom/server";
8+
9+
const ABORT_DELAY = 5000;
10+
11+
const handleRequest = (
12+
request: Request,
13+
responseStatusCode: number,
14+
responseHeaders: Headers,
15+
remixContext: EntryContext
16+
) =>
17+
isbot(request.headers.get("user-agent"))
18+
? handleBotRequest(
19+
request,
20+
responseStatusCode,
21+
responseHeaders,
22+
remixContext
23+
)
24+
: handleBrowserRequest(
25+
request,
26+
responseStatusCode,
27+
responseHeaders,
28+
remixContext
29+
);
30+
export default handleRequest;
31+
32+
const handleBotRequest = (
33+
request: Request,
34+
responseStatusCode: number,
35+
responseHeaders: Headers,
36+
remixContext: EntryContext
37+
) =>
38+
new Promise((resolve, reject) => {
39+
let didError = false;
40+
41+
const { pipe, abort } = renderToPipeableStream(
42+
<RemixServer context={remixContext} url={request.url} />,
43+
{
44+
onAllReady: () => {
45+
const body = new PassThrough();
46+
47+
responseHeaders.set("Content-Type", "text/html");
48+
49+
resolve(
50+
new Response(body, {
51+
headers: responseHeaders,
52+
status: didError ? 500 : responseStatusCode,
53+
})
54+
);
55+
56+
pipe(body);
57+
},
58+
onShellError: (error: unknown) => {
59+
reject(error);
60+
},
61+
onError: (error: unknown) => {
62+
didError = true;
63+
64+
console.error(error);
65+
},
66+
}
67+
);
68+
69+
setTimeout(abort, ABORT_DELAY);
70+
});
71+
72+
const handleBrowserRequest = (
73+
request: Request,
74+
responseStatusCode: number,
75+
responseHeaders: Headers,
76+
remixContext: EntryContext
77+
) =>
78+
new Promise((resolve, reject) => {
79+
let didError = false;
80+
81+
const { pipe, abort } = renderToPipeableStream(
82+
<RemixServer context={remixContext} url={request.url} />,
83+
{
84+
onShellReady: () => {
85+
const body = new PassThrough();
86+
87+
responseHeaders.set("Content-Type", "text/html");
88+
89+
resolve(
90+
new Response(body, {
91+
headers: responseHeaders,
92+
status: didError ? 500 : responseStatusCode,
93+
})
94+
);
95+
96+
pipe(body);
97+
},
98+
onShellError: (error: unknown) => {
99+
reject(error);
100+
},
101+
onError: (error: unknown) => {
102+
didError = true;
103+
104+
console.error(error);
105+
},
106+
}
107+
);
108+
109+
setTimeout(abort, ABORT_DELAY);
110+
});

react-quill/app/root.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { MetaFunction } from "@remix-run/node";
2+
import {
3+
Links,
4+
LiveReload,
5+
Meta,
6+
Outlet,
7+
Scripts,
8+
ScrollRestoration,
9+
} from "@remix-run/react";
10+
11+
export const meta: MetaFunction = () => ({
12+
charset: "utf-8",
13+
title: "New Remix App",
14+
viewport: "width=device-width,initial-scale=1",
15+
});
16+
17+
export default function App() {
18+
return (
19+
<html lang="en">
20+
<head>
21+
<Meta />
22+
<Links />
23+
</head>
24+
<body>
25+
<Outlet />
26+
<ScrollRestoration />
27+
<Scripts />
28+
<LiveReload />
29+
</body>
30+
</html>
31+
);
32+
}

react-quill/app/routes/index.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { json } from "@remix-run/node";
2+
import type { ActionArgs, LinksFunction } from '@remix-run/node';
3+
import { Form } from "@remix-run/react";
4+
import { useState } from "react";
5+
import stylesheetQuill from "react-quill/dist/quill.snow.css";
6+
import { ClientOnly } from "remix-utils";
7+
8+
import { FallbackComponent } from "~/components/fallback-component";
9+
import { TextEditor } from "~/components/textEditor.client";
10+
11+
export const links: LinksFunction = () => {
12+
return [{ rel: "stylesheet", href: stylesheetQuill }];
13+
}
14+
15+
export const action = async ({ request }: ActionArgs) => {
16+
const form = await request.formData();
17+
const textEditorValue = form.get("textEditor");
18+
return json({textEditorValue});
19+
};
20+
21+
export default function Index() {
22+
const [textEditor, setTextEditor] = useState("");
23+
return (
24+
<Form method="post">
25+
<ClientOnly fallback={<FallbackComponent />}>
26+
{() => (
27+
<TextEditor
28+
theme="snow"
29+
placeholder="Write description"
30+
onChange={setTextEditor}
31+
value={textEditor}
32+
/>
33+
)}
34+
</ClientOnly>
35+
<input
36+
type="hidden"
37+
name="textEditor"
38+
value={textEditor}
39+
/>
40+
<br />
41+
<button type="submit">Submit</button>
42+
</Form>
43+
);
44+
}

react-quill/package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"private": true,
3+
"sideEffects": false,
4+
"scripts": {
5+
"build": "remix build",
6+
"dev": "remix dev",
7+
"start": "remix-serve build"
8+
},
9+
"dependencies": {
10+
"@remix-run/node": "*",
11+
"@remix-run/react": "*",
12+
"@remix-run/serve": "*",
13+
"isbot": "^3.6.5",
14+
"react": "^18.2.0",
15+
"react-dom": "^18.2.0",
16+
"react-quill": "^2.0.0",
17+
"remix-utils": "^5.1.0"
18+
},
19+
"devDependencies": {
20+
"@remix-run/dev": "*",
21+
"@remix-run/eslint-config": "*",
22+
"@types/react": "^18.0.25",
23+
"@types/react-dom": "^18.0.8",
24+
"eslint": "^8.27.0",
25+
"typescript": "^4.8.4"
26+
},
27+
"engines": {
28+
"node": ">=14"
29+
}
30+
}

0 commit comments

Comments
 (0)