Skip to content

Commit b332b4d

Browse files
committed
feat: add dynamic routing
1 parent 5578ec4 commit b332b4d

File tree

4 files changed

+427
-115
lines changed

4 files changed

+427
-115
lines changed

demo/server/pages/RouterRSC.re

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
let markdownStyles = (~background, ~text) => {
2+
Printf.sprintf(
3+
{|
4+
.markdown h1 {
5+
font-size: 2.25rem;
6+
font-weight: bold;
7+
line-height: 2.5;
8+
}
9+
10+
.markdown h2 {
11+
font-size: 1.875rem;
12+
font-weight: bold;
13+
line-height: 2.5;
14+
}
15+
16+
.markdown h3 {
17+
font-size: 1.5rem;
18+
font-weight: bold;
19+
line-height: 2.5;
20+
}
21+
22+
.markdown h4 {
23+
font-size: 1.25rem;
24+
font-weight: bold;
25+
line-height: 2.5;
26+
}
27+
28+
.markdown h5 {
29+
font-size: 1.125rem;
30+
font-weight: bold;
31+
line-height: 2.5;
32+
}
33+
34+
.markdown h6 {
35+
font-size: 1rem;
36+
font-weight: bold;
37+
line-height: 2.5;
38+
}
39+
40+
.markdown p {
41+
font-size: 1rem;
42+
margin-bottom: 1rem;
43+
}
44+
45+
.markdown ul, .markdown ol {
46+
padding-left: 2rem;
47+
margin-bottom: 1rem;
48+
}
49+
50+
.markdown li {
51+
margin-bottom: 0.5rem;
52+
}
53+
54+
.markdown blockquote {
55+
border-left: 4px solid %s;
56+
padding-left: 1rem;
57+
margin: 1.5rem 0;
58+
font-style: italic;
59+
}
60+
61+
.markdown pre {
62+
padding: 1rem;
63+
margin: 1.5rem 0;
64+
background-color: %s;
65+
color: %s;
66+
border-radius: 0.375rem;
67+
}
68+
69+
.markdown code {
70+
display: block;
71+
margin: 1rem;
72+
padding-left: 1rem;
73+
padding-right: 1rem;
74+
font-family: monospace;
75+
background-color: %s;
76+
color: %s;
77+
padding: 0.25rem 0.5rem;
78+
border-radius: 0.25rem;
79+
}
80+
|},
81+
background,
82+
background,
83+
text,
84+
background,
85+
text,
86+
);
87+
};
88+
89+
module NoteSkeleton = {
90+
[@react.component]
91+
let make = (~isEditing as _) => {
92+
Dream.error(log => log("NoteSkeleton"));
93+
<div className="flex items-center justify-center h-full">
94+
<Text> "Loading..." </Text>
95+
</div>;
96+
};
97+
};
98+
99+
module App = {
100+
[@react.async.component]
101+
let make = (~selectedId, ~isEditing, ~searchText, ~sleep) => {
102+
Lwt.return(
103+
<html>
104+
<head>
105+
<meta charSet="utf-8" />
106+
<style
107+
dangerouslySetInnerHTML={
108+
"__html":
109+
markdownStyles(
110+
~background=Theme.Color.gray2,
111+
~text=Theme.Color.gray12,
112+
),
113+
}
114+
/>
115+
<link rel="stylesheet" href="/output.css" />
116+
</head>
117+
<body>
118+
<DemoLayout background=Theme.Color.Gray2 mode=FullScreen>
119+
<div className="flex flex-row gap-8">
120+
<section
121+
className="flex-1 basis-1/4 gap-4 min-w-[400px]" key="sidebar">
122+
<section
123+
className="flex flex-col gap-1 z-1 max-w-[85%] pointer-events-none mb-6"
124+
key="sidebar-header">
125+
<Text size=Large weight=Bold>
126+
"server-reason-react notes"
127+
</Text>
128+
<p>
129+
<Text color=Theme.Color.Gray10> "migrated from " </Text>
130+
<Link.Text
131+
size=Text.Small
132+
href="https://github.com/reactjs/server-components-demo">
133+
"reactjs/server-components-demo"
134+
</Link.Text>
135+
<Text color=Theme.Color.Gray10>
136+
" with (server)-reason-react and Melange"
137+
</Text>
138+
</p>
139+
</section>
140+
<section
141+
className="mt-4 mb-4 flex flex-row gap-2"
142+
role="menubar"
143+
key="menubar">
144+
<SearchField searchText selectedId isEditing />
145+
</section>
146+
<nav className="mt-4">
147+
<div className="mb-4"> <Hr /> </div>
148+
<div className="mb-4">
149+
<Button noteId=None>
150+
{React.string("Create a note")}
151+
</Button>
152+
</div>
153+
<Hr />
154+
<React.Suspense fallback={<NoteListSkeleton />}>
155+
<NoteList searchText sleep />
156+
</React.Suspense>
157+
</nav>
158+
</section>
159+
<section
160+
key="note-viewer" className="flex-1 basis-3/4 max-w-[75%]">
161+
<React.Suspense fallback={<NoteSkeleton isEditing />}>
162+
<NoteItem selectedId isEditing sleep />
163+
</React.Suspense>
164+
</section>
165+
</div>
166+
</DemoLayout>
167+
</body>
168+
</html>,
169+
);
170+
};
171+
};
172+
173+
let handler = request => {
174+
let selectedId =
175+
Dream.query(request, "selectedId")
176+
|> Option.map(string => int_of_string_opt(string))
177+
|> Option.value(~default=None);
178+
179+
let isEditing =
180+
Dream.query(request, "isEditing")
181+
|> Option.map(v => v == "true")
182+
|> Option.value(~default=false);
183+
184+
let ssr =
185+
Dream.query(request, "ssr")
186+
|> Option.map(v => v == "false")
187+
|> Option.value(~default=true);
188+
189+
let searchText =
190+
Dream.query(request, "searchText") |> Option.value(~default="");
191+
192+
let sleep =
193+
Dream.query(request, "sleep")
194+
->Option.bind(Float.of_string_opt)
195+
->Option.bind(value =>
196+
if (value < 0.) {
197+
None;
198+
} else {
199+
Some(value);
200+
}
201+
);
202+
203+
DreamRSC.createFromRequest(
204+
~disableSSR=!ssr,
205+
~bootstrapModules=["/static/demo/DummyRouterRSC.re.js"],
206+
<App selectedId isEditing searchText sleep />,
207+
request,
208+
);
209+
};

demo/server/server.re

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,20 @@ let getAndPost = (path, handler) =>
1111
],
1212
);
1313

14-
let rscRouting = (basePath, handler) => {
14+
let splitListAt = (n, list) => {
15+
let rec aux = (i, acc, remaining_list) =>
16+
if (i == 0) {
17+
(List.rev(acc), remaining_list == [] ? [""] : remaining_list);
18+
} else {
19+
switch (remaining_list) {
20+
| [] => (List.rev(acc), [])
21+
| [h, ...t] => aux(i - 1, [h, ...acc], t)
22+
};
23+
};
24+
aux(n, [], list);
25+
};
26+
27+
let rscRoutes = (basePath, handler) => {
1528
RouteDefinitions.generated_routes_paths
1629
|> List.map(path => {
1730
let path = path == "/" ? "" : path;
@@ -22,7 +35,8 @@ let rscRouting = (basePath, handler) => {
2235
request => {
2336
// Redirect when the route is accessed with a trailing slash
2437
Dream.log("Redirecting to /demo%s", path);
25-
Dream.redirect(request, basePath ++ path);
38+
let query = Dream.target(request) |> Dream.split_target |> snd;
39+
Dream.redirect(request, basePath ++ path ++ "?" ++ query);
2640
},
2741
),
2842
getAndPost(
@@ -44,15 +58,46 @@ let rscRouting = (basePath, handler) => {
4458
let element =
4559
switch (rscParam) {
4660
| Some(rscPath) =>
47-
let rscRouteSegments = rscPath |> String.split_on_char('/');
61+
Dream.log(
62+
"rscPath: %s",
63+
rscPath
64+
|> String.split_on_char('/')
65+
|> List.length
66+
|> string_of_int,
67+
);
68+
let rscSegmentLevel =
69+
(routeSegments |> List.length)
70+
- (rscPath |> String.split_on_char('/') |> List.length);
71+
72+
/**
73+
* To get the dynamic segments (/:id) we cannot get them from the rsc path
74+
* but from the route path segments.
75+
* We then split the route path into 2 lists:
76+
* - the first list is the parent segments that aren't required but used to find the correct component
77+
* - the second list is the rsc segments
78+
* the level is the number of segments in the rsc query param
79+
* Probably there is a better way to do this but let's do this for now
80+
*/
81+
let (parentSegments, rscSegments) =
82+
splitListAt(rscSegmentLevel, routeSegments);
83+
4884
RouteDefinitions.(
49-
routes |> renderComponent(routeSegments, rscRouteSegments)
85+
routes
86+
|> renderComponent(
87+
request,
88+
parentSegments,
89+
rscSegments,
90+
basePath,
91+
)
5092
)
5193
|> Option.value(~default=React.null);
5294
| None =>
5395
<Supersonic.RouterContext.Provider url={URL.makeExn(url)}>
5496
{switch (
55-
RouteDefinitions.(routes |> renderByPath(routeSegments))
97+
RouteDefinitions.(
98+
routes
99+
|> renderByPath(request, routeSegments, ~basePath)
100+
)
56101
) {
57102
| Some(element) => element
58103
| None => React.null
@@ -103,7 +148,7 @@ let server =
103148
getAndPost(Routes.singlePageRSC, Pages.SinglePageRSC.handler),
104149
getAndPost(Routes.dummyRouterRSC, Pages.DummyRouterRSC.handler),
105150
getAndPost(Routes.serverOnlyRSC, Pages.ServerOnlyRSC.handler),
106-
...rscRouting(Routes.router, Pages.Router.handler),
151+
...rscRoutes(Routes.router, Pages.Router.handler),
107152
]),
108153
);
109154

0 commit comments

Comments
 (0)