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+
118let 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+
1333module RouteRegistry = {
1434 type route = {
1535 level: int ,
@@ -51,38 +71,157 @@ module RouteRegistry = {
5171};
5272
5373module 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]
96235external navigate : (~replace : bool , string ) => unit = "window.__navigate" ;
97236
98- module DOM = Webapi . Dom ;
99- module Location = DOM . Location ;
100- module History = DOM . History ;
101-
102237module 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