Skip to content

Commit 5578ec4

Browse files
committed
feat: work with router context
1 parent 3b77aaa commit 5578ec4

File tree

4 files changed

+209
-82
lines changed

4 files changed

+209
-82
lines changed

demo/server/dune

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
(libraries
88
dream
99
demo_shared_native
10+
server-reason-react.url_native
1011
react
1112
reactDOM
1213
html

demo/server/server.re

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ let rscRouting = (basePath, handler) => {
2828
getAndPost(
2929
basePath ++ path,
3030
request => {
31+
let url = {
32+
let protocol = Dream.tls(request) ? "https" : "http";
33+
let host =
34+
switch (Dream.header(request, "Host")) {
35+
| Some(h) => h
36+
| None => ""
37+
};
38+
let target = Dream.target(request);
39+
Printf.sprintf("%s://%s%s", protocol, host, target);
40+
};
41+
3142
let routeSegments = String.split_on_char('/', path);
3243
let rscParam = Dream.query(request, "rsc");
3344
let element =
@@ -36,9 +47,17 @@ let rscRouting = (basePath, handler) => {
3647
let rscRouteSegments = rscPath |> String.split_on_char('/');
3748
RouteDefinitions.(
3849
routes |> renderComponent(routeSegments, rscRouteSegments)
39-
);
50+
)
51+
|> Option.value(~default=React.null);
4052
| None =>
41-
RouteDefinitions.(routes |> renderByPath(routeSegments))
53+
<Supersonic.RouterContext.Provider url={URL.makeExn(url)}>
54+
{switch (
55+
RouteDefinitions.(routes |> renderByPath(routeSegments))
56+
) {
57+
| Some(element) => element
58+
| None => React.null
59+
}}
60+
</Supersonic.RouterContext.Provider>
4261
};
4362

4463
handler(~element, request);

demo/universal/native/shared/Supersonic.re

Lines changed: 183 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
exception NoProvider(string);
2+
module DOM = Webapi.Dom;
3+
module Location = DOM.Location;
4+
module History = DOM.History;
5+
6+
module URL = {
7+
include URL;
8+
9+
let to_json = t => {
10+
t |> toString |> Melange_json.To_json.string;
11+
};
12+
13+
let of_json = json => {
14+
json |> Melange_json.Of_json.string |> makeExn;
15+
};
16+
};
17+
118
let findPathDifference = (path1: list(string), path2: list(string)) => {
219
let rec findCommonPrefix = (p1, p2, acc) => {
320
switch (p1, p2) {
@@ -10,6 +27,9 @@ let findPathDifference = (path1: list(string), path2: list(string)) => {
1027
findCommonPrefix(path1, path2, []);
1128
};
1229

30+
[@mel.send]
31+
external dispatchEvent: (Dom.window, Dom.event) => unit = "dispatchEvent";
32+
1333
module RouteRegistry = {
1434
type route = {
1535
level: int,
@@ -51,38 +71,157 @@ module RouteRegistry = {
5171
};
5272

5373
module RouterContext = {
54-
type route = {
55-
level: int,
56-
path: string,
57-
loader: (string, string) => unit,
74+
type t = {
75+
url: URL.t,
76+
navigate: (~replace: bool, string) => unit,
77+
};
78+
79+
[@mel.new] external makeEventIE11Compatible: string => Dom.event = "Event";
80+
81+
[@mel.scope "document"]
82+
external createEventNonIEBrowsers: string => Dom.event = "createEvent";
83+
84+
[@mel.send]
85+
external initEventNonIEBrowsers: (Dom.event, string, bool, bool) => unit =
86+
"initEvent";
87+
88+
[@platform js]
89+
let safeMakeEvent = eventName =>
90+
if (Js.typeof(DOM.Event.make) == "function") {
91+
makeEventIE11Compatible(eventName);
92+
} else {
93+
let event = createEventNonIEBrowsers("Event");
94+
initEventNonIEBrowsers(event, eventName, true, true);
95+
event;
96+
};
97+
98+
[@platform js]
99+
let push = path => {
100+
History.pushState(History.state(DOM.history), "", path, DOM.history);
101+
DOM.EventTarget.dispatchEvent(
102+
safeMakeEvent("popstate"),
103+
DOM.Window.asEventTarget(DOM.window),
104+
);
58105
};
59106

60-
type t = {navigate: (~replace: bool, string) => unit};
107+
[@platform js]
108+
let replace = path => {
109+
History.replaceState(History.state(DOM.history), "", path, DOM.history);
110+
DOM.EventTarget.dispatchEvent(
111+
safeMakeEvent("popstate"),
112+
DOM.Window.asEventTarget(DOM.window),
113+
);
114+
};
61115

62-
let context: React.Context.t(t) =
63-
React.createContext({navigate: (~replace as _, _) => ()});
116+
[@platform js]
117+
let watchUrl = callback => {
118+
let watcherID = _ =>
119+
callback(URL.makeExn(Location.href(DOM.window->DOM.Window.location)));
120+
DOM.EventTarget.addEventListener(
121+
"popstate",
122+
watcherID,
123+
DOM.Window.asEventTarget(DOM.window),
124+
);
125+
watcherID;
126+
};
64127

65128
[@platform js]
129+
let unwatchUrl = watcherID => {
130+
DOM.EventTarget.removeEventListener(
131+
"popstate",
132+
watcherID,
133+
DOM.Window.asEventTarget(DOM.window),
134+
);
135+
};
136+
137+
let context: React.Context.t(option(t)) = React.createContext(None);
138+
66139
module Provider = {
67140
let provider = React.Context.provider(context);
68141

69-
[@react.component]
70-
let make = (~value, ~children) => {
71-
React.createElement(
72-
provider,
73-
{
74-
"value": value,
75-
"children": children,
76-
},
77-
);
142+
[@react.client.component]
143+
let make = (~url: URL.t, ~children: React.element) => {
144+
switch%platform (Runtime.platform) {
145+
| Client =>
146+
let (url, setUrl) = React.useState(() => url);
147+
148+
React.useEffect0(() => {
149+
let watcherId = watchUrl(url => setUrl(_ => url));
150+
151+
/**
152+
* check for updates that may have occured between
153+
* the initial state and the subscribe above
154+
*/
155+
let newUrl =
156+
URL.makeExn(Location.href(DOM.window->DOM.Window.location));
157+
if (newUrl == url) {
158+
setUrl(_ => newUrl);
159+
};
160+
161+
Some(() => unwatchUrl(watcherId));
162+
});
163+
let navigate = (~replace as _, path: string) => {
164+
let location = DOM.window->DOM.Window.location;
165+
let curPath =
166+
Location.pathname(location)
167+
->String.sub(5, String.length(Location.pathname(location)) - 5)
168+
|> String.split_on_char('/');
169+
let pathSegments =
170+
path->String.sub(5, String.length(path) - 5)
171+
|> String.split_on_char('/');
172+
let (commonPrefix, remainingDifference) =
173+
findPathDifference(curPath, pathSegments);
174+
175+
let route: option(RouteRegistry.route) =
176+
RouteRegistry.find((commonPrefix |> List.length) - 2);
177+
178+
switch (route) {
179+
| Some(route) =>
180+
switch (route.loader) {
181+
| Some(loader) =>
182+
loader(
183+
commonPrefix |> String.concat("/"),
184+
remainingDifference |> String.concat("/"),
185+
)
186+
| None => ()
187+
}
188+
| None => ()
189+
};
190+
RouteRegistry.clearAboveLevel((commonPrefix |> List.length) - 1);
191+
192+
push(path) |> ignore;
193+
};
194+
195+
React.createElement(
196+
provider,
197+
{
198+
"value":
199+
Some({
200+
url,
201+
navigate,
202+
}),
203+
"children": children,
204+
},
205+
);
206+
| Server =>
207+
provider(
208+
~value=
209+
Some({
210+
url,
211+
navigate: (~replace as _, _) =>
212+
failwith("navigate in'tnot supported on server"),
213+
}),
214+
~children,
215+
(),
216+
)
217+
};
78218
};
79219
};
80220

81-
[@platform native]
82-
module Provider = {
83-
[@react.component]
84-
let make = (~value as _, ~children) => {
85-
children;
221+
let use = () => {
222+
switch (React.useContext(context)) {
223+
| Some(context) => context
224+
| None => raise(NoProvider("RouterContext requires a provider"))
86225
};
87226
};
88227
};
@@ -95,63 +234,12 @@ external setNavigate:
95234
[@platform js]
96235
external navigate: (~replace: bool, string) => unit = "window.__navigate";
97236

98-
module DOM = Webapi.Dom;
99-
module Location = DOM.Location;
100-
module History = DOM.History;
101-
102237
module Router = {
103238
[@react.client.component]
104239
let make = (~children: React.element) =>
105240
switch%platform (Runtime.platform) {
106241
| Server => children
107242
| Client =>
108-
let rscNavigation = (~replace as _, path: string) => {
109-
let location = DOM.window->DOM.Window.location;
110-
let curPath =
111-
Location.pathname(location)
112-
->String.sub(5, String.length(Location.pathname(location)) - 5)
113-
|> String.split_on_char('/');
114-
let pathSegments =
115-
path->String.sub(5, String.length(path) - 5)
116-
|> String.split_on_char('/');
117-
let (commonPrefix, remainingDifference) =
118-
findPathDifference(curPath, pathSegments);
119-
120-
let route: option(RouteRegistry.route) =
121-
RouteRegistry.find((commonPrefix |> List.length) - 2);
122-
123-
switch (route) {
124-
| Some(route) =>
125-
switch (route.loader) {
126-
| Some(loader) =>
127-
loader(
128-
commonPrefix |> String.concat("/"),
129-
remainingDifference |> String.concat("/"),
130-
)
131-
| None => ()
132-
}
133-
| None => ()
134-
};
135-
RouteRegistry.clearAboveLevel((commonPrefix |> List.length) - 1);
136-
let origin = Location.origin(location);
137-
138-
let finalURL =
139-
URL.makeExn(
140-
origin
141-
++ "/demo"
142-
++ (commonPrefix |> String.concat("/"))
143-
++ "/"
144-
++ (remainingDifference |> String.concat("/")),
145-
);
146-
History.pushState(
147-
History.state(DOM.history),
148-
"",
149-
URL.toString(finalURL),
150-
DOM.history,
151-
)
152-
|> ignore;
153-
};
154-
155243
// let popStateHandler = () => {
156244
// let handlePopState = _ => {
157245
// let newPath = Location.pathname(DOM.window->DOM.Window.location);
@@ -173,11 +261,9 @@ module Router = {
173261
// );
174262
// };
175263

176-
setNavigate(Webapi.Dom.window, rscNavigation);
177-
178264
// React.useEffect0(popStateHandler);
179265

180-
children;
266+
children
181267
};
182268
};
183269

@@ -213,10 +299,17 @@ module Route = {
213299
(
214300
~path: string,
215301
~children: React.element,
216-
~outlet: React.element,
302+
~outlet: option(React.element),
217303
~level: int,
218304
) => {
219-
let (outlet, setOutlet) = React.useState(() => outlet);
305+
Js.log("Route: " ++ path);
306+
let (outlet, setOutlet) =
307+
React.useState(() =>
308+
switch (outlet) {
309+
| Some(outlet) => outlet
310+
| None => React.null
311+
}
312+
);
220313
let (cachedNodeKey, setCachedNodeKey) = React.useState(() => path);
221314

222315
let%browser_only loader = (commonPrefix, remainingDifference) => {
@@ -268,12 +361,21 @@ module Link = {
268361
~replace: bool=false,
269362
~className: option(string)=?,
270363
) => {
364+
let {RouterContext.url, navigate} = RouterContext.use();
365+
let path = URL.pathname(url);
366+
let isActive = path == to_;
271367
let handleClick = (e: React.Event.Mouse.t) => {
272368
React.Event.Mouse.preventDefault(e);
273369
navigate(~replace, to_);
274370
};
275371

276-
<button onClick=handleClick ?className> children </button>;
372+
let className =
373+
switch (className) {
374+
| Some(className) => className ++ (isActive ? " font-bold" : "")
375+
| None => ""
376+
};
377+
378+
<button onClick=handleClick className> children </button>;
277379
};
278380
};
279381

@@ -290,6 +392,9 @@ module Navigation = {
290392
<Link to_="/demo/router/about/work" className="text-white">
291393
{React.string("About work")}
292394
</Link>
395+
<Link to_="/demo/router/about" className="text-white">
396+
{React.string("About (404)")}
397+
</Link>
293398
<Link to_="/demo/router/dashboard" className="text-white">
294399
{React.string("Dashboard")}
295400
</Link>

0 commit comments

Comments
 (0)