Skip to content

Commit 2a185de

Browse files
authored
Superjson, monaco editor, client side validation (#71)
* initial monaco editor * wrapping stuff * raw superjson support * fix superjson hard coding * biome lint * fix monaco not getting updated on auto fill * biome lint 2 * fix lockfile issue * explain superjson input
1 parent a4e2cee commit 2a185de

File tree

14 files changed

+823
-180
lines changed

14 files changed

+823
-180
lines changed

packages/dev-app/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
"@trpc/react-query": "^11.0.0-next-beta.264",
1616
"@trpc/server": "^11.0.0-next-beta.264",
1717
"autoprefixer": "^10.4.14",
18-
"jsoneditor": "^9.10.3",
1918
"next": "^13.2.4",
2019
"next-transpile-modules": "^10.0.0",
2120
"postcss": "^8.4.21",
@@ -24,12 +23,13 @@
2423
"superjson": "1.12.2",
2524
"tailwindcss": "^3.3.1",
2625
"ts-loader": "^9.4.2",
27-
"zod": "^3.21.4"
26+
"zod": "^3.24.2"
2827
},
2928
"devDependencies": {
3029
"@types/node": "^18.15.5",
3130
"@types/react": "^18.0.28",
3231
"@types/react-dom": "^18.0.11",
32+
"react-scan": "^0.2.12",
3333
"typescript": "^5.4.5"
3434
},
3535
"ct3aMetadata": {

packages/dev-app/src/pages/_app.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type { AppType } from "next/app";
33
import { api } from "~/utils/api";
44

55
import "~/styles/globals.css";
6-
import "jsoneditor/dist/jsoneditor.css";
76

87
const MyApp: AppType = ({ Component, pageProps }) => {
98
return <Component {...pageProps} />;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// pages/_document
2+
import { Head, Html, Main, NextScript } from "next/document";
3+
4+
export default function Document() {
5+
return (
6+
<Html lang="en">
7+
<Head>
8+
{/* React scan */}
9+
<script src="https://unpkg.com/react-scan/dist/auto.global.js" />
10+
</Head>
11+
<body>
12+
<Main />
13+
<NextScript />
14+
</body>
15+
</Html>
16+
);
17+
}

packages/dev-app/src/router.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@ import { z } from "zod";
33
import { createTRPCRouter, procedure } from "~/server/api/trpc";
44

55
const postsRouter = createTRPCRouter({
6+
complexSuperJson: procedure
7+
.input(
8+
z.object({
9+
id: z.bigint(),
10+
name: z.string(),
11+
createdAt: z.date(),
12+
tags: z.set(z.string()),
13+
metadata: z.map(z.string(), z.string()),
14+
}),
15+
)
16+
.query(({ input }) => {
17+
return {
18+
message: "You used superjson!",
19+
input: input,
20+
};
21+
}),
622
meta: procedure
723
.meta({
824
description: "This is a router that contains posts",
@@ -31,13 +47,31 @@ const postsRouter = createTRPCRouter({
3147
createPost: procedure
3248
.input(
3349
z.object({
34-
text: z.string(),
50+
text: z.string().min(1),
51+
nested: z.object({
52+
nestedText: z.string(),
53+
}),
3554
}),
3655
)
3756
.mutation(({ input }) => {
57+
return {
58+
...input,
59+
};
60+
}),
61+
dateTest: procedure
62+
.input(
63+
z.object({
64+
date: z.date(),
65+
nested: z.object({
66+
text: z.string(),
67+
}),
68+
}),
69+
)
70+
.mutation(({ input }) => {
71+
console.log(input);
3872
return {
3973
id: "aoisdjfoasidjfasodf",
40-
text: input.text,
74+
time: input.date.getTime(),
4175
};
4276
}),
4377
createNestedPost: procedure

packages/trpc-ui/README.md

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ app.use("/panel", (_, res) => {
5454

5555
// Dynamically import renderTrpcPanel only in development
5656
const { renderTrpcPanel } = await import("trpc-ui");
57-
57+
5858
return res.send(
5959
renderTrpcPanel(myTrpcRouter, {
6060
url: "http://localhost:4000/trpc", // Base url of your trpc server
@@ -200,11 +200,77 @@ app.use("/panel", (_, res) => {
200200
});
201201
```
202202

203-
Submitting superjson only data types like `Date` or `Map` are not yet supported (they will be soon), but superjson data types returned from the server will be rendered correctly.
203+
### Superjson example and usage
204+
205+
When you are using superjson, the json editor will show a basic wrapping of superjson input. Your actual json input goes under the _json_ key, and information about how fields should be parsed goes under the _meta_ key. Do not delete the _json_ key or enter your input outside of it, or else your input will not be parsed correctly.
206+
207+
```json
208+
{
209+
"json": {},
210+
"meta": {
211+
"values": {}
212+
}
213+
}
214+
```
215+
216+
Let's say on your backend, you want to have a zod validator like this, which supports data types not supported by json directly.
217+
218+
```ts
219+
import { z } from "zod";
220+
221+
const UserSchema = z.object({
222+
id: z.bigint(),
223+
name: z.string(),
224+
createdAt: z.date(),
225+
tags: z.set(z.string()),
226+
metadata: z.map(z.string(), z.string()),
227+
});
228+
229+
type User = z.infer<typeof UserSchema>;
230+
231+
const user: User = {
232+
id: BigInt("9007199254740991"),
233+
name: "John Doe",
234+
createdAt: new Date("2025-03-16T12:00:00Z"),
235+
tags: new Set(["admin", "active"]),
236+
metadata: new Map([
237+
["lastLogin", new Date("2025-03-15T08:30:00Z")],
238+
["loginCount", 42],
239+
["isPremium", true],
240+
["tier", "gold"],
241+
]),
242+
};
243+
```
244+
245+
Enter the json supported equivalent under the _json_ key, and and specify how they should be parsed under the _meta.values_ key. Before validating this data on the backend, superjson will deserialize it back into JavaScript bigints, dates, sets, and maps, meaning it will pass your zod validation!
246+
247+
```json
248+
{
249+
"json": {
250+
"id": "9007199254740991",
251+
"name": "John Doe",
252+
"createdAt": "2025-03-16T12:00:00.000Z",
253+
"tags": ["admin", "active"],
254+
"metadata": [["tier", "gold"]]
255+
},
256+
"meta": {
257+
"values": {
258+
"id": ["bigint"],
259+
"createdAt": ["Date"],
260+
"tags": ["set"],
261+
"metadata": ["map"]
262+
}
263+
}
264+
}
265+
```
266+
267+
**You will likely need to uncheck "validate input on client" in order to be able to send your validation to the backend when using custom superjson inputs**
268+
269+
Also be warned that currently, input fields will not be rendered for superjson only inputs. You will have to input all data manually through the json editor.
204270

205271
## Contributing
206272

207-
`trpc-ui` welcomes and encourages open source contributions. Please see our [contributing](./CONTRIBUTING.md) guide for information on how to develop locally. Besides contributing PRs, one of the most helpful things you can to is to look at the existing [feature proposals](https://github.com/aidansunbury/trpc-ui/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20label%3Aenhancement) and leave a 👍 on the features that would be most helpful for you.
273+
`trpc-ui` welcomes and encourages open source contributions. Please see our [contributing](./CONTRIBUTING.md) guide for information on how to develop locally. Besides contributing PRs, one of the most helpful things you can to is to look at the existing [feature proposals](https://github.com/aidansunbury/trpc-ui/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20label%3Aenhancement) and leave a 👍 on the features that would be most helpful for you.
208274

209275
## Comparisons
210276

packages/trpc-ui/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
"@trpc/server": "^11.0.0-next-beta.264",
6161
"@types/jest": "^29.2.4",
6262
"@types/json-bigint": "^1.0.1",
63-
"@types/jsoneditor": "^9.9.1",
6463
"@types/react": "^18.0.21",
6564
"@types/react-dom": "^18.0.6",
6665
"ajv": "^8.11.2",
@@ -72,7 +71,6 @@
7271
"gulp-replace": "^1.1.3",
7372
"jest": "^29.3.1",
7473
"json-bigint": "^1.0.0",
75-
"jsoneditor": "^9.10.3",
7674
"pkg-pr-new": "^0.0.35",
7775
"postcss": "^8.4.19",
7876
"react": "18.2.0",
@@ -95,6 +93,7 @@
9593
"zustand": "^4.1.5"
9694
},
9795
"dependencies": {
96+
"@monaco-editor/react": "^4.7.0",
9897
"@stoplight/json-schema-sampler": "^0.3.0",
9998
"@textea/json-viewer": "^3.0.0",
10099
"clsx": "^2.1.1",

packages/trpc-ui/src/react-app/Root.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { MetaHeader } from "./components/MetaHeader";
2424
import { RouterContainer } from "./components/RouterContainer";
2525
import { SideNav } from "./components/SideNav";
2626
import { TopBar } from "./components/TopBar";
27+
import { RenderOptionsProvider } from "./components/contexts/OptionsContext";
2728

2829
export function RootComponent({
2930
rootRouter,
@@ -41,11 +42,13 @@ export function RootComponent({
4142
<SiteNavigationContextProvider>
4243
<ClientProviders trpc={trpc} options={options}>
4344
<HotKeysContextProvider>
44-
<SearchOverlay>
45-
<div className="relative flex h-full w-full flex-1 flex-col">
46-
<AppInnards rootRouter={rootRouter} options={options} />
47-
</div>
48-
</SearchOverlay>
45+
<RenderOptionsProvider options={options}>
46+
<SearchOverlay>
47+
<div className="relative flex h-full w-full flex-1 flex-col">
48+
<AppInnards rootRouter={rootRouter} options={options} />
49+
</div>
50+
</SearchOverlay>
51+
</RenderOptionsProvider>
4952
</HotKeysContextProvider>
5053
</ClientProviders>
5154
</SiteNavigationContextProvider>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { RenderOptions } from "@src/render";
2+
import React, { createContext, useContext, ReactNode } from "react";
3+
4+
// @ts-expect-error
5+
const RenderOptionsContext = createContext<RenderOptions>(null);
6+
7+
interface RenderOptionsProviderProps {
8+
options: RenderOptions;
9+
children: ReactNode;
10+
}
11+
12+
export const RenderOptionsProvider: React.FC<RenderOptionsProviderProps> = ({
13+
options,
14+
children,
15+
}) => {
16+
// Provide the options as a readonly value (React context values are immutable by design)
17+
return (
18+
<RenderOptionsContext.Provider value={options}>
19+
{children}
20+
</RenderOptionsContext.Provider>
21+
);
22+
};
23+
24+
export const useRenderOptions = (): RenderOptions => {
25+
const context = useContext(RenderOptionsContext);
26+
27+
if (context === null) {
28+
throw new Error(
29+
"useRenderOptions must be used within a RenderOptionsProvider",
30+
);
31+
}
32+
33+
return context;
34+
};

packages/trpc-ui/src/react-app/components/form/Field.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@ export function Field({
1919
inputNode: ParsedInputNode;
2020
control: Control<any>;
2121
}) {
22-
const label = inputNode.path.join(".");
23-
const path = `${ROOT_VALS_PROPERTY_NAME}.${label}`;
22+
const logicallLabel = inputNode.path.join(".");
23+
const path = `${ROOT_VALS_PROPERTY_NAME}.${logicallLabel}`;
24+
const pathArray = inputNode.path.slice();
25+
if (pathArray.length > 0 && pathArray[0] === "json") {
26+
pathArray.shift();
27+
}
28+
29+
const label = pathArray.join(".");
2430
switch (inputNode.type) {
2531
case "string":
2632
return (

packages/trpc-ui/src/react-app/components/form/JSONEditor.tsx

Lines changed: 0 additions & 44 deletions
This file was deleted.

0 commit comments

Comments
 (0)