package virtual_dom
OCaml bindings for the virtual-dom library
Install
Dune Dependency
Authors
Maintainers
Sources
v0.17.0.tar.gz
sha256=812711b4d5ce634a413580cd7096482de8d71abec9ae37bb01f7c0dcec2968ec
doc/src/virtual_dom.html5_history/html5_history.ml.html
Source file html5_history.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
open Core module Dom = Js_of_ocaml.Dom module Dom_html = Js_of_ocaml.Dom_html module Js = Js_of_ocaml.Js (* History api only cares about path, query, and fragment and the browser doesn't have access to userinfo to be able to fully recreate the uri correctly. Before sending to history api we strip all the parts of the URI that are not necessary *) let uri_to_html5_history_string uri = Uri.make ~scheme:"https" ~path:(Uri.path uri) ~query:(Uri.query uri) ?fragment:(Uri.fragment uri) () (* http/s uri's get their paths canonicalized with a leading slash so we ensure we canonicalize as a https uri and then remove. *) |> Uri.canonicalize |> (fun uri -> Uri.with_scheme uri None) |> Uri.to_string ;; module T = struct module type Payload = sig type t [@@deriving bin_io] end module Entry = struct type 'p t = { payload : 'p option ; uri : Uri.t } end type 'p t = { payload_module : (module Payload with type t = 'p) ; payload_bin_shape : string ; popstate_bus : ('p Entry.t -> unit, read_write) Bus.t ; log_s : Sexp.t -> unit } let log_s t sexp : unit = t.log_s sexp let initialised = ref false let convert_state (type p) (t : p t) (state : Js.Unsafe.top Js.t Js.opt) = let result = match Js.Opt.to_option state with | None -> error_s [%message "Html5_history" "history state was null, presumably due to initial page load"] | Some state -> let (module Payload : Payload with type t = p) = t.payload_module in let get_string (x : _ Js.t) key = match Js.Optdef.to_option (Js.Unsafe.get x (Js.string key)) with | None -> None | Some value -> (match Js.to_string (Js.typeof value) with | "string" -> Some (Js.to_string value) | _ -> None) in (match get_string state "bin_shape", get_string state "payload_v1" with | None, _ | _, None -> let state = (* a state object can be "anything that can be serialised", which is not precisely the same as "can be JSONd", but this is a reasonable best effort. *) match Js_of_ocaml.Json.output state with | exception exn -> [%sexp "failed to turn state into JSON", (exn : exn)] | string -> [%sexp (Js.to_string string : string)] in error_s [%message "Html5_history" "history state non-null, but bin_shape or payload missing" (state : Sexp.t)] | Some saved_shape, Some payload -> (match String.equal saved_shape t.payload_bin_shape with | false -> error_s [%message "Html5_history" "history event bin shape mismatch" ~saved_shape ~expected:t.payload_bin_shape] | true -> Or_error.try_with (fun () -> (* Even though the bin-shapes are the same and we would expect this to always succeed, it's still possible that it fails due to serializers that involve other formats e.g. Binable.Of_sexpable *) let payload = Base64.decode_exn payload in Binable.of_string (module Payload) payload))) in match result with | Ok state -> Some state | Error error -> log_s t [%sexp (error : Error.t)]; None ;; let current_uri () = let string = Js.to_string Dom_html.window##.location##.href in match Uri.of_string string with | uri -> uri | exception exn -> raise_s [%message "Html5_history" "BUG: browser gave us a URI we can't parse" string (exn : exn)] ;; let init_exn (type p) ?(log_s = ignore) payload_module = (match !initialised with | true -> failwith "You called Html5_history.init_exn twice" | false -> initialised := true); let (module Payload : Payload with type t = p) = payload_module in let payload_bin_shape = Bin_prot.Shape.eval_to_digest_string [%bin_shape: Payload.t] in let popstate_bus = Bus.create_exn [%here] Arity1 ~on_subscription_after_first_write:Allow ~on_callback_raise:Error.raise in let t = { payload_module; payload_bin_shape; popstate_bus; log_s } in let (_ : Dom_html.event_listener_id) = let handler event = let payload = let state : Js.Unsafe.top Js.t Js.opt = (* [Dom_html.popStateEvent##.state] claims the type is [Js.Unsafe.top Js.t] when actually it could be null. *) Js.Unsafe.get event (Js.string "state") in convert_state t state in let uri = current_uri () in Bus.write popstate_bus { payload; uri }; Js._true in Dom.addEventListener Dom_html.window Dom_html.Event.popstate (Dom_html.handler handler) Js._true in t ;; let popstate_bus t = Bus.read_only t.popstate_bus let current t = let payload = (* [Dom_html.window##.history##.state] claims the type is [Js.Unsafe.top Js.t] when actually it could be null. *) let state : Js.Unsafe.top Js.t Js.opt = Js.Unsafe.get Dom_html.window##.history (Js.string "state") in convert_state t state in let uri = current_uri () in { Entry.payload; uri } ;; let push_or_replace (type p) t action ?uri state : unit = let (module Payload : Payload with type t = p) = t.payload_module in let payload = Binable.to_string (module Payload) state in (* we've got to base64 it or unicode-inspired corruption happens *) let payload = Base64.encode_exn payload in let state = Js.Unsafe.obj [| "bin_shape", Js.Unsafe.inject (Js.string t.payload_bin_shape) ; "payload_v1", Js.Unsafe.inject (Js.string payload) |] in let title = (* according to https://developer.mozilla.org/en-US/docs/Web/API/History/pushState, most browsers ignore this parameter, and passing the empty string is safe against future behaviour changes. *) Js.string "" in let uri = match uri with | None -> Js.null | Some uri -> uri |> uri_to_html5_history_string |> Js.string |> Js.some in match action with | `Replace -> Dom_html.window##.history##replaceState state title uri | `Push -> Dom_html.window##.history##pushState state title uri ;; let replace t ?uri p : unit = push_or_replace t `Replace ?uri p let push t ?uri p : unit = push_or_replace t `Push ?uri p end module Opinionated = struct module Html5_history = T module type Uri_routing = sig type t [@@deriving equal, sexp_of] val parse : Uri.t -> (t, [ `Not_found ]) Result.t val to_path_and_query : t -> Uri.t end module type History_state = sig type uri_routing type t [@@deriving bin_io, equal, sexp_of] val to_uri_routing : t -> uri_routing val of_uri_routing : uri_routing -> t end module type Arg_modules = sig module Uri_routing : Uri_routing module History_state : History_state with type uri_routing := Uri_routing.t end type 's t = { html5_history : 's Html5_history.t ; arg_modules : (module Arg_modules with type History_state.t = 's) ; mutable current_state : 's ; changes_bus : ('s -> unit, read_write) Bus.t } let log_s t sexp : unit = Html5_history.log_s t.html5_history sexp let push_or_replace (type s) (module Arg_modules : Arg_modules with type History_state.t = s) html5_history action state = let open Arg_modules in let uri = Uri_routing.to_path_and_query (History_state.to_uri_routing state) in Html5_history.push_or_replace html5_history action ~uri state ;; let init_exn ?log_s:log_s_arg (type u s) history_state_module uri_routing_module ~on_bad_uri = let module Arg_modules = struct module History_state = (val history_state_module : History_state with type t = s and type uri_routing = u) module Uri_routing = (val uri_routing_module : Uri_routing with type t = u) end in let open Arg_modules in let html5_history = Html5_history.init_exn ?log_s:log_s_arg (module History_state) in let current_state = let { Html5_history.Entry.payload; uri } = Html5_history.current html5_history in match payload with | Some payload -> Html5_history.log_s html5_history [%message "Html5_history" "initial history state from state payload" (payload : History_state.t)]; payload | None -> (match Uri_routing.parse uri with | Ok routing -> History_state.of_uri_routing routing | Error `Not_found -> let message = [%message "Html5_history" "The server should not have served up the main HTML file on this uri, \ as it does not route" ~uri:(Uri.to_string uri)] in (match on_bad_uri with | `Raise -> raise_s message | `Default_state s -> Html5_history.log_s html5_history message; s)) in (* this effectively canonicalises the address bar, and sets the state object such that we can just un-bin-prot it on navigation rather than use the URI. *) push_or_replace (module Arg_modules) html5_history `Replace current_state; let t = { html5_history ; arg_modules = (module Arg_modules) ; current_state ; changes_bus = Bus.create_exn [%here] Arity1 ~on_subscription_after_first_write:Allow ~on_callback_raise:Error.raise } in let (_ : _ Bus.Subscriber.t) = let bus = Html5_history.popstate_bus html5_history in Bus.subscribe_exn bus [%here] ~f:(fun state -> match state.payload with | None -> log_s t [%message "Html5_history" "ignored popstate due to no payload"] | Some payload -> log_s t [%message "Html5_history" "popstate" ~_:(payload : History_state.t)]; t.current_state <- payload; Bus.write t.changes_bus payload) in t ;; let current t = t.current_state let changes_bus t = Bus.read_only t.changes_bus let update (type s) t next_state : unit = let (module Arg_modules : Arg_modules with type History_state.t = s) = t.arg_modules in let open Arg_modules in let prev_state = t.current_state in let what_do = match History_state.equal prev_state next_state with | true -> (* optimisation to avoid spamming history.state. *) `Nothing | false -> (match Uri_routing.equal (History_state.to_uri_routing prev_state) (History_state.to_uri_routing next_state) with | false -> `Push | true -> `Replace) in t.current_state <- next_state; match what_do with | `Nothing -> () | (`Replace | `Push) as action -> log_s t [%message "Html5_history" "updating history state" (action : [ `Push | `Replace ]) (prev_state : History_state.t) (next_state : History_state.t)]; push_or_replace t.arg_modules t.html5_history action next_state ;; let replace t next_state : unit = t.current_state <- next_state; push_or_replace t.arg_modules t.html5_history `Replace next_state ;; let sync_to_bonsai t ~extra_bus ~get_state ~ = let (_ : _ Bus.Subscriber.t) = Bus.subscribe_exn (changes_bus t) [%here] ~f:schedule_navigate_to in let (_ : _ Bus.Subscriber.t) = Bus.subscribe_exn extra_bus [%here] ~f:(fun next -> match get_state next with | Error `Uninitialised -> () | Ok next -> update t next) in () ;; end include T
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>