diff --git a/src/tea_app.ml b/src/tea_app.ml index 8e25032..e6b043e 100644 --- a/src/tea_app.ml +++ b/src/tea_app.ml @@ -8,416 +8,259 @@ Tea.AppEx or so as a simple wrapper? *) -(* module type Program = sig +module type Program = sig type flags type model type msg - val init : flags -> model - val update : model -> msg -> model * 'msg Tea_cmd.t - val subscriptions : model -> int - (* val view : model -> msg Vdom.t *) -end *) - -(* type 'flags 'model testRec = { - init : 'flags -> 'model -} *) - -(* type ('flags, 'model, 'msg) fullProgram = { - internal : unit -> unit; - init : 'flags -> 'model * 'msg Tea_cmd.t; - update : 'model -> 'msg -> 'model * 'msg Tea_cmd.t; - view : 'model -> 'msg Vdom.t; -} *) - -type ('flags, 'model, 'msg) program = { - init : 'flags -> 'model * 'msg Tea_cmd.t; - update : 'model -> 'msg -> 'model * 'msg Tea_cmd.t; - view : 'model -> 'msg Vdom.t; - subscriptions : 'model -> 'msg Tea_sub.t; - shutdown : 'model -> 'msg Tea_cmd.t; -} - - -type ('flags, 'model, 'msg) standardProgram = { - init : 'flags -> 'model * 'msg Tea_cmd.t; - update : 'model -> 'msg -> 'model * 'msg Tea_cmd.t; - view : 'model -> 'msg Vdom.t; - subscriptions : 'model -> 'msg Tea_sub.t; -} - -type ('model, 'msg) beginnerProgram = { - model : 'model; - update : 'model -> 'msg -> 'model; - view : 'model -> 'msg Vdom.t; -} - - -type ('model, 'msg) pumpInterface = { - startup : unit -> unit; - render_string : 'model -> string; - handleMsg : 'model -> 'msg -> 'model; - shutdown : 'msg Tea_cmd.t -> unit; -} - - -type 'msg programInterface = < - pushMsg : 'msg -> unit; -> Js.t - -external makeProgramInterface : - pushMsg:('msg -> unit) -> - shutdown:(unit -> unit) -> - getHtmlString:(unit -> string) -> - 'msg programInterface = "" [@@bs.obj] - - - -(* TODO: Need to refactor the program layers to layer everything properly, things are a bit mixed up right now... *) - - -(* let programStateWrapper initModel pump = - let model = ref initModel in - let rec handler msg = - let newModel = pump !model msg in - (model := newModel) in - handler *) - -let programStateWrapper initModel pump shutdown = -(* let programStateWrapper : 'model -> ('msg Vdom.applicationCallbacks ref -> 'model -> 'msg) -> ('msg -> unit) = fun initModel pump -> *) -(* let programStateWrapper : 'model -> ('msg Vdom.applicationCallbacks ref -> 'model -> 'msg -> 'model) -> 'msg programInterface = fun initModel pump -> *) - let open Vdom in - let model = ref initModel in - let callbacks = ref { enqueue = fun _msg -> Js.log "INVALID enqueue CALL!" } in - let pumperInterfaceC () = pump callbacks in - let pumperInterface = pumperInterfaceC () in - (* let handler = function - | None -> () - | Some msg -> - let newModel = pumper !model msg in - let () = (model := newModel) in - () in *) - let pending : 'msg list option ref = ref None in - let rec handler msg = - match !pending with - | None -> - let () = pending := Some [] in - (* let () = Js.log ("APP", "mainloop", "pre", !model) in *) - let newModel = pumperInterface.handleMsg !model msg in - (* let () = Js.log ("APP", "mainloop", "post", newModel) in *) - let () = (model := newModel) in - ( match !pending with - | None -> failwith "INVALID message queue state, should never be None during message processing!" - | Some [] -> pending := None - | Some msgs -> - let () = pending := None in - List.iter handler (List.rev msgs) - ) - | Some msgs -> pending := Some (msg :: msgs) in - let finalizedCBs : 'msg Vdom.applicationCallbacks = { - enqueue = fun msg -> handler msg; - } in - let () = (callbacks := finalizedCBs) in - let pi_requestShutdown () = - let () = callbacks := { enqueue = fun _msg -> Js.log "INVALID message enqueued when shut down" } in - let cmd = shutdown !model in - let () = pumperInterface.shutdown cmd in - () in - let render_string () = - let rendered = pumperInterface.render_string !model in - rendered in - let () = pumperInterface.startup () in - makeProgramInterface - ~pushMsg:handler - ~shutdown:pi_requestShutdown - ~getHtmlString:render_string - + val init : flags -> model * msg Tea_cmd.t + val update : model -> msg -> model * msg Tea_cmd.t + val view : model -> msg Vdom.t + val subscriptions : model -> msg Tea_sub.t + val shutdown : model -> msg Tea_cmd.t +end -let programLoop update view subscriptions initModel initCmd = function - | None -> fun callbacks -> - let oldSub = ref Tea_sub.none in - let handleSubscriptionChange model = - (* let open Vdom in *) - let newSub = subscriptions model in - oldSub := (Tea_sub.run callbacks callbacks !oldSub newSub) in - { startup = - ( fun () -> - let () = Tea_cmd.run callbacks initCmd in - let () = handleSubscriptionChange initModel in - () - ) - ; render_string = - ( fun model -> - let vdom = view model in - let rendered = Vdom.renderToHtmlString vdom in - rendered - ) - ; handleMsg = - ( fun model msg -> - let newModel, cmd = update model msg in - (* let open Vdom in *) - let () = Tea_cmd.run callbacks cmd in - let () = handleSubscriptionChange newModel in - newModel - ) - ; shutdown = (fun cmd -> - let () = Tea_cmd.run callbacks cmd in (* TODO: Perhaps add cancelable commands? *) - let () = oldSub := (Tea_sub.run callbacks callbacks !oldSub Tea_sub.none) in - () - ) - } - | Some parentNode -> fun callbacks -> - (* let priorRenderedVdom = ref [view initModel] in *) - let priorRenderedVdom = ref [] in - (* let lastVdom = ref (!priorRenderedVdom) in *) - let latestModel = ref initModel in - let nextFrameID = ref None in - let doRender _delta = - match !nextFrameID with - | None -> () (* The render has been canceled, possibly by shutting down, do nothing *) - | Some _id -> - let newVdom = [view !latestModel] in - let justRenderedVdom = Vdom.patchVNodesIntoElement callbacks parentNode !priorRenderedVdom newVdom in - let () = priorRenderedVdom := justRenderedVdom in - (* let () = Vdom.patchVNodesIntoElement callbacks parentNode !priorRenderedVdom !lastVdom in - let () = priorRenderedVdom := (!lastVdom) in *) - (nextFrameID := None) in - let scheduleRender () = match !nextFrameID with - | Some _ -> () (* A frame is already scheduled, nothing to do *) +module Make( M: Program ) = struct + + type pumpInterface = { + startup : unit -> unit; + render_string : M.model -> string; + handleMsg : M.model -> M.msg -> M.model; + shutdown : M.msg Tea_cmd.t -> unit; + } + + type programInterface = < + pushMsg : M.msg -> unit; + > Js.t + + external makeProgramInterface : + pushMsg:(M.msg -> unit) -> + shutdown:(unit -> unit) -> + getHtmlString:(unit -> string) -> + programInterface = "" [@@bs.obj] + + + let programStateWrapper initModel pump shutdown = + let open Vdom in + let model = ref initModel in + let callbacks = ref { enqueue = fun _msg -> Js.log "INVALID enqueue CALL!" } in + let pumperInterfaceC () = pump callbacks in + let pumperInterface = pumperInterfaceC () in + (* let handler = function + | None -> () + | Some msg -> + let newModel = pumper !model msg in + let () = (model := newModel) in + () in *) + let pending : M.msg list option ref = ref None in + let rec handler msg = + match !pending with | None -> - if false then (* This turns on or off requestAnimationFrame or real-time rendering, false for the benchmark, should be true about everywhere else. *) - let id = Web.Window.requestAnimationFrame doRender in - let () = nextFrameID := Some id in - () - else - let () = nextFrameID := Some (-1) in - doRender 16 in - (* let () = Js.log (Vdom.createVNodeIntoElement callbacks !lastVdom parentNode) in *) - (* We own the passed in node, clear it out TODO: Clear it out properly *) - (* let () = Js.log ("Blah", Web.Node.firstChild parentNode, Js.Null.test (Web.Node.firstChild parentNode), false, true) in *) - let clearPnode () = while (Js.Array.length (Web.Node.childNodes parentNode)) > 0 do - match Js.Null.to_opt (Web.Node.firstChild parentNode) with - | None -> () - | Some firstChild -> let _removedChild = Web.Node.removeChild parentNode firstChild in () - done in - (* let () = Vdom.patchVNodesIntoElement callbacks parentNode [] (!lastVdom) in *) - (* let () = Vdom.patchVNodesIntoElement callbacks parentNode [] (!priorRenderedVdom) in *) - (* Initial render *) - let oldSub = ref Tea_sub.none in - let handleSubscriptionChange model = - (* let open Vdom in *) - let newSub = subscriptions model in - oldSub := (Tea_sub.run callbacks callbacks !oldSub newSub) in - let handlerStartup () = - let () = clearPnode () in - let () = Tea_cmd.run callbacks initCmd in - let () = handleSubscriptionChange !latestModel in - let () = nextFrameID := Some (-1) in - let () = doRender 16 in + let () = pending := Some [] in + (* let () = Js.log ("APP", "mainloop", "pre", !model) in *) + let newModel = pumperInterface.handleMsg !model msg in + (* let () = Js.log ("APP", "mainloop", "post", newModel) in *) + let () = (model := newModel) in + ( match !pending with + | None -> failwith "INVALID message queue state, should never be None during message processing!" + | Some [] -> pending := None + | Some msgs -> + let () = pending := None in + List.iter handler (List.rev msgs) + ) + | Some msgs -> pending := Some (msg :: msgs) in + let finalizedCBs : M.msg Vdom.applicationCallbacks = { + enqueue = fun msg -> handler msg; + } in + let () = (callbacks := finalizedCBs) in + let pi_requestShutdown () = + let () = callbacks := { enqueue = fun _msg -> Js.log "INVALID message enqueued when shut down" } in + let cmd = shutdown !model in + let () = pumperInterface.shutdown cmd in () in - let render_string model = - let vdom = view model in - let rendered = Vdom.renderToHtmlString vdom in + let render_string () = + let rendered = pumperInterface.render_string !model in rendered in - let handler model msg = - let newModel, cmd = update model msg in - let () = latestModel := newModel in - (* let open Vdom in *) - (* let () = Js.log ("APP", "latestModel", "precmd", !latestModel) in *) - let () = Tea_cmd.run callbacks cmd in - (* let () = Js.log ("APP", "latestModel", "postcmd", !latestModel) in *) - (* TODO: Figure out if it is better to get view on update like here, or do it in doRender... *) - (* let newVdom = view newModel in (* Process VDom diffs here with callbacks *) *) - (* let () = Vdom.patchVNodeIntoElement callbacks parentNode !lastVdom newVdom in *) - (* let () = Js.log lastVdom in *) - (* let () = Js.log newVdom in *) - (* let () = Js.log (Vdom.createVNodeIntoElement callbacks newVdom parentNode) in *) - (* let () = lastVdom := [newVdom] in *) - let () = scheduleRender () in - (* let () = Js.log ("APP", "latestModel", "presub", !latestModel) in *) - let () = handleSubscriptionChange newModel in - (* let () = Js.log ("APP", "latestModel", "postsub", !latestModel) in *) - newModel in - let handlerShutdown cmd = - (* let open Vdom in *) - let () = nextFrameID := None in - let () = Tea_cmd.run callbacks cmd in - let () = oldSub := (Tea_sub.run callbacks callbacks !oldSub Tea_sub.none) in - let () = priorRenderedVdom := [] in - let () = clearPnode () in - () in - { startup = handlerStartup - ; render_string = render_string - ; handleMsg = handler - ; shutdown = handlerShutdown - } - - -let program : ('flags, 'model, 'msg) program -> Web.Node.t Js.null_undefined -> 'flags -> 'msg programInterface = - fun {init; update; view; subscriptions; shutdown} pnode flags -> - let () = Web.polyfills () in - let initModel, initCmd = init flags in - let opnode = Js.Null_undefined.to_opt pnode in - let pumpInterface = programLoop update view subscriptions initModel initCmd opnode in - programStateWrapper initModel pumpInterface shutdown - - -let standardProgram : ('flags, 'model, 'msg) standardProgram -> Web.Node.t Js.null_undefined -> 'flags -> 'msg programInterface = - fun {init; update; view; subscriptions} pnode args -> - program { - init = init; - update = update; - view = view; - subscriptions = subscriptions; - shutdown = fun _model -> Tea_cmd.none - } pnode args - - -let beginnerProgram : ('model, 'msg) beginnerProgram -> Web.Node.t Js.null_undefined -> unit -> 'msg programInterface = - fun {model; update; view} pnode () -> - standardProgram { - init = (fun () -> (model, Tea_cmd.none)); - update = (fun model msg -> (update model msg, Tea_cmd.none)); - view = view; - subscriptions = (fun _model -> Tea_sub.none) - } pnode () - - -let map func vnode = - Vdom.map func vnode - -(* let fullProgram program pnode flags = - match Js.Null_undefined.to_opt pnode with - | None -> Web.Document.body () - | Some parentNode -> parentNode *) - -(* class fullProgramClass {internal; init; update; view} pnode flags = object(self) *) -(* class ['msg, 'model] fullProgramClass - (msgHandler : 'model -> 'msg -> 'model * 'msg Tea_cmd.t) - (initModel : 'model) - (initCmd : 'msg Tea_cmd.t) - (view : 'model -> 'msg Vdom.t) - pnode = - object(self) - val mutable model = initModel - val mutable lastView = view initModel - - initializer - Js.log initCmd - - method update (msg : 'msg) = - let (newModel, newCmd) = msgHandler model msg in - model <- newModel; - cmd <- newCmd - end *) - -(* let programStateWrapperInit initModel = - ref initModel - -let programStateWrapper model pump = - let rec handler msg = - let newModel = pump handler !model msg in - (model := newModel) in - handler - - - -let programLoopInit msgHandler view model = function - | None -> None - | Some parentNode -> - let vdom = view model in - let () = Js.log (Vdom.createVNodesIntoElement msgHandler [vdom] parentNode) in - let rvdom = ref vdom in - Some (parentNode, rvdom) - -let programLoop = function - | None -> fun update _view _initModel msgHandler model msg -> - let newModel, _newCmd = update model msg in (* TODO: Process commands to msgHandler *) - newModel - | Some (parentNode, lastVdom) -> fun update view initModel msgHandler -> - let handler model msg = - let newModel, _newCmd = update model msg in (* TODO: Process commands to msgHandler *) - let newVdom = view newModel in (* Process VDom diffs here with msgHandler *) - (* let () = Js.log lastVdom in *) - (* let () = Js.log newVdom in *) - (lastVdom := newVdom); - newModel in - handler - - -let program {init; update; view} pnode flags = - let initModel, initCmd = init flags in - let opnode = Js.Null_undefined.to_opt pnode in - let modelState = programStateWrapperInit initModel in - let rec viewState msgHandler = programLoopInit msgHandler view initModel opnode - and pump_unfixed msgHandler = programLoop viewState update view initModel msgHandler in - (* let rec pump model msg = programLoop opnode update view initModel msgHandler model msg *) - let rec msgHandler msg = programStateWrapper modelState (pump_unfixed msgHandler) msg in - fun msg -> msgHandler msg *) - - - (* new fullProgramClass - update - initModel - initCmds - view - (Js.Null_undefined.to_opt pnode) *) - - (* { - internal = (fun () -> Js.log "internal update"); - init = init; - update = update; - view = view; - } (Js.Null_undefined.to_opt pnode) flags *) - - - (* match Js.Null_undefined.to_opt pnode with - | None -> Web.Document.body () - | Some parentNode -> parentNode *) - - -(* let beginnerProgram program = function - | None -> Js.log 42 - | Some parentNode -> Js.log 84 *) - -(* let beginnerProgram program pnode = match Js.Null_undefined.to_opt pnode with - | None -> Web.Document.body () - | Some node -> node *) - -(* let beginnerPrograms pnode = match Js.Null_undefined.to_opt pnode with - | None -> Web.Document.body () - | Some node -> Web.Node.style node *) - + let () = pumperInterface.startup () in + makeProgramInterface + ~pushMsg:handler + ~shutdown:pi_requestShutdown + ~getHtmlString:render_string + + + let programLoop update view subscriptions initModel initCmd = function + | None -> fun callbacks -> + let oldSub = ref Tea_sub.none in + let handleSubscriptionChange model = + (* let open Vdom in *) + let newSub = subscriptions model in + oldSub := (Tea_sub.run callbacks callbacks !oldSub newSub) in + { startup = + ( fun () -> + let () = Tea_cmd.run callbacks initCmd in + let () = handleSubscriptionChange initModel in + () + ) + ; render_string = + ( fun model -> + let vdom = view model in + let rendered = Vdom.renderToHtmlString vdom in + rendered + ) + ; handleMsg = + ( fun model msg -> + let newModel, cmd = update model msg in + (* let open Vdom in *) + let () = Tea_cmd.run callbacks cmd in + let () = handleSubscriptionChange newModel in + newModel + ) + ; shutdown = (fun cmd -> + let () = Tea_cmd.run callbacks cmd in (* TODO: Perhaps add cancelable commands? *) + let () = oldSub := (Tea_sub.run callbacks callbacks !oldSub Tea_sub.none) in + () + ) + } + | Some parentNode -> fun callbacks -> + (* let priorRenderedVdom = ref [view initModel] in *) + let priorRenderedVdom = ref [] in + (* let lastVdom = ref (!priorRenderedVdom) in *) + let latestModel = ref initModel in + let nextFrameID = ref None in + let doRender _delta = + match !nextFrameID with + | None -> () (* The render has been canceled, possibly by shutting down, do nothing *) + | Some _id -> + let newVdom = [view !latestModel] in + let justRenderedVdom = Vdom.patchVNodesIntoElement callbacks parentNode !priorRenderedVdom newVdom in + let () = priorRenderedVdom := justRenderedVdom in + (* let () = Vdom.patchVNodesIntoElement callbacks parentNode !priorRenderedVdom !lastVdom in + let () = priorRenderedVdom := (!lastVdom) in *) + (nextFrameID := None) in + let scheduleRender () = match !nextFrameID with + | Some _ -> () (* A frame is already scheduled, nothing to do *) + | None -> + if false then (* This turns on or off requestAnimationFrame or real-time rendering, false for the benchmark, should be true about everywhere else. *) + let id = Web.Window.requestAnimationFrame doRender in + let () = nextFrameID := Some id in + () + else + let () = nextFrameID := Some (-1) in + doRender 16 in + (* let () = Js.log (Vdom.createVNodeIntoElement callbacks !lastVdom parentNode) in *) + (* We own the passed in node, clear it out TODO: Clear it out properly *) + (* let () = Js.log ("Blah", Web.Node.firstChild parentNode, Js.Null.test (Web.Node.firstChild parentNode), false, true) in *) + let clearPnode () = while (Js.Array.length (Web.Node.childNodes parentNode)) > 0 do + match Js.Null.to_opt (Web.Node.firstChild parentNode) with + | None -> () + | Some firstChild -> let _removedChild = Web.Node.removeChild parentNode firstChild in () + done in + (* let () = Vdom.patchVNodesIntoElement callbacks parentNode [] (!lastVdom) in *) + (* let () = Vdom.patchVNodesIntoElement callbacks parentNode [] (!priorRenderedVdom) in *) + (* Initial render *) + let oldSub = ref Tea_sub.none in + let handleSubscriptionChange model = + (* let open Vdom in *) + let newSub = subscriptions model in + oldSub := (Tea_sub.run callbacks callbacks !oldSub newSub) in + let handlerStartup () = + let () = clearPnode () in + let () = Tea_cmd.run callbacks initCmd in + let () = handleSubscriptionChange !latestModel in + let () = nextFrameID := Some (-1) in + let () = doRender 16 in + () in + let render_string model = + let vdom = view model in + let rendered = Vdom.renderToHtmlString vdom in + rendered in + let handler model msg = + let newModel, cmd = update model msg in + let () = latestModel := newModel in + (* let open Vdom in *) + (* let () = Js.log ("APP", "latestModel", "precmd", !latestModel) in *) + let () = Tea_cmd.run callbacks cmd in + (* let () = Js.log ("APP", "latestModel", "postcmd", !latestModel) in *) + (* TODO: Figure out if it is better to get view on update like here, or do it in doRender... *) + (* let newVdom = view newModel in (* Process VDom diffs here with callbacks *) *) + (* let () = Vdom.patchVNodeIntoElement callbacks parentNode !lastVdom newVdom in *) + (* let () = Js.log lastVdom in *) + (* let () = Js.log newVdom in *) + (* let () = Js.log (Vdom.createVNodeIntoElement callbacks newVdom parentNode) in *) + (* let () = lastVdom := [newVdom] in *) + let () = scheduleRender () in + (* let () = Js.log ("APP", "latestModel", "presub", !latestModel) in *) + let () = handleSubscriptionChange newModel in + (* let () = Js.log ("APP", "latestModel", "postsub", !latestModel) in *) + newModel in + let handlerShutdown cmd = + (* let open Vdom in *) + let () = nextFrameID := None in + let () = Tea_cmd.run callbacks cmd in + let () = oldSub := (Tea_sub.run callbacks callbacks !oldSub Tea_sub.none) in + let () = priorRenderedVdom := [] in + let () = clearPnode () in + () in + { startup = handlerStartup + ; render_string = render_string + ; handleMsg = handler + ; shutdown = handlerShutdown + } + + + let program : Web.Node.t Js.null_undefined -> M.flags -> programInterface = + fun pnode flags -> + let () = Web.polyfills () in + let initModel, initCmd = M.init flags in + let opnode = Js.Null_undefined.to_opt pnode in + let pumpInterface = programLoop M.update M.view M.subscriptions initModel initCmd opnode in + programStateWrapper initModel pumpInterface M.shutdown + + let map func vnode = + Vdom.map func vnode -(* -module type ProgramState = sig end -module MakeProgram (Prog : Program) : ProgramState = struct - (* module Program = Prog *) +module type StandardProgram = sig + type flags + type model + type msg + val init : flags -> model * msg Tea_cmd.t + val update : model -> msg -> model * msg Tea_cmd.t + val view : model -> msg Vdom.t + val subscriptions : model -> msg Tea_sub.t end -let makeProgram p = - let module P = (val p : Program) in - (module struct - let x = P.init - let y = 42 - end : ProgramState) - - -module type Main = sig +module type BeginnerProgram = sig + type model + type msg + val init : model + val update : model -> msg -> model + val view : model -> msg Vdom.t end -module type App = sig +module OfStandard( M: StandardProgram ) : Program = struct + type flags = M.flags + type msg = M.msg + type model = M.model + let init = M.init + let view = M.view + let update = M.update + let subscriptions = M.subscriptions + let shutdown model = Tea_cmd.none end -(* -module Make (Prog : Program) : App = struct - (* let x = M.x + 1 *) -end *) - -module Make (MainProg : Main) : App = struct - (* let x = M.x + 1 *) +module MakeStandard ( M: StandardProgram ) = Make(OfStandard(M)) + +module OfBeginner ( M: BeginnerProgram ) : Program = struct + type flags = unit + type msg = M.msg + type model = M.model + let init () = ( M.init, Tea_cmd.none ) + let view = M.view + let update model msg = ( M.update model msg , Tea_cmd.none ) + let subscriptions model = Tea_sub.none + let shutdown model = Tea_cmd.none end -(* let programWithFlags (module Prog : Program) = - 42 *) *) +module MakeBeginner (M : BeginnerProgram ) = Make(OfBeginner(M)) + diff --git a/src/tea_html.ml b/src/tea_html.ml index 70f1fd7..c76c033 100644 --- a/src/tea_html.ml +++ b/src/tea_html.ml @@ -1,5 +1,12 @@ +type 'msg t = 'msg Vdom.t +type 'msg prop = 'msg Vdom.property + +type 'msg createNode = ?key:string -> ?unique:string -> + 'msg prop list -> 'msg t list -> 'msg t + open Vdom + module Cmds = Tea_html_cmds (* let map lift vdom = @@ -172,7 +179,7 @@ let onInputOpt ?(key="") msg = (fun ev -> match Js.Undefined.to_opt ev##target with | None -> None - | Some target -> match Js.Undefined.to_opt target##value with + | Some target -> match Js.Undefined.to_opt (Web_node.getValue target) with | None -> None | Some value -> msg value ) @@ -184,7 +191,7 @@ let onChangeOpt ?(key="") msg = (fun ev -> match Js.Undefined.to_opt ev##target with | None -> None - | Some target -> match Js.Undefined.to_opt target##value with + | Some target -> match Js.Undefined.to_opt (Web_node.getValue target) with | None -> None | Some value -> msg value ) @@ -208,7 +215,7 @@ let onCheckOpt ?(key="") msg = (fun ev -> match Js.Undefined.to_opt ev##target with | None -> None - | Some target -> match Js.Undefined.to_opt target##checked with + | Some target -> match Web_node.getChecked target |> Js.Undefined.to_opt with | None -> None | Some value -> msg value ) diff --git a/src/tea_html.mli b/src/tea_html.mli new file mode 100644 index 0000000..46174f4 --- /dev/null +++ b/src/tea_html.mli @@ -0,0 +1,123 @@ +type 'msg t = 'msg Vdom.t +type 'msg prop = 'msg Vdom.property + +(* Nodes *) + +val noNode : 'msg t +val text : string -> 'msg t + +type 'msg createNode = ?key:string -> ?unique:string -> + 'msg prop list -> 'msg t list -> 'msg t + +(* TODO: needed? *) +val node : ?namespace:string -> string -> 'msg createNode + +val lazy1 : string -> ( unit -> 'msg t) -> 'msg t + +(* HTML Elements *) + +val br : 'msg prop list -> 'msg t + +val br' : 'msg createNode +val div : 'msg createNode +val span : 'msg createNode +val p : 'msg createNode +val pre : 'msg createNode +val a : 'msg createNode +val section : 'msg createNode +val header : 'msg createNode +val footer : 'msg createNode +val h1 : 'msg createNode +val h2 : 'msg createNode +val h3 : 'msg createNode +val h4 : 'msg createNode +val h5 : 'msg createNode +val h6 : 'msg createNode +val i : 'msg createNode +val strong : 'msg createNode +val button : 'msg createNode +val input' : 'msg createNode +val textarea : 'msg createNode +val label : 'msg createNode +val ul : 'msg createNode +val ol : 'msg createNode +val li : 'msg createNode +val table : 'msg createNode +val thead : 'msg createNode +val tfoot : 'msg createNode +val tbody : 'msg createNode +val th : 'msg createNode +val tr : 'msg createNode +val td : 'msg createNode +val progress : 'msg createNode +val img : 'msg createNode +val select : 'msg createNode +val option' : 'msg createNode +val form : 'msg createNode +val nav : 'msg createNode +val main : 'msg createNode +val aside : 'msg createNode +val article : 'msg createNode +val details : 'msg createNode +val figcaption : 'msg createNode +val figure : 'msg createNode +val mark : 'msg createNode +val summary : 'msg createNode +val time : 'msg createNode + +(* Properties *) + +val noProp : 'msg prop +val id : string -> 'msg prop + +(* `href` is actually an attribute, not a property, but need it here for Elm compat... *) +val href : string -> 'msg prop + +(* `src` is actually an attribute, not a property, but need it here for Elm compat... *) +val src : string -> 'msg prop + +val class' : string -> 'msg prop +(* val classList : (string * bool) list -> 'msg prop *) +val type' : string -> 'msg prop +val style : string -> string -> 'msg prop +val styles : (string * string) list -> 'msg prop +val placeholder : string -> 'msg prop +val autofocus : bool -> 'msg prop +val value : string -> 'msg prop +val name : string -> 'msg prop +val checked : bool -> 'msg prop +val for' : string -> 'msg prop +val hidden : bool -> 'msg prop +val target : string -> 'msg prop +val action : string -> 'msg prop +val method' : string -> 'msg prop + +(* Events *) + +(* TODO: should this be exposed? *) +val onCB : string -> string -> (Web.Node.event -> 'msg option) -> 'msg prop +val onMsg : string -> 'msg -> 'msg prop + +val onInput : ?key:string -> (string -> 'msg) -> 'msg prop +val onChange : ?key:string -> (string -> 'msg) -> 'msg prop +val onClick : 'msg -> 'msg prop +val onDoubleClick : 'msg -> 'msg prop +val onBlur : 'msg -> 'msg prop +val onFocus : 'msg -> 'msg prop +val onCheck : ?key:string -> (bool -> 'msg) -> 'msg prop +val onMouseDown : 'msg -> 'msg prop +val onMouseUp : 'msg -> 'msg prop +val onMouseEnter : 'msg -> 'msg prop +val onMouseLeave : 'msg -> 'msg prop +val onMouseOver : 'msg -> 'msg prop +val onMouseOut : 'msg -> 'msg prop + +module Attributes : sig + val max : string -> 'msg prop + val min : string -> 'msg prop + val step : string -> 'msg prop + val disabled : bool -> 'msg prop + val selected : bool -> 'msg prop + val acceptCharset : string -> 'msg prop + val rel : string -> 'msg prop +end diff --git a/src/tea_html_cmds.mli b/src/tea_html_cmds.mli new file mode 100644 index 0000000..670e342 --- /dev/null +++ b/src/tea_html_cmds.mli @@ -0,0 +1 @@ +val focus : string -> 'a Tea_cmd.t diff --git a/src/tea_navigation.ml b/src/tea_navigation.ml index 3aab776..05ac02a 100644 --- a/src/tea_navigation.ml +++ b/src/tea_navigation.ml @@ -1,13 +1,3 @@ - -type ('flags, 'model, 'msg) navigationProgram = - { init : 'flags -> Web.Location.location -> 'model * 'msg Tea_cmd.t - ; update : 'model -> 'msg -> 'model * 'msg Tea_cmd.t - ; view : 'model -> 'msg Vdom.t - ; subscriptions : 'model -> 'msg Tea_sub.t - ; shutdown : 'model -> 'msg Tea_cmd.t - } - - let getLocation () = Web.Location.asRecord (Web.Document.location ()) @@ -63,22 +53,34 @@ let newUrl url = () ) - -let navigationProgram locationToMessage stuff = - let init flag = - stuff.init flag (getLocation ()) in - - let subscriptions model = - Tea_sub.batch - [ subscribe locationToMessage - ; stuff.subscriptions model - ] in - - let open! Tea_app in - program - { init = init - ; update = stuff.update - ; view = stuff.view - ; subscriptions = subscriptions - ; shutdown = stuff.shutdown - } +module type NavigationProgram = sig + type flags + type model + type msg + val init : flags -> Web.Location.location -> model * msg Tea_cmd.t + val update : model -> msg -> model * msg Tea_cmd.t + val view : model -> msg Vdom.t + val subscriptions : model -> msg Tea_sub.t + val shutdown : model -> msg Tea_cmd.t + val locationHandler : Web.Location.location -> msg +end + +module ToProgram (M : NavigationProgram) : Tea_app.Program = struct + type msg = M.msg + type model = M.model + type flags = M.flags + let subscriptions model = Tea_sub.batch + [ subscribe M.locationHandler + ; M.subscriptions model + ] + + let init flags = + M.init flags (getLocation ()) + + let update = M.update + let view = M.view + let shutdown = M.shutdown +end + +module MakeNavigationProgram( M: NavigationProgram ) = + Tea_app.Make(ToProgram(M)) diff --git a/src/vdom.mli b/src/vdom.mli new file mode 100644 index 0000000..b5155e3 --- /dev/null +++ b/src/vdom.mli @@ -0,0 +1,109 @@ +(* https://github.com/Matt-Esch/virtual-dom/blob/master/docs/vnode.md *) + +type 'msg applicationCallbacks = { + enqueue : 'msg -> unit; +} + +(* Attributes are not properties *) +(* https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes *) + +type 'msg eventHandler = + | EventHandlerCallback of string * (Web.Node.event -> 'msg option) + | EventHandlerMsg of 'msg + +type 'msg eventCache = + { handler : Web.Node.event_cb + ; cb : (Web.Node.event -> 'msg option) ref + } + +type 'msg property = + | NoProp + | RawProp of string * string (* TODO: This last string needs to be made something more generic, maybe a function... *) + (* Attribute (namespace, key, value) *) + | Attribute of string * string * string + | Data of string * string + (* Event (name, userkey, callback) *) + | Event of string * 'msg eventHandler * 'msg eventCache option ref + (* | Event of string * (Web.Event.t -> 'msg) *) + | Style of (string * string) list + +type 'msg properties = 'msg property list + +type 'msg t = + | CommentNode of string + | Text of string + (* Node (namespace, tagName, key, unique, properties, children) *) + | Node of string * string * string * string * 'msg properties * 'msg t list + (* | ArrayNode of string * string * string * string * 'msg property array * 'msg t array *) + (* LazyGen (key, fnGenerator) *) + | LazyGen of string * (unit -> 'msg t) * 'msg t ref + (* Tagger (toString, toDom, toVNodes) *) +(* | Tagger of (unit -> string) * ('msg applicationCallbacks ref -> Web.Node.t -> Web.Node.t -> int -> 'msg t list -> Web.Node.t) * (unit -> 'msg t) *) + (* Tagger (tagger, vdom) *) + | Tagger of ('msg applicationCallbacks ref -> 'msg applicationCallbacks ref) * 'msg t + (* *) + (* | Tagger of (('a -> 'msg) -> 'a t -> 'msg t) *) + (* Custom (key, cbAdd, cbRemove, cbChange, properties, children) *) + (* | Custom of string * (unit -> Web.Node.t) * (Web.Node.t -> unit) * *) + + +(* Nodes *) + +val noNode : 'msg t + +val comment : string -> 'msg t + +val text : string -> 'msg t + +val fullnode : string -> string -> string -> string + -> 'msg properties -> 'msg t list -> 'msg t + +val node : ?namespace:string -> string -> ?key:string -> ?unique:string + -> 'msg properties -> 'msg t list -> 'msg t + +val lazyGen : string -> (unit -> 'msg t) -> 'msg t + +(* Properties *) + +val noProp : 'msg property + +val prop : string -> string -> 'msg property + +val onCB : string -> string -> (Web.Node.event -> 'msg option) -> + 'msg property + +val onMsg : string -> 'msg -> 'msg property + +val attribute : string -> string -> string -> 'msg property + +val data : string -> string -> 'msg property + +val style : string -> string -> 'msg property + +val styles : (string * string) list -> 'msg property + +(* Accessors *) + +val renderToHtmlString : 'msg t -> string + +(* Patching / Diffing *) + +val eventHandler_Register : + 'msg applicationCallbacks ref -> + Web.Node.t -> + string -> + 'msg eventHandler -> + 'msg eventCache option + +val eventHandler_Unregister : Web.Node.t -> string + -> 'msg eventCache option -> 'msg option + + +val patchVNodesIntoElement : 'msg applicationCallbacks ref -> + Web.Node.t -> 'msg t list -> 'msg t list -> 'msg t list + +(* TODO: rename mapCallbacks *) +val wrapCallbacks : ('a -> 'b) -> 'b applicationCallbacks ref -> + 'a applicationCallbacks ref + +val map : ('a -> 'b) -> 'a t -> 'b t diff --git a/src/web_node.ml b/src/web_node.ml index a8ec9d4..8013e91 100644 --- a/src/web_node.ml +++ b/src/web_node.ml @@ -35,6 +35,8 @@ type event = t Web_event.t type event_cb = t Web_event.cb +let getValue t = t##value +let getChecked t = t##checked external getProp_asEventListener : t -> 'key -> t Web_event.cb Js.undefined = "" [@@bs.get_index] @@ -66,7 +68,7 @@ let removeChild n child = n##removeChild child let insertBefore n child refNode = n##insertBefore child refNode -let remove n child = n##remove child +let remove n = n##remove () let setAttributeNS n namespace key value = n##setAttributeNS namespace key value diff --git a/src/web_node.mli b/src/web_node.mli new file mode 100644 index 0000000..1cca090 --- /dev/null +++ b/src/web_node.mli @@ -0,0 +1,65 @@ +type style + +val getStyle : style -> string -> string Js.null + +val setStyle : style -> string -> string Js.null -> unit + +type t + +val document_node : t + +type event = t Web_event.t + +type event_cb = t Web_event.cb + +val getProp_asEventListener : t -> 'key -> t Web_event.cb Js.undefined + +val setProp_asEventListener : t -> 'key -> t Web_event.cb Js.undefined -> + unit + +val getValue : t -> string Js.Undefined.t +val getChecked : t -> bool Js.Undefined.t + +val getProp : t -> 'key -> 'value + +val setProp : t -> 'key -> 'value -> unit + +val style : t -> style + +val getStyle : t -> string -> string Js.null + +val setStyle : t -> string -> string Js.null -> unit + +val setStyleProperty: t -> ?priority:bool -> string -> + string Js.null -> unit + +val childNodes: t -> t Js.Array.t +val firstChild: t -> t Js.Null.t +val appendChild: t -> t -> t +val removeChild: t -> t -> t +val insertBefore: t -> t -> t -> t +val remove: t -> unit + +val setAttributeNS : t -> string -> string -> string -> unit +val setAttribute : t -> string -> string -> unit +val setAttributeNsOptional : t -> string -> string -> string -> unit +val removeAttributeNS : t -> string -> string -> unit +val removeAttribute : t -> string -> unit +val removeAttributeNsOptional : t -> string -> string -> unit + +val addEventListener : t -> string -> t Web_event.cb -> + Web_event.options -> unit + +val removeEventListener : t -> string -> t Web_event.cb -> + Web_event.options -> unit + +val focus : t -> unit + +(* Text Nodes only *) + +val set_nodeValue: t -> string -> unit +val get_nodeValue: t -> string Js.Null.t + + +(* Polyfills *) +val remove_polyfill : unit -> unit diff --git a/test/test_client_attribute_removal.ml b/test/test_client_attribute_removal.ml index 917fb2b..5aa291b 100644 --- a/test/test_client_attribute_removal.ml +++ b/test/test_client_attribute_removal.ml @@ -1,63 +1,62 @@ -open Tea.App open Tea.Html -type model = { - selected: string option; - languages: string list -} - -type message = - | Select of string - | Delete -[@@bs.deriving {accessors}] +module App = struct + type model = { + selected: string option; + languages: string list + } -let render_selected = function - | Some selected -> - div [] - [ text ("you selected " ^ selected) - ; div [onClick Delete] [text "delete selection"]] - | None -> div [] [text "Nothing selected"] - -(* let lang l is_selected = - * let baseProps = [onClick (Select l); style "color" "blue"] in - * let props = if is_selected == true then (style "border" "1px solid black")::baseProps else baseProps - * in - * li props [text l] *) - -let lang l is_selected = - li - [ onClick (Select l) - ; style "color" "blue" - ; if is_selected then style "border" "1px solid black" else noProp - ; if is_selected then Vdom.attribute "" "lang" l else noProp - ] - [ text l ] - -let render_languages selected languages = - let is_selected selected language = - match selected with - | Some l -> language == l - | None -> false - in - let rendered = List.map (fun l -> lang l (is_selected selected l)) languages in - ul [] rendered - -let update state = function - | Select lang -> { state with selected = Some lang} - | Delete -> { state with selected = None } - -let view state = - div [] - [ render_selected state.selected - ; render_languages state.selected state.languages] - -let main = - let initialState = { + let init = { selected = Some "Erlang"; languages = ["Erlang"; "Ocaml"; "Clojure"] - } in - beginnerProgram { - model = initialState; - update; - view; } + + type msg = + | Select of string + | Delete + [@@bs.deriving {accessors}] + + let render_selected = function + | Some selected -> + div [] + [ text ("you selected " ^ selected) + ; div [onClick Delete] [text "delete selection"]] + | None -> div [] [text "Nothing selected"] + + (* let lang l is_selected = + * let baseProps = [onClick (Select l); style "color" "blue"] in + * let props = if is_selected == true then (style "border" "1px solid black")::baseProps else baseProps + * in + * li props [text l] *) + + let lang l is_selected = + li + [ onClick (Select l) + ; style "color" "blue" + ; if is_selected then style "border" "1px solid black" else noProp + ; if is_selected then Vdom.attribute "" "lang" l else noProp + ] + [ text l ] + + let render_languages selected languages = + let is_selected selected language = + match selected with + | Some l -> language == l + | None -> false + in + let rendered = List.map (fun l -> lang l (is_selected selected l)) languages in + ul [] rendered + + let update state = function + | Select lang -> { state with selected = Some lang} + | Delete -> { state with selected = None } + + let view state = + div [] + [ render_selected state.selected + ; render_languages state.selected state.languages] + +end + +module P = Tea.App.MakeBeginner(App) +let main = P.program diff --git a/test/test_client_btn_update_span.ml b/test/test_client_btn_update_span.ml index cb0578e..3ed0636 100644 --- a/test/test_client_btn_update_span.ml +++ b/test/test_client_btn_update_span.ml @@ -1,34 +1,31 @@ -open Tea.App open Tea.Html -type msg = - | Trigger -[@@bs.deriving {accessors}] +module BeginnerApp = struct + type msg = + | Trigger + [@@bs.deriving {accessors}] -type model = (string option * string option) + type model = (string option * string option) -let update' model = function - | Trigger -> - let (left, _) = model in - (left, Some "right") + let update model = function + | Trigger -> + let (left, _) = model in + (left, Some "right") -let render_model = function - | (Some _, Some _) -> - input' [value "This should be on screen"] [] - | _ -> - span [] [text "nothing"] + let render_model = function + | (Some _, Some _) -> + input' [value "This should be on screen"] [] + | _ -> + span [] [text "nothing"] -let view' model = - div [] - [ button [onClick Trigger] [text "trigger rerender"] - ; render_model model - ] + let view model = + div [] + [ button [onClick Trigger] [text "trigger rerender"] + ; render_model model + ] + let init = (Some "left", None); +end - -let main = - beginnerProgram { - model = (Some "left", None); - update = update'; - view = view' - } +module P = Tea.App.MakeBeginner(BeginnerApp) +let main = P.program diff --git a/test/test_client_counter.ml b/test/test_client_counter.ml index 9d8c613..40e7d9b 100644 --- a/test/test_client_counter.ml +++ b/test/test_client_counter.ml @@ -1,51 +1,51 @@ -open Tea.App open Tea.Html -type msg = - | Increment - | Decrement - | Reset - | Set of int - -let update model = function - | Increment -> model + 1 - | Decrement -> model - 1 - | Reset -> 0 - | Set v -> v - - -let view_button title msg = - button - [ onClick msg - ] - [ text title - ] - -let view model = - div - [] - [ span - [ style "text-weight" "bold" ] - [ text (string_of_int model) ] - ; br [] - ; view_button "Increment" ( - if model >= 3 then - Decrement - else - Increment - ) - ; br [] - ; view_button "Decrement" Decrement - ; br [] - ; view_button "Set to 42" (Set 42) - ; br [] - ; if model <> 0 then view_button "Reset" Reset else noNode - ] - - -let main = - beginnerProgram { - model = 4; - update; - view; - } +module BeginnerApp = struct + type model = int + + type msg = + | Increment + | Decrement + | Reset + | Set of int + + let update model = function + | Increment -> model + 1 + | Decrement -> model - 1 + | Reset -> 0 + | Set v -> v + + + let view_button title msg = + button + [ onClick msg + ] + [ text title + ] + + let view model = + div + [] + [ span + [ style "text-weight" "bold" ] + [ text (string_of_int model) ] + ; br [] + ; view_button "Increment" ( + if model >= 3 then + Decrement + else + Increment + ) + ; br [] + ; view_button "Decrement" Decrement + ; br [] + ; view_button "Set to 42" (Set 42) + ; br [] + ; if model <> 0 then view_button "Reset" Reset else noNode + ] + + let init = 4 +end + +module P = Tea.App.MakeBeginner(BeginnerApp) +let main = P.program diff --git a/test/test_client_drag.ml b/test/test_client_drag.ml index 767b04a..13f68e0 100644 --- a/test/test_client_drag.ml +++ b/test/test_client_drag.ml @@ -1,110 +1,108 @@ -open Tea -open Tea.App -open Tea.Html -open Tea.Mouse +module App = struct + open Tea + open Tea.Html + open Tea.Mouse -type msg = - | DragStart of position - | DragAt of position - | DragEnd of position -[@@bs.deriving {accessors}] + type msg = + | DragStart of position + | DragAt of position + | DragEnd of position + [@@bs.deriving {accessors}] -type drag = - { start : position - ; current : position - } + type drag = + { start : position + ; current : position + } -type model = - { position : position - ; drag : drag option - } + type model = + { position : position + ; drag : drag option + } -let init () = - ( {position = {x = 200; y = 200}; drag = None}, Cmd.none ) + let init () = + ( {position = {x = 200; y = 200}; drag = None}, Cmd.none ) -let getPosition {position; drag} = - match drag with - | None -> - position + let getPosition {position; drag} = + match drag with + | None -> + position - | Some {start; current} -> - { x = position.x + current.x - start.x - ; y = position.y + current.y - start.y - } + | Some {start; current} -> + { x = position.x + current.x - start.x + ; y = position.y + current.y - start.y + } -let updateHelp ({position} as model) = function - | DragStart xy -> - { position - ; drag = Some {start = xy; current = xy} - } + let updateHelp ({position} as model) = function + | DragStart xy -> + { position + ; drag = Some {start = xy; current = xy} + } - | DragAt xy -> - { position - ; drag = match model.drag with - | None -> None - | Some drag -> Some {drag with current = xy} - } + | DragAt xy -> + { position + ; drag = match model.drag with + | None -> None + | Some drag -> Some {drag with current = xy} + } - | DragEnd _ -> - { position = getPosition model - ; drag = None - } + | DragEnd _ -> + { position = getPosition model + ; drag = None + } + + + let update model msg = + ( updateHelp model msg, Cmd.none ) + + + let subscriptions model = + match model.drag with + | None -> + Sub.none + + | Some _ -> + Sub.batch [ Mouse.moves dragAt; Mouse.ups dragEnd ] + + + let px number = + (string_of_int number) ^ "px" + + let onMouseDown = + onCB "mousedown" "" (fun ev -> + Json.Decoder.decodeEvent (Json.Decoder.map dragStart Mouse.position) ev + |> Result.result_to_option + ) + + let view model = + let realPosition = getPosition model in + div + [ onMouseDown + ; styles + [ "background-color", "#3C8D2F" + ; "cursor", "move" + + ; "width", "100px" + ; "height", "100px" + ; "border-radius", "4px" + ; "position", "absolute" + ; "left", px realPosition.x + ; "top", px realPosition.y + + ; "color", "white" + ; "display", "flex" + ; "align-items", "center" + ; "justify-content", "center" + ] + ] + [ text "Drag Me!" + ] + + type flags = unit +end -let update model msg = - ( updateHelp model msg, Cmd.none ) - - -let subscriptions model = - match model.drag with - | None -> - Sub.none - - | Some _ -> - Sub.batch [ Mouse.moves dragAt; Mouse.ups dragEnd ] - - -let px number = - (string_of_int number) ^ "px" - -let onMouseDown = - onCB "mousedown" "" (fun ev -> - Json.Decoder.decodeEvent (Json.Decoder.map dragStart Mouse.position) ev - |> Result.result_to_option - ) - -let view model = - let realPosition = getPosition model in - div - [ onMouseDown - ; styles - [ "background-color", "#3C8D2F" - ; "cursor", "move" - - ; "width", "100px" - ; "height", "100px" - ; "border-radius", "4px" - ; "position", "absolute" - ; "left", px realPosition.x - ; "top", px realPosition.y - - ; "color", "white" - ; "display", "flex" - ; "align-items", "center" - ; "justify-content", "center" - ] - ] - [ text "Drag Me!" - ] - - -let main = - standardProgram { - init; - update; - view; - subscriptions; - } +module P = Tea.App.MakeStandard(App) +let main = P.program