diff --git a/CHANGES.md b/CHANGES.md index d8db6872..c75692b0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,31 @@ +- Added a `duplicate` button for elements within auto-generated list forms. + +- Added a function to resize prt colum widths. + +- Add `Bonsai.Value.of_opt` to new `Bonsai.Cont` api. + +- Migrated bonsai examples to new `Bonsai.Cont` API. + +- Add documentation about changes and upgrade strategies between proc and cont + +- Add 3 new APIs to close Bonsai notifications: + * `close_all_notifications` closes all currently open notifications + * `close_oldest_notification` closes the oldest currently open notification + * `close_newest_notification` closes the newest currently open notification + +- Rename `Bonsai.Cont.yoink` to `Bonsai.Cont.peek` to mirror the peek operation in +other abstract data types + +- Made the `close_when_clicked_outside` argument to `Bonsai_web_ui_popover` dynamic. + +- Changed Bonsai path generation so that paths are only extended at branch points where +multple children might reference the paths. + +- Balance long chains of Sub nodes to prevent stack overflows and suboptimal +linear chains of incremental model values. + +- Added effects to lock and unlock focus in the `Bonsai_web_ui_partial_render_table` + - `Bonsai_web_ui_form` and `Bonsai_web_ui_form2` were merged into `Bonsai_web_ui_form` under submodules `With_automatic_view` and `With_manual_view`, respectively. diff --git a/README.md b/README.md index 0b643238..bec59a36 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,6 @@ # Bonsai - Bonsai is a library for building interactive browser-based UI. -The [Getting Started with Bonsai](./docs/getting_started/index.md) -guide is good if you're new to web development entirely or just want to see a -walkthrough of a couple simple example apps. - -Examples of using Bonsai in a web browser can be found in the `examples` -directory. - -## Docs - Documentation can be found in the [docs](./docs) directory, and API documentation can be found in [src/bonsai.mli](./src/bonsai.mli). diff --git a/bindings/dygraph/src/default_legend.ml b/bindings/dygraph/src/default_legend.ml index e5545f1b..b4dbfef3 100644 --- a/bindings/dygraph/src/default_legend.ml +++ b/bindings/dygraph/src/default_legend.ml @@ -5,6 +5,7 @@ module Model = struct module Series = struct type t = { label : string + ; override_label_for_visibility : string option ; value : Raw_html.t option ; dash : Raw_html.t option ; color : string option @@ -15,7 +16,21 @@ module Model = struct let toggle_visibility t = { t with is_visible = not t.is_visible } - let view { label; value; dash; color; is_visible; is_highlighted } ~on_toggle = + let label_for_visibility t = + Option.value t.override_label_for_visibility ~default:t.label + ;; + + let view + { label + ; override_label_for_visibility = _ + ; value + ; dash + ; color + ; is_visible + ; is_highlighted + } + ~on_toggle + = let dash = match dash with | None -> Vdom.Node.none @@ -64,12 +79,12 @@ module Model = struct { x_label : string ; x_value : Raw_html.t option ; series : Series.t list - ; past_series : Series.t Map.M(String).t + ; past_series_visibility : bool Map.M(String).t } [@@deriving equal, sexp] let view - { x_label; x_value; series; past_series = _ } + { x_label; x_value; series; past_series_visibility = _ } ~on_toggle ~select_all ~select_none @@ -109,7 +124,8 @@ module Model = struct select_all_or_none :: x :: List.map series ~f:(fun series -> - Series.view series ~on_toggle:(fun () -> on_toggle series.label)) + Series.view series ~on_toggle:(fun () -> + on_toggle (Series.label_for_visibility series))) in (* Mostly copied from vdom_input_widgets *) Vdom.Node.div @@ -127,7 +143,7 @@ end module Action = struct type t = | From_graph of Legend_data.t - | Toggle_visibility of string + | Toggle_visibility of { label_for_visibility : string } | Select_none | Select_all [@@deriving equal, sexp] @@ -179,15 +195,18 @@ let apply_action { model with x_value; series } | Select_none -> map_series ~f:(fun series -> { series with is_visible = false }) | Select_all -> map_series ~f:(fun series -> { series with is_visible = true }) - | Toggle_visibility label -> + | Toggle_visibility { label_for_visibility } -> map_series ~f:(fun series -> - if String.(series.label = label) + if String.(Model.Series.label_for_visibility series = label_for_visibility) then Model.Series.toggle_visibility series else series) ;; -let series_from_info { Per_series_info.label; visible_by_default } = +let series_from_info + { Per_series_info.label; override_label_for_visibility; visible_by_default } + = { Model.Series.label + ; override_label_for_visibility ; is_visible = visible_by_default ; is_highlighted = false ; value = None @@ -207,7 +226,7 @@ let create ~x_label ~per_series_info { Model.x_label ; x_value = None ; series = List.map per_series_info ~f:series_from_info - ; past_series = String.Map.empty + ; past_series_visibility = String.Map.empty } | Some (model : Model.t) -> let existing_y_labels = List.map model.series ~f:Model.Series.label in @@ -216,24 +235,40 @@ let create ~x_label ~per_series_info && [%equal: string list] model_y_labels existing_y_labels then { model with x_label } else ( - let past_series = - (* Every time the [model_y_labels] changes, we want to remember the visibility - status of all the series labels we know about so far. This will help in the - case where we toggle visibility on series A, flip to a graph which does not - have that series, and then flip back to the original graph. Without - remembering, the visibility status of series A revert back to the default - status. *) - List.fold ~init:model.past_series model.series ~f:(fun past_series series -> - Map.set past_series ~key:series.label ~data:series) + (* Every time the [model_y_labels] changes, we want to remember the visibility + status of all the series labels we know about so far. This will help in the + case where we toggle visibility on series A, flip to a graph which does not + have that series, and then flip back to the original graph. Without + remembering, the visibility status of series A revert back to the default + status. *) + let past_series_visibility = + List.fold + ~init:model.past_series_visibility + model.series + ~f:(fun past_series_visibility series -> + Map.set + past_series_visibility + ~key:(Model.Series.label_for_visibility series) + ~data:series.is_visible) in let series = List.map per_series_info ~f:(fun per_series_info -> - let { Per_series_info.label; visible_by_default = _ } = per_series_info in - match Map.find past_series label with - | None -> series_from_info per_series_info - | Some series -> series) + let { Per_series_info.label + ; override_label_for_visibility + ; visible_by_default = _ + } + = + per_series_info + in + let series = series_from_info per_series_info in + let label_for_visibility = + Option.value override_label_for_visibility ~default:label + in + match Map.find past_series_visibility label_for_visibility with + | None -> series + | Some is_visible -> { series with is_visible }) in - { model with x_label; series; past_series }) + { model with x_label; series; past_series_visibility }) in let%sub state = Bonsai_extra.state_machine0_dynamic_model @@ -250,7 +285,8 @@ let create ~x_label ~per_series_info let view = Model.view model - ~on_toggle:(fun label -> inject_action (Toggle_visibility label)) + ~on_toggle:(fun label_for_visibility -> + inject_action (Toggle_visibility { label_for_visibility })) ~select_all:(fun () -> inject_action Select_all) ~select_none:(fun () -> inject_action Select_none) in diff --git a/bindings/dygraph/src/default_legend.mli b/bindings/dygraph/src/default_legend.mli index 8a3301f9..6f1da8c6 100644 --- a/bindings/dygraph/src/default_legend.mli +++ b/bindings/dygraph/src/default_legend.mli @@ -13,6 +13,7 @@ module Model : sig module Series : sig type t = { label : string + ; override_label_for_visibility : string option ; value : Raw_html.t option ; dash : Raw_html.t option ; color : string option @@ -20,17 +21,19 @@ module Model : sig ; is_highlighted : bool } [@@deriving equal, fields ~getters, sexp] + + val label_for_visibility : t -> string end type t = { x_label : string ; x_value : Raw_html.t option ; series : Series.t list - ; past_series : Series.t Map.M(String).t - (** [past_series] remembers all the series (by series label) that we've ever seen. - This means that if someone makes a change to a particular series (e.g. toggles - visibility), moves to a graph without that series, and then moves back to the - original graph, the information will not be lost. + ; past_series_visibility : bool Map.M(String).t + (** [past_series_visibility] remembers all the series (by [label_for_visibility]) that + we've ever seen. This means that if someone makes a change to a particular series + (e.g. toggles visibility), moves to a graph without that series, and then moves + back to the original graph, the information will not be lost. This may sound like a memory leak, and it kind of is, but the hope is that the total number of unique series labels that one sees over the lifetime of a graph is @@ -42,7 +45,7 @@ end module Action : sig type t = | From_graph of Legend_data.t - | Toggle_visibility of string + | Toggle_visibility of { label_for_visibility : string } | Select_none | Select_all [@@deriving equal, sexp] diff --git a/bindings/dygraph/src/per_series_info.ml b/bindings/dygraph/src/per_series_info.ml index e01ceb73..5e63a304 100644 --- a/bindings/dygraph/src/per_series_info.ml +++ b/bindings/dygraph/src/per_series_info.ml @@ -3,10 +3,16 @@ open! Import type t = { label : string + ; override_label_for_visibility : string option ; visible_by_default : bool } [@@deriving fields ~getters] +let create ?override_label_for_visibility label ~visible_by_default = + { label; override_label_for_visibility; visible_by_default } +;; + let create_all_visible labels = - List.map labels ~f:(fun label -> { label; visible_by_default = true }) + List.map labels ~f:(fun label -> + { label; override_label_for_visibility = None; visible_by_default = true }) ;; diff --git a/bindings/dygraph/src/per_series_info.mli b/bindings/dygraph/src/per_series_info.mli index cda48516..d6f35ed7 100644 --- a/bindings/dygraph/src/per_series_info.mli +++ b/bindings/dygraph/src/per_series_info.mli @@ -3,8 +3,25 @@ open! Import type t = { label : string + ; override_label_for_visibility : string option + (** It may be helpful to distinguish the series label from the "label for visibility". + + For example, in some graphs we encode information about the symbol we are looking at + in the series label. That information changes as you look at different symbols. + However, the second series always semantically means the same thing as you move from + symbol to symbol. If you uncheck the second series to disable visibility, you may + want to remember that change even if the series label changes. + + If you want to just use the [label] as the semantic identifier for persisting + visilibity, then just set [override_label_for_visibility] to None. *) ; visible_by_default : bool } [@@deriving fields ~getters] +val create + : ?override_label_for_visibility:string + -> string + -> visible_by_default:bool + -> t + val create_all_visible : string list -> t list diff --git a/changelog.md b/changelog.md index 6cfdf2ea..94ba41e1 100644 --- a/changelog.md +++ b/changelog.md @@ -134,7 +134,7 @@ degree than otherwise available. ## 2020-06-10 `Bonsai.Proc` module added. To read more, check out -[this document](./docs/blogs/proc.md). +[this document](./docs/blog/proc.md). ## 2020-03-17 - Model type removed from `('input, 'model, 'result) Bonsai.t`. diff --git a/docs/advanced/how_bonsai_works.md b/docs/advanced/how_bonsai_works.md new file mode 100644 index 00000000..f68f01d8 --- /dev/null +++ b/docs/advanced/how_bonsai_works.md @@ -0,0 +1,411 @@ +How Bonsai Works + +This article talks about how Bonsai works. It is probably most useful to +Bonsai maintainers, although it can provide useful context for power +users as well. We'll cover: + +- How Bonsai's internals work +- How the `local_ graph` API works + +```{=html} + +``` +# `Computation.t` and `Value.t` + +One of Bonsai's invariants is that the computation graph is a static +DAG. This offers [a lot of benefits](./why_no_bind.mdx). + +Bonsai's internal representation of this graph is based around 2 types. + +`Value.t` is effectively a wrapper around Incremental's `Incr.t`, and +ultimately gets compiled down to `Incr.t`s. + +`Computation.t` represents the "structure" of the computation graph. +It's implemented as a variant of types such as: + +- `Leaf0` and `Leaf1`: structure around state nodes +- `Store` and `Fetch`: powers Bonsai's `Dynamic_scope` tools +- `Assoc`: allows creating a dynamic number of copies of some internal + `Computation.t`, each with their own state / lifecycle. +- `Switch`: allows selecting one of several `Computation.t`s to + evaluate. +- `Path`: returns the path to a new node in the computation graph, + which is useful as a unique ID. +- `Lifecycle`: allows running `on_activate` / `on_deactivate` / + `after_display` lifecycle events +- `Return`: a very important one: just return an incremental + `Value.t`. +- `Sub`: possibly the most important one: allows a computation to use + the output of **any other** computation in the graph. This is what + makes the computation graph a DAG, and not a tree. + +## How do `Computation.t` and `Value.t` interact? + +Many of the variants of `Computation.t` are records, that store some +`Value.t`. For example, `Switch` stores an `int Value.t` representing +which of the `Computation.t`s is currently active. And `Assoc` stores an +input `Map.t`. Of particular importance, `Return` stores some arbitrary +`Value.t`. + +## How was the graph constructed before? + +Up until late 2023, Bonsai's public-facing API directly constructed a +`Computation.t` graph, which provides structure for `Value.t` +incremental calculations. Let's go over the key parts. + +### sub + +`val sub: 'a Computation.t -> f:('a Value.t -> 'b Computation.t) -> 'b Computation.t`. +This was commonly used via the `let%sub` syntax. It: + +- Creates a new "named" `Value.t`, which essentially reads + `'a Computation.t`. +- Applies the provided `f` function to that value, generating a new + `'b Computation.t`. +- Creates a new `Sub` node for the graph, which contains these 2 + `Computation.t`s. + +Essentially, this allows us to compute a `'a Value.t`, use it in 2 +separate computations, and then `map` them together. + +If we only had a tree, we would have to clone that `'a Value.t` for each +of the 2 subcomputations. We would have to redo all calculations twice, +and we couldn't share state. + +But with `sub`, we essentially create a symlink / reference / pointer to +the shared `'a Value.t`, so we can use it in multiple places without +issue. This is why `Sub` is so important. + +### arr + +`val arr : 'a Value.t -> f:('a -> 'b) -> 'b Computation.t`. There are +also `arr2`, `arr3` versions, which combine several `Value.t`s into one +`Computation.t`. + +This was commonly used via the `let%arr` syntax. All `arr` does is call +`Value.map`, and wrap the result in a `Return` node. + +The entire point of `arr` is to force you to use `sub`, so that our new +`Value.t` is defined in terms of references/symlinks to any shared work, +and we don't accidentially clone that work / lose shared state. + +### And the rest? + +We've talked about the `Sub` node via `sub`, and how new `Value.t`s get +created (and then wrapped in `Return`) via `arr`. What about the other +`Computation.t`s? + +This is where all the `Bonsai.state_machine`, `Bonsai.Dynamic_scope.*`, +`Bonsai.assoc`, etc primitives come in; they create `Leaf0`, +`Fetch`/`Store`, `Assoc`, and other nodes. Then, using `sub` on those +nodes adds them into the graph. + +## Ok, but how do we get an `Incr.t` from all of this? + +Before Bonsai boots up your web UI, it performs a compilation pass. +First, it recursively "evaluates" your computation graph, resulting in a +single `Computation.info` record. + +```{=html} + +``` +### What is Computation.info + +For every `Computation.t`, we recursively create this +`Computation.info`, which includes: + +- `'model Meta.Model.t`, which contains the default value / type id + structure for the `Computation`. + - For many `Computation.t`s, such as `Path`, `Lifecycle`, and + `Return`, this is just `Meta.Model.unit`. + - For the "state" nodes `Leaf0` and `Leaf1`, this is the + user-provided model metadata. + - For nodes defined in terms of other `Computation.t`s, such as + `Sub`, `Assoc`, `Switch`, and `Store`, we recursively combine + the `Meta.Model.t` of subcomputations. +- `'input Meta.Input.t`, which contains a type id for inputs to the + `Computation.t`. For `Leaf1`, which is a state machine with input, + this is the user-provided input metadata. For recursive + `Computation.t`s, this combines inputs of subcomputations. + Otherwise, it's `Meta.Input.unit`. +- `'action Action.id`, which is a type id for a state machine's + `Action.t`, is assembled similarly. +- As is `reset`, which is a `'model -> 'model` function used to + implement `with_model_resetter`. +- And so is `apply_action`, which users provide when creating state + machines, and is the state machine transition, with the added + ability to dispatch events. + +### `run` and `Snapshot.t` + +The final, and most interesting part of `Computation.info` is `run`, +which takes: + +- An "environment", which contains all "named" `Value.t`s memoized via + `sub`, andthe available dynamically scoped variables. +- A `Time_source.t` +- The current computation path +- An `Incr.t` with the current state +- An `inject` function, which creates `Effect.t`s that apply + `'action`s + +and outputs a `Snapshot.t`. which represents the state of the +computation, and all its subcomputations. + +`Snapshot.t` packages an `Input.t`, which is all `Incr.t` (and empty +`unit`) inputs to the computation and its subcomputations, some +lifecycle state, and a result, which is an `Incr.t` with the dynamic +value produced by the computation. + +So essentially, `run` is the "logic" of the `Computation.t`. Let's +discuss a few examples: + +For `Return`, `run` creates a trivial snapshot with no input and no +lifecycle. It calls `Value.eval` on its internal `Value.t`, and uses +that for the `Snapshot.t`'s result. `Value.eval` is provided the +"environment", so that `Value.t`s built in terms of "named" +symlink/reference/pointer `Value.t`s can be evaluated. + +For `Leaf0`, `run` is similar, except that it returns the current state +`Incr.t` and the `inject` function for `result`. `Leaf1` is almost the +same, except that it uses the `input : 'input Value.t` field of its +`Computation.t` node for the `Snapshot.t`'s input. + +`Store`'s `run` is probably the simplest out of computations with +subcomputations; it adds whatever dynamically scoped value it is storing +into the environment, then calls the inner computation's `run` with this +new environment. + +For the rest, read the implementation in +[bonsai/src/eval](https://github.com/janestreet/bonsai/blob/master/src/eval.ml). + +### So where do models + actions live? + +The top-level `Computation.info` for our whole web UI now contains a +`'model Meta.Model.t`, which represents the structure + default values +of the state of everything. We can use that to create a +`'model Incr.Var.t`, which will be a single variable tracking the state +of our whole web UI! + +It's shaped roughly like the `Computation.t` graph itself: every `Sub` +is a tuple of the `'model` of its `from` and the `'model` of its `into`; +assocs are `Map.t`s of the inner's `'model`, and leaf `'model`s are what +you'd expect. + +There are similar tree-like structures storing the `apply_action` and +`reset` functions for everything. And the "actual" representation of an +action is a path in that tree. + +You can think of `gather` as a pass up the tree, which collects the +structure of everything and assembles these whole-app state/action/etc +trees, and a second pass down the tree, which provides `Incr.t` +accessors for recursive sub-parts of those trees. + +### And then what? + +The private `bonsai.driver` library uses `eval` to construct this +`Computation.info`, instantiates the environment, default model, +`Effect.t` queue, and some other things, calls `run` on the top-level +computation, extracts the `result Incr.t` and `input Incr.t` from the +resulting snapshot, and puts these all together in a `Bonsai_driver.t` +type that provides an API for running the Bonsai computation. + +This driver is mostly used for testing. + +### Testing??? I want a website! + +`Bonsai_web.Start` pretty much does the same thing, except that it does +some web specific stuff, and expects a `Vdom.Node.t Computation.t`. Once +it has a `Vdom.Node.t Incr.t`, it uses `virtual_dom` to generate DOM +from the vdom / attach it to some element, and then run `virtual_dom`'s +diff-and-patch algorithm when the `Incr.t` changes. + +`Bonsai_web.Start` actually mostly does this through +`Incr_dom.Start_app`. + +# The `local_ graph` API + +In late 2023, we released a new implementation of Bonsai's API, called +`Cont`. That's because it is implemented in the continuation-passing +style. + +As covered above, in the previous API, calls to `sub`, `arr`, and +`Bonsai.*` primitives directly created `Computation.t` nodes (and the +underlying `Value.t`s). + +The new API doesn't actually change the internals! As before, +`Computation.t` is the static structure of the computation graph, and +`Value.t` is a wrapper around `Incr.t`, adding support for +"reference/symlink" values, which enable `sub` and work-sharing, and a +bunch of other stuff. + +In the new API, we have one `Bonsai.t` type, which is actually just +`Value.t`. Instead of `Computation.t`s, we pass around an +abstractly-typed `Bonsai.graph` value. This `graph` value is used to +construct Bonsai's computation graph, as we'll discuss in a bit. + +## Why do we need `graph`, and what is `local_`? + +There are two "phases" for Bonsai apps: + +1. "`Computation.t` building", where we build up the static computation + graph. +2. "Runtime", which starts when `Bonsai_web.Start.start` is run. At + this point, the Computation.t templates are instantiated and flush + with data. Runtime code is all in functions provided to + `Bonsai.map`; `let%arr` hides this somewhat, but the pure OCaml + "contents" of `let%arr` calls are actually de-sugared to anonymous + functions. + +Once "runtime" starts, you can no longer do any `Computation.t` +building; there aren't any `Computation.t`s left to build! It's all just +one big `Incr.t`! And in fact, Bonsai's type signatures force separation +of "computation building" code and "runtime" code, even though all this +code lives together in the same files and functions. + +But with a new API, where there's just one `Bonsai.t` type, how do we +enforce this separation? What's stopping us from referring to `graph` in +runtime code, inside of some `let%arr`, and blowing up our application? + +The new [local\_ +mode](https://blog.janestreet.com/oxidizing-ocaml-locality/) gives us a +new way to have the type-checker guarantee the separation of phases! To +dramatically oversimplify `local_`: when a function annotates an +argument with `local_`, it's promising not to close over it, partially +apply it to any variables, put it in a ref, or otherwise stash it away. + +Because all our runtime code is put in closures, use of `local_` means +that we just can't use `graph`! And so, we get a "phase witness" +property in `graph` through `local_`. + +As a result, Bonsai can enforce the same invariants as before, but +provide a simpler API and provide even more power and flexibility to UI +authors! + +But what's going on under the surface? And what is `graph`? And why can +the new Bonsai API use `Bonsai.map`? + +## How `local_ graph` Works + +### Background on `Sub` + +Recall that the `sub` is the memoization-like pattern that powers +work-sharing for incrementality: we can store a reusable value with a +unique name (via Type_equal.id), which downstream incremental `Value.t`s +can reference (and reuse!) instead of repeating work. The "Sub" +`Computation.t` node has 3 fields: + +- `from` is the computation being memoized +- `via` is the `Type_equal.id` identifier +- `into` is the downstream computation that uses the value of `from`. + +Let's say we want to use the output of `comp1` and `comp2` (both +`Computation.t`s) in another computation called +`thing_we_want_to_compute`. This is what the corresponding computation +graph looks like: + +```{=html} + +``` + Sub { from=comp1; via=; into = + Sub { from=comp2; via=; into = + ... into = thing_we_want_to_compute ... + } + } + +### What is `graph`, and how is `Cont` implemented? + +The new `Cont` implementation has a very primitive API (not exposed to +end-users), which simulates algebraic effects: + +- `perform` takes a `local_ graph -> Computation.t`, evaluates it, and + adds it into the computation graph. It returns a `Value.t` memoized + alias to this new computation. +- `handle` places a `Value.t` in the context of a fresh computation + graph. It then wraps the result in a `Computation.t`. This is + generally used to construct a portion of the computation graph in + isolation. The subgraph is then added to the main graph via + `perform`. + +`graph` is the mechanism for composing / combining `Computation.t`s via +`Sub`. It is implemented as a ref holding a +`Computation.t -> Computation.t` function. `graph` starts as `Fn.id`. At +each invocation of `perform`, we overwrite it to still return its input, +but nested in a new layer of `Sub`. + +```{=html} + +``` +Let's look at how this works in practice. The top part is the code, and +the bottom is the value of `graph` after every line: + +```{=html} + +``` + 1.let computation graph = + 2. let model1, _inject = Bonsai.state 5 graph in + 3. let model2, _inject = Bonsai.state 6 graph in + 4. model2 + + --- + + 1. graph = { f = fun r -> r } + 2. graph = { f = fun r -> Sub { from=state 5; via=; into=r } } + 3. graph = { f = fun r -> Sub { from=state 5; via=; into=Sub { from=state 6; via=; into=r } } } + 4. graph.f () ==> Sub { from=state 5; via=; into=Sub { from=state 6; via=; into= } } + +Instead of `state 5/6`, we'd actually have something more like +`Leaf0 { ... }`, but that's not really relevant. We also do some +optimizations if `sub`ing isn't actually necessary. + +Handle is much simpler: we run our `Value.t` construction function (`f`) +on a fresh `graph`, which becomes constructed via calls to `perform` +presumably inside `f`. Then, we call `graph.f` on that result, so that +it is inside the nested `Sub`s, giving it access to those memoized +aliases. Finally, we wrap this in a `Computation.t`, which can be +`perform`ed into other computations. + +```{=html} + +``` +A consequence of `perform`'s implementation is that the shape of +`Computation.t` under the Cont API is very linear: with the exception of +`Switch`/`Assoc` nodes, it is a chain of `Sub`s, each with more-or-less +a single node on the `From` side. You can think of this as a list of all +the things that have been instantiated and memoized, in the order of +instantiation. + +### Why is `Bonsai.map` safe now? + +But why can we now use `Bonsai.map` instead of `let%sub` and `let%arr` +combos? After all, we're not passing `graph` to `let%map`... How could +it construct `Sub` nodes and add them into the graph? + +In short, We cheat and make `graph` global. We still require users to +pass it when possible, so that creating costly stateful nodes is an +explicit operation. diff --git a/docs/advanced/readme.md b/docs/advanced/readme.md new file mode 100644 index 00000000..52e0e9db --- /dev/null +++ b/docs/advanced/readme.md @@ -0,0 +1,3 @@ +# Bonsai Docs: \[Advanced Topics\] + +Learn how to do advanced things in Bonsai diff --git a/docs/advanced/why_no_bind.md b/docs/advanced/why_no_bind.md new file mode 100644 index 00000000..5f0b3efe --- /dev/null +++ b/docs/advanced/why_no_bind.md @@ -0,0 +1,49 @@ +Why No Bind? + +Many have asked the question "`Bonsai.t` is an Applicative, why isn't it +a Monad?" Certainly, for the users of a library, having a Monad gives +the user much more power. For example, take a look at this code using +monadic bind: + +```{=html} + +``` +``` ocaml +val x: bool t +val y: 'a t +val z: 'a t + +val bind: 'a t -> f:('a -> 'b t) -> 'b t + +bind x ~f:(fun x -> if x then y else z) +``` + +Look at that, we've built a `'a t` that dynamically chooses between +either `y` or `z` depending on the value of `x`. Try to do the same with +a type that is only an Applicative! (Do not try to do the same with an +Applicative, it is impossible.) + +The dynamism on display is quite useful, but sadly, it's dynamism that +would hurt Bonsai if `Value.t` were a Monad. It would prevent +"whole-program analysis" of the app, which would make some nice Bonsai +features impossible: + +1. Bonsai runs an optimization pass on the program, resulting in a + seriously condensed incremental graph. If an app were to dynamically + generate bonsai values, we would potentially have to run the + optimization many times a second, slowing things down considerably. +2. We can attach instrumentation for debugging to nodes in the + computation graph, or even add specialized "debugging" nodes. +3. With `bind`-based dynamism, it would become difficult to reason + about the "stateful" bits of Bonsai with models. If a `match%sub` + node were to toggle between arms, state of the inactive subtrees + would be lost. + +And from a performance perspective, `bind` would likely make Bonsai apps +considerably slower: dynamism is fairly slow in Incremental, the library +that Bonsai is built on top of. + +In practice, preventing Bonsai.t from implementing the Monad interface +hasn't blocked much: using the `match%sub` and `Bonsai.assoc` +primitives, pretty much anything you'd want to do with `bind` is +possible, and we get to keep all the benefits listed above. diff --git a/docs/blogs/history.md b/docs/blog/history.md similarity index 100% rename from docs/blogs/history.md rename to docs/blog/history.md diff --git a/docs/blogs/proc.md b/docs/blog/proc.md similarity index 100% rename from docs/blogs/proc.md rename to docs/blog/proc.md diff --git a/docs/blog/readme.md b/docs/blog/readme.md new file mode 100644 index 00000000..1eb983b0 --- /dev/null +++ b/docs/blog/readme.md @@ -0,0 +1,4 @@ +# Bonsai Docs: \[Blog Posts\] + +A collection of blog posts about Bonsai, mostly focusing on the history +of its API. diff --git a/docs/blogs/letsub.md b/docs/blogs/letsub.md deleted file mode 100644 index 4c640fa8..00000000 --- a/docs/blogs/letsub.md +++ /dev/null @@ -1,546 +0,0 @@ -```{=html} - -``` -... and I want you to as well. - -Although the goal of this post is to explain `let%sub` *in Bonsai*, I -think I'll do better by explaining the same idea in the context of a -much simpler example. - -Let's say we're building a tiny little language for doing simple -arithmetic: - -```{=html} - -``` -``` ocaml -type t = - | Const of int - | Add of t * t - | Map of - { t : t - ; f : int -> int - } - -let const i = Const i -let add x y = Add (x, y) -let map t ~f = Map { t; f } - -let rec eval : t -> int = function - | Const i -> i - | Add (x, y) -> eval x + eval y - | Map { t; f } -> f (eval t) -;; -``` - -Now we use it to compute something like so: - -```{=html} - -``` -``` ocaml -let print_and_return i = - printf "got here\n"; - i -;; - -let x = map (add (const 5) (const 1)) ~f:(fun i -> print_and_return (i / 2)) -let doubled = map x ~f:(fun i -> i * 2) -let tripled = map x ~f:(fun i -> i * 3) -let y = add doubled tripled -let () = printf "y = %d\n" (eval y) -``` - -**Here's the question:** how many times does "got here" print? Put -another way, how many times do we evaluate `x`? - -I don't think anyone will be too surprised in this case that the answer -is two. But still, let's break it down. Let's first visualize the -computatation that `x` represents: - -``{=html} - -Each box represents a `t`. We start with two `const` boxes that we -create from hard-coded ints. We combine them using an `add`. Finally, we -map over that. - -Now what do you think `y` looks like? Does it look like this? - -``{=html} - -or this? - -``{=html} - -As much as you might *want* it to look like the first, it doesn't. It -looks like the second. If you use `x` twice, you're duplicating the -entire computation of `x`. **There is no sharing.** - -# How to add sharing - -So how can we extend the language to enable us to share computation? -There's more than one way. - -## Eager evaluation - -The obvious way in our completely trivial language is to just eagerly -evaluate the ints as we construct them, but I claim that's cheating. In -Bonsai, often the values depend on user action and cannot, even in -principle, be eagerly computed. So what other options do we have? - -## Mutability - -To me, this feels a lot like trying to improve the performance of the -following function - -``` ocaml -let rec fib n = - match n with - | 0 | 1 -> n - | n -> fib (n - 1) + fib (n - 2) -``` - -This function suffers the same problem as `y` - we recompute the exact -same values over and over again. How do we solve the problem? -Memoization! We cache previously computed values so that if we're asked -to compute the same thing again, we just return the cached value. - -Let's apply this idea to our language. A lot of this is repeat. The -interesting change is that we add a new function `cache` which takes a -`t` and returns a new `t` that's really just a mutable version of that -`t`. The mutability allows us to replace the unevaluated `t` with an -evaluated int the first time we compute it. - -```{=html} - -``` -``` ocaml - type t = - | Const of int - | Add of t * t - | Map of - { t : t - ; f : int -> int - } - | Cached of { mutable value : [ `Not_yet_evaluated of t | `Evaluated of int ] } - - let const i = Const i - let add x y = Add (x, y) - let map t ~f = Map { t; f } - let cache t = Cached { value = `Not_yet_evaluated t } - - let eval t = - let rec eval = function - | Const i -> i - | Add (x, y) -> eval x + eval y - | Map { t; f } -> f (eval t) - | Cached cached_value -> - (match cached_value.value with - | `Evaluated i -> i - | `Not_yet_evaluated t -> - let i = eval t in - cached_value.value <- `Evaluated i; - i) - in - eval t - ;; -``` - -And here's a test that shows that by caching `x` we now only print "got -here" once, even though we mapped on `x` twice: - -```{=html} - -``` -``` ocaml - let%expect_test "" = - let x = map (add (const 5) (const 1)) ~f:(fun i -> print_and_return (i / 2)) in - let x = cache x in - let doubled = map x ~f:(fun i -> i * 2) in - let tripled = map x ~f:(fun i -> i * 3) in - let y = add doubled tripled in - printf "y = %d\n" (eval y); - [%expect - {| - got here - y = 15 - |}] - ;; -``` - -### By the way, this is how deferred and incremental work - -In the first section, I claimed that `map` does not share work. If you -map on a thing twice, you compute it twice. - -This may have come as a pretty big surprise to you (it did for me) -because I'm accustomed to using things that absolutely do not work that -way. Incremental is one case, but the more common case is deferred. -Consider the following snipped of code: - -`ocaml skip let x : unit Deferred.t = Clock.after (sec 1.) >>| fun () -> print_endline "x" in let y = x >>| fun () -> print_endline "y" in let z = x >>| fun () -> print_endline "z" in let%map () = Deferred.all_unit [x;y;z] in` - -Will "x" get printed once or twice? Once! So wait... we mapped on a -thing twice and it got computed once. What gives? - -Deferred uses mutability - like our `Cached` variant - to share -computation between multiple maps. So let me revise my claims from -above: *without mutability* `map` does not share work. - -For example, haskell has no mutability and therefore "x" will definitely -print twice in the following code: - - main :: IO () - main = do - let x = print "x" - y = x >> print "y" - z = x >> print "z" - y - z - -## Sub - -Bonsai doesn't use mutability to achieve sharing - it uses `sub`. How -does that work? To see, let's add `sub` to our language: - -```{=html} - -``` -``` ocaml - module Uid = Unique_id.Int () - - type t = - | Const of int - | Precomputed_value of Uid.t - | Add of t * t - | Map of - { t : t - ; f : int -> int - } - | Sub of - { thing_to_precompute : t - ; name : Uid.t - ; body_that_uses_precomputed_thing : t - } - - let const i = Const i - let add x y = Add (x, y) - let map t ~f = Map { t; f } - - let sub t ~f = - let name = Uid.create () in - Sub - { thing_to_precompute = t - ; name - ; body_that_uses_precomputed_thing = f (Precomputed_value name) - } - ;; - - let eval t = - let rec eval ~scope = function - | Const i -> i - | Precomputed_value name -> Map.find_exn scope name - | Add (x, y) -> eval ~scope x + eval ~scope y - | Map { t; f } -> f (eval ~scope t) - | Sub { thing_to_precompute; name; body_that_uses_precomputed_thing } -> - let i = eval ~scope thing_to_precompute in - let scope = Map.set scope ~key:name ~data:i in - eval ~scope body_that_uses_precomputed_thing - in - eval ~scope:Uid.Map.empty t - ;; - - let%expect_test "" = - let x = map (add (const 5) (const 1)) ~f:(fun i -> print_and_return (i / 2)) in - let y = - sub x ~f:(fun x -> - let doubled = map x ~f:(fun i -> i * 2) in - let tripled = map x ~f:(fun i -> i * 3) in - add doubled tripled) - in - printf "y = %d\n" (eval y); - [%expect - {| - got here - y = 15 - |}] - ;; -``` - -How does `sub` work? It takes an expression `thing_to_precompute` and a -function which wants to use that expression while only evaluating it -once. The trick is that instead of passing `thing_to_precompute` -directly to the function, it instead mints a unique name for that -expression and passes `(Precomputed_value name)` to `f`. - -Later when evaluating the `sub` expression, we evaluate -`thing_to_precompute` once and then store the evaluated int in a map -called `scope`, using the key `name`. Then we evaluate the body which, -remember, doesn't refer directly to `thing_to_precompute` but instead on -`Precomputed_value name`. Every time the body needs to find the value of -`thing_to_precompute`, it just looks up the value for `name` in `scope`. - -QED! - -### A bit of polishing - -From a conceptual standpoint, the previous section really does explain -how `sub` works. But you might still wonder why Bonsai has such an -unusual signature for `sub`: - -`ocaml skip val sub : 'a Computation.t -> f:('a Value.t -> 'b Computation.t) -> 'b Computation.t` - -Why do we need these two types? The real answer is we don't *need* them, -but it makes things a little cleaner. - -Notice that in the last version of our language (the one with sub), the -`f` in `sub t ~f` always gets a version of `t` that is constructed via -the `Precomputed_value of Uid.t` constructor. Can we encode that fact in -the types? - -Sure - let's give that case its own type: - -```{=html} - -``` -``` ocaml - type precomputed_value = Uid.t - - type t = - | Const of int - | Add of t * t - | Arr of - { value : precomputed_value - ; f : int -> int - } - | Sub of - { bound : t - ; name : Uid.t - ; body : t - } - - let const i = Const i - let add x y = Add (x, y) - let arr value ~f = Arr { value; f } - - let sub t ~f = - let name = Uid.create () in - Sub { bound = t; name; body = f name } - ;; -``` - -Now what's the type of sub? - -`ocaml skip val sub : t -> f:(value -> t) -> t` - -Huh... that's starting to look a lot like Bonsai's version of sub. And -if you want to see what `eval` looks like now, it's really not very -different: - -```{=html} - -``` -``` ocaml - let eval t = - let rec eval ~scope = function - | Const i -> i - | Add (x, y) -> eval ~scope x + eval ~scope y - | Arr { value; f } -> - let i = Map.find_exn scope value in - f i - | Sub { bound; name; body } -> - let i = eval ~scope bound in - let scope = Map.set scope ~key:name ~data:i in - eval ~scope body - in - eval ~scope:Uid.Map.empty t - ;; -``` - -You might notice that I snuck in one last change, which is that I -removed `map` completely and replaced it with `arr`. The important -difference is that `map` mapped over a `t` whereas `arr` maps over a -`precomputed_value`. What does that mean from a practical perspective? -It means that `arr` is only allowed to map over precomputed values. In -other words, mapping over a non-precomputed thing twice won't even -compile. If you want to map, first `sub` to get a precomputed -`precomputed_value` and then `arr` to map over that. - -Here it is in action: - -```{=html} - -``` -``` ocaml - let%expect_test "" = - let x = - sub - (add (const 5) (const 1)) - ~f:(fun sum -> arr sum ~f:(fun i -> print_and_return (i / 2))) - in - let y = - sub x ~f:(fun x -> - let doubled = arr x ~f:(fun i -> i * 2) in - let tripled = arr x ~f:(fun i -> i * 3) in - add doubled tripled) - in - printf "y = %d\n" (eval y); - [%expect - {| - got here - y = 15 - |}] - ;; -``` - -### Why does Bonsai have map then? - -If `map` let's you do "a bad thing", why does Bonsai have it? Historical -accident. If Bonsai devs could remove it, they would. - -## Applicatives vs. Arrows - -If the above was just way to easy to understand, I can make it a bit -more abstract for you. Everything we've talked about so far is -essentially a case study in understanding applicatives vs. arrows. - -An applicative is something that satisfies this interface: - -```{=html} - -``` -``` ocaml -module type Applicative = sig - type 'a t - - val return : 'a -> 'a t - val map : 'a t -> f:('a -> 'b) -> 'b t - val both : 'a t -> 'b t -> ('a * 'b) t -end -``` - -or in picture form: - -``{=html} - -Note: this isn't the typical way to describe an applicative but it's -equivalent to the typical version and it's way easier to understand. Our -simple arithmetic language is actually an applicative in disguise with -`const` and `add` serving as `return` and `both`. - -What types of computation graphs can you represent with an applicative? -They key is to realize that two branches can be combined via `both` but -a single branch can never split back into two parallel branches. - -In other words, assuming your graph is connected left to right, you can -see a shape that looks something like this: - -``{=html} - -but never this: - -``{=html} - -But the latter shape is often what we want! In particular, when we want -to compute a single value and reuse it in two different later -computations (without necessarily using mutability). - -This ability to share a value with two later computations is exactly -what arrow gives you on top of applicative. - -```{=html} - -``` -``` ocaml -module type Arrow = sig - type ('a, 'b) t - - val arr : ('a -> 'b) -> ('a, 'b) t - - (* Combine two arrows in series *) - val compose : ('a, 'b) t -> ('b, 'c) t -> ('a, 'c) t - - (* Combine two arrows in parallel *) - val ( &&& ) : ('a, 'b1) t -> ('a, 'b2) t -> ('a, 'b1 * 'b2) t -end -``` - -``{=html} - -If you're willing to squint past the fact that an arrow type has two -type parameters and the oh-so-descriptive name of `&&&`, you can see -that `&&&` gives us the exact shape that we were missing. It lets us -compute some value `'a` and then pass it to two later computations, in -effect **sharing the value of `'a`**. - -It's no coincidence that Bonsai *is an arrow*. It might not expose an -interface that looks like `module type Arrow`, but it's actually -equivalent to that interface. The key is realizing that the following -type definitions is the right one: - -`ocaml skip type ('a, 'b) t = 'a Value.t -> 'b Computation.t` - -Using that as your primary arrow type, it's not hard to define `arr`, -`compose`, and `(&&&)` using Bonsai's `let%sub` and `let%arr`: - -```{=html} - -``` -``` ocaml -module Arrow_from_bonsai : sig - type ('a, 'b) t = 'a Value.t -> 'b Computation.t - - val arr : ('a -> 'b) -> ('a, 'b) t - val compose : ('a, 'b) t -> ('b, 'c) t -> ('a, 'c) t - val ( &&& ) : ('a, 'b1) t -> ('a, 'b2) t -> ('a, 'b1 * 'b2) t -end = struct - type ('a, 'b) t = 'a Value.t -> 'b Computation.t - - let arr f value = - let%arr value = value in - f value - ;; - - let compose t1 t2 value = - let%sub result = t1 value in - t2 result - ;; - - let ( &&& ) t1 t2 value = - let%sub result1 = t1 value in - let%sub result2 = t2 value in - let%arr result1 = result1 - and result2 = result2 in - result1, result2 - ;; -end -``` - -## tl;dr: Can you summarize? - -Sure, here are my main takeaways: - -- `let%sub` enables sharing. You can compute a value once and use it - in multiple sub-computations. - -- Avoid `let%map`. If you limit yourself to only using `let%sub` and - `let%arr` you won't be able to accidentally duplicate work. - -- Thinking of `type ('a, 'b) t = 'a Value.t -> 'b Computation.t` as - the "real" bonsai type (in some sense) was a huge conceptual - simplification for me. For some reason, wrapping my head around what - a `Value` *is* and what a `Computation` *is* was quite hard. - However, it's immediately obvious how values of `type ('a, 'b) t` - can be composed into arbitrarily large and complicated graphs, and - being able to visualize the graph makes it more clear what I'm - doing. That said, actually operating on that type is awkward for - reasons outlined [in this post](./proc.md), which is why we have - `Value` and `Computation`. diff --git a/docs/blogs/rpc_effect.md b/docs/blogs/rpc_effect.md deleted file mode 100644 index a792b841..00000000 --- a/docs/blogs/rpc_effect.md +++ /dev/null @@ -1,293 +0,0 @@ -Historically, Bonsai and Async haven't gotten along too well. Because -Bonsai only works with pure programs and immutable values, the -side-effectful and mutable nature of Async operations meant that the -best architecture for web apps involved splitting the program into "pure -bonsai" and "impure async" worlds. These two worlds were then connected -by using specific APIs, like `Effect.of_deferred_fun` (which turns an -Deferred-returning function into an Effect-returning function) and -`Bonsai.Var.t` (which is used to track live data and inject it into -Bonsai) - -The split was certainly annoying, but it did come with a side-benefit: -pure Bonsai components are really easy to test because mocking out -communication with the outside world is really easy. - -This year the team has been busy making Bonsai powerful enough to -robustly deal with mutable values and impure functions, culminating in -`Bonsai_web.Rpc_effect`, a module exposing first-class Bonsai components -for performing RPCs, tracking their responses, and monitoring the status -of your connections. - -Highlights of the [module's -mli](https://ocaml.org/p/bonsai/v0.16.0/doc/Bonsai_web/Rpc_effect/index.html) -include: - -```{=html} - -``` -- **Boilerplate-free:** Connection information is passed implicitly - through the Bonsai computation graph, which means you don't need to - thread rpc-sending effect parameters (or a `Connection.t`) through - your whole app. -- **Powerful combinators:** `Polling_state_rpc.poll` turns a polling - state-RPC into a Bonsai component which automatically fetches new - data when an input `'query Value.t` changes (and on a timer so that - the server can forward updates). -- **No compromises for testability:** Testing components that use this - module is as easy as providing implementations for any of the RPCs - invoked during each test. If your server's RPC implementations are - in a `js_of_ocaml`-compatible library, you could even use those for - tests. -- **Helpful utilities:** `Status.state` is a computation for tracking - whether the UI is connected to its server. If it has been - disconnected, it also tracks how long ago it was last connected. - -# Migrating an existing app - -Suppose you have a Bonsai app whose main computation has the following -type: - -``` ocaml -val app - : counters:int Map.M(String).t Value.t - -> send_increment_rpc:(int -> unit Or_error.t Effect.t) - -> send_decrement_rpc:(int -> unit Or_error.t Effect.t) - -> Vdom.Node.t Computation.t -``` - -To migrate this signature to use the `Rpc_effect` module, follow these -steps. The diffs below include just the essense of the migration; merely -making these changes will yield type errors that should be -straightforward enough to address. - -## Step 1 - Move all RPC dispatches into the main Bonsai computation. - -The goal in this step is to remove all the parameters to `app`. - -``` diff - let app -- ~counters - ~send_increment_rpc - ~send_decrement_rpc - = -+ let%sub counters = -+ Rpc_effect.Polling_state_rpc.poll -+ (module Unit) -+ (module Counter_state) -+ Protocol.Counter_state.t -+ ~where_to_connect:Self -+ ~every:(Time_ns.Span.of_ms 16.0) -+ (Value.return ()) -+ in -``` - -The new `counters` variable has a different type than the old one, since -`Rpc_effect.Polling_state_rpc.poll` has extra cases for handling when no -response has yes been received or when the last response was an error. - -``` diff - let app -- ~send_increment_rpc -- ~send_decrement_rpc - = -+ let%sub send_decrement_rpc = -+ Rpc_effect.Rpc.dispatcher Protocol.Decrement_request.t ~where_to_connect:Self -+ in -+ let%sub send_increment_rpc = -+ Rpc_effect.Rpc.dispatcher Protocol.Increment_request.t ~where_to_connect:Self -+ in -``` - -## Step 2 - Delete the old connection logic. - -The `Rpc_effect` module creates its own connection for sending RPCs to -`Self`. Thus, we should get rid of the old logic that connects and sends -RPC connections. While most of the diff below consists of removing code -made unused by previous changes, we include it all to demonstrate how -much untestable code we get to remove. In addition, we also now get an -opportunity to replace the `eprint_s` call below with a better error -handling mechanism, such as giving the user a notification that they -aren't connected to the server. - -``` diff --let counters conn = -- let client = Polling_state_rpc.Client.create Protocol.Counter_state.t in -- let%map.Deferred.Or_error first_response = -- let%bind conn = Rpc_connection.connected conn in -- Polling_state_rpc.Client.dispatch client conn () -- in -- let world_state_var = Bonsai.Var.create first_response in -- Async_kernel.Clock_ns.every' -- ~continue_on_error:true -- (Time_ns.Span.of_sec (1.0 /. 30.0)) -- (fun () -> -- match%map -- let%bind conn = Rpc_connection.connected conn in -- Polling_state_rpc.Client.dispatch client conn () -- with -- | Ok () -> () -- | Error error -> -- eprint_s [%message "Failed to poll counter state" (error : Error.t)]); -- let (_ : _ Bus.Subscriber.t) = -- Bus.subscribe_exn (Polling_state_rpc.Client.bus client) [%here] ~f:(fun _ -> -- Bonsai.Var.set world_state_var) -- in -- Bonsai.Var.value world_state_var --;; -- --let send_increment_rpc conn = -- Effect.of_deferred_fun (fun how_much -> -- let%bind conn = Rpc_connection.connected conn in -- Rpc.Rpc.dispatch Protocol.Increment_request.t conn how_much) --;; -- --let send_decrement_rpc conn = -- Effect.of_deferred_fun (fun how_much -> -- let%bind conn = Rpc_connection.connected conn in -- Rpc.Rpc.dispatch Protocol.Increment_request.t conn how_much) --;; - - let run () = - Async_js.init (); -- let conn = -- Rpc_connection.create -- ~server_name:"ws-server" -- ~connect:(fun () -> Rpc.Connection.client ()) -- ~address:(module Unit) -- Deferred.Or_error.return -- in -- let%bind counters = counters conn |> Deferred.Or_error.ok_exn in -- let counters = Bonsai.Value.map counters ~f:Counter_state.counters in -- let (_ : (unit, Nothing.t) Start.Handle.t) = -- Start.start -- Start.Result_spec.just_the_view -- ~bind_to_element_with_id:"app" -- (App.app -- ~counters -- ~send_increment_rpc:(send_increment_rpc conn) -- ~send_decrement_rpc:(send_decrement_rpc conn)) -+ let (_ : (unit, Nothing.t) Start.Handle.t) = -+ Bonsai_web.Start.start Start.Result_spec.just_the_view ~bind_to_element_with_id:"app" App.app - in - Deferred.never () -``` - -## Step 3 - Provide RPC implementations for tests - -Since `Rpc_effect` causes tests to use `Async` machinery, we have to -include this line near the top of each test file. - -``` ocaml -open! Async_js_test -``` - -The main changes to test code include: - -- Pass RPC implementations to `Handle.create` instead of passing - effect implementations to the app computation. -- Use `yield_until_no_jobs_remain` to flush the async scheduler so - that side-effects happen at the time you expect. -- Use `return ()` at the end of your test, since the test uses async. - -``` diff - let%expect_test "Click on the buttons" = - let handle = - Handle.create -- (Result_spec.vdom Fn.id) -- (App.app -- ~counters:(Value.return String.Map.empty) -- ~send_increment_rpc:(fun how_much -> -- let%map.Effect () = Effect.print_s [%message "incremented" (how_much : int)] in -- Ok ()) -- ~send_decrement_rpc:(fun how_much -> -- let%map.Effect () = Effect.print_s [%message "decremented" (how_much : int)] in -- Ok ())) -+ ~rpc_implementations: -+ [ Rpc.Rpc.implement' Protocol.Increment_request.t (fun _state how_much -> -+ print_s [%message "incremented" (how_much : int)]) -+ ; Rpc.Rpc.implement' Protocol.Decrement_request.t (fun _state how_much -> -+ print_s [%message "decremented" (how_much : int)]) -+ ] -+ (Result_spec.vdom Fn.id) -+ App.app - in - Handle.click_on handle ~get_vdom:Fn.id ~selector:"button.increment"; -+ let%bind () = Async_kernel_scheduler.yield_until_no_jobs_remain () in - Handle.click_on handle ~get_vdom:Fn.id ~selector:"button.decrement"; -+ let%bind () = Async_kernel_scheduler.yield_until_no_jobs_remain () in - [%expect {| - (incremented (how_much 1)) -- (decremented (how_much 1)) |}] -+ (decremented (how_much 1)) |}]; -+ return () - ;; -``` - -# Unconventional apps - -The assumption built into the steps above is that your app uses a single -`Persistent_connection` to the host server through which all RPCs are -sent. We have also assumed that there is no special connection logic. - -For apps that don't fit this pattern, the usage of `Rpc_effect` is -mostly the same, except that you have to specify the connection -yourself, rather than relying on the one created by `Rpc_effect` itself. - -## Step 1 - Extend the `Where_to_connect.Custom` variant. - -This variant case is the constructor used by the code that dispatches -the RPC and also the code that provides the connection (as you'll see -later). - -``` ocaml -type Rpc_effect.Where_to_connect.Custom.t += Conn -``` - -## Step 2 - Use the new variant case whenever dispatching an RPC - -In the previous example, we used `Self` for all the `~where_to_connect` -parameters, but since we're providing our own connection, we use -`Custom Conn`. - -``` diff - let%sub send_decrement_rpc = -- Rpc_effect.Rpc.dispatcher Protocol.Decrement_request.t ~where_to_connect:Self -+ Rpc_effect.Rpc.dispatcher Protocol.Decrement_request.t ~where_to_connect:(Custom Conn) - in - let%sub send_decrement_rpc = -- Rpc_effect.Rpc.dispatcher Protocol.Decrement_request.t ~where_to_connect:Self -+ Rpc_effect.Rpc.dispatcher Protocol.Decrement_request.t ~where_to_connect:(Custom Conn) - in -``` - -## Step 3 - Provide your connection using a `Connector.t` - -The `Connector` module abstracts over different ways of obtaining -connections, including `async_durable` and `persistent_connection`. - -In the app's startup code, provide a connector constructed with either -`Rpc_effect.Connector.async_durable` or -`Rpc_effect.Connector.persistent_connection`. - -``` ocaml -let run () = - let conn = - Rpc_connection.create - ~server_name:"ws-server" - ~connect:(fun () -> Rpc.Connection.client ()) - ~address:(module Unit) - Deferred.Or_error.return - in - let (_ : (unit, Nothing.t) Start.Handle.t) = - Start.start - Start.Result_spec.just_the_view - ~bind_to_element_with_id:"app" - ~custom_connector:(function - | Conn -> Rpc_effect.Connector.persistent_connection (module Rpc_connection) conn - | _ -> assert false) - App.app - in - Deferred.never () -``` diff --git a/docs/blogs/testing.md b/docs/blogs/testing.md deleted file mode 100644 index 604b9ed1..00000000 --- a/docs/blogs/testing.md +++ /dev/null @@ -1,377 +0,0 @@ -Traditional approaches for testing web applications can be infuriating. -With tools like selenium or puppeteer, there's an entire headless -browser running in the background, and not only do you need to find a -way to reconfigure the app or library for testing, slow test execution -and race condition-related bugs are a constant companion. - -Fortunately, Bonsai is built with functional purity in mind: - -- **Virtual_dom:** A type that you're likely familiar with, - `Vdom.Node.t` is a pure representation of the impure DOM. - -- **Incremental:** While Incremental is primarily an implementation - detail, the library itself is used to accelerate pure functions. - -Bonsai itself is all about building a DAG of components in a declarative -manner, with statically tracked inputs to the graph, as well as a state -machine for user interactivity. State machines and pure functions are -fantastic for structuring programs, but they also lend themselves -particularly well to testing. - -Instead of running an entire browser, the `Bonsai_web_test` library -makes the assumption that the `Virtual_dom` is the source of truth, and -this lets us run all of our tests in just OCaml, permitting us to use -other utilities like `ppx_expect_test`. - -## Getting Ready For Testing - -Testing a program built using Js_of_ocaml involves a few changes to your -normal workflow. - - (library ( - (name my_ui_test) - (js_of_ocaml ()) ; Test library must be marked with js_of_ocaml - (libraries (core my_ui)) - (inline_tests ( ; Native tests must be disabled - (native dont_build_dont_run) - (javascript build_and_run))))) - -Your jenga start-file also needs to specify the `javascript-runtest` -alias for the project. - - (alias ((name build) (deps ( - (alias %{root}/app/my-app/test/javascript-runtest) - ; ... your other build targets here... - )))) - -## Basics of testing: printing the VDOM for a component - -Let's see what a test for the simplest Bonsai component would look like. - -First the component: - -```{=html} - -``` -``` ocaml -let hello_world : Vdom.Node.t Computation.t = - Bonsai.const (Vdom.Node.span [ Vdom.Node.text "hello world" ]) -;; -``` - -And now the test: - -```{=html} - -``` -``` ocaml -module Handle = Bonsai_web_test.Handle -module Result_spec = Bonsai_web_test.Result_spec - -let%expect_test "it shows hello world" = - let handle = Handle.create (Result_spec.vdom Fn.id) hello_world in - Handle.show handle; - [%expect {| hello world |}] -;; -``` - -With this very basic test, we can see two important aspects of testing -using Bonsai: creating a `Handle.t`, and using it to print the contents -of the component. The first argument to `Handle.create` is a value -returned by `Bonsai_web_testing.Result_spec.vdom`, which finds a value -of type `Vdom.Node.t` inside of the result of the component. For many -applications and components that you'll want to test, the component -simply has the type `Vdom.Node.t Computation.t`, so passing the identity -function to `Result_spec.vdom` is sufficient. (We'll see later that more -complex result specs can be used to test more complex components). - -Finally, `Handle.show` will print the contents of the current -virtual-dom returned by the component. - -## A more dynamic component - -`Bonsai.const` can be useful, but it's certainly not the most exciting. -To spice things up, let's build a component that operates on its input: - -```{=html} - -``` -``` ocaml -let hello_user (name : string Value.t) : Vdom.Node.t Computation.t = - let%arr name = name in - Vdom.Node.span [ Vdom.Node.textf "hello %s" name ] -;; -``` - -Now, in your app, the `name` parameter may be a `Value.t` that comes -from the result of another computation, or it may originate from a -`Bonsai.Var.t` that is updated from an RPC (or something similar to an -RPC). To make things easier for us, let's use `Bonsai.Var.t` to get a -mutable handle on a `Value.t` and see what happens when we change the -Var. - -```{=html} - -``` -``` ocaml -let%expect_test "shows hello to a user" = - let user_var = Bonsai.Var.create "Bob" in - let user = Bonsai.Var.value user_var in - let handle = Handle.create (Result_spec.vdom Fn.id) (hello_user user) in - Handle.show handle; - [%expect {| hello Bob |}]; - Bonsai.Var.set user_var "Alice"; - Handle.show handle; - [%expect {| hello Alice |}] -;; -``` - -As expected, after changing the `Var.t`, the contents in the DOM are -updated! - -For tests like this, where the contents of a component are printed more -than once, we have a helper function that will print the diff between -two versions of the view: `Handle.show_diff`. This is how you'd write -the example above: - -```{=html} - -``` -``` ocaml -let%expect_test "shows hello to a user" = - let user_var = Bonsai.Var.create "Bob" in - let user = Bonsai.Var.value user_var in - let handle = Handle.create (Result_spec.vdom Fn.id) (hello_user user) in - Handle.show handle; - [%expect {| hello Bob |}]; - Bonsai.Var.set user_var "Alice"; - Handle.show_diff handle; - [%expect - {| - -| hello Bob - +| hello Alice - |}] -;; -``` - -While the diff in this instance isn't particularly illuminating, when -testing components that produce hundreds of lines of output, it can be -*much* easier to only review the diff. - -## A more interactive component - -Having values flow into the computation from outside is only one aspect -of Bonsai's dynamism. Bonsai also permits components to maintain a state -machine that can be transitioned by actions stemming from a user -interacting with the view. Before talking about testing these -components, let's first build one. - -Here, we actually use the `hello_user` component defined previously, but -the `string Value.t` comes from the component-local state instead of a -`Var`: - -```{=html} - -``` -``` ocaml -let hello_textbox : Vdom.Node.t Computation.t = - let%sub state, set = - Bonsai.state "" ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] - in - let%sub message = hello_user state in - let%arr message = message - and set = set in - Vdom.Node.div - [ Vdom.Node.input ~attrs:[ Vdom.Attr.on_input (fun _ text -> set text) ] (); message ] -;; -``` - -This component is fully self-contained -- its type is -`Vdom.Node.t Computation.t`, but that interior state is changeable by -typing into the `` text-box. - -Testing it is similar to the first component that we tested. We'll start -out with just printing its starting state, but after that, -`Handle.input_text` makes an appearance, allowing us to trigger the -`on_input` event listener. - -```{=html} - -``` -``` ocaml -let%expect_test "shows hello to a specified user" = - let handle = Handle.create (Result_spec.vdom Fn.id) hello_textbox in - Handle.show handle; - [%expect - {| -
- - hello -
- |}]; - Handle.input_text handle ~get_vdom:Fn.id ~selector:"input" ~text:"Bob"; - Handle.show_diff handle; - [%expect - {| -
- - -| hello - +| hello Bob -
- |}]; - Handle.input_text handle ~get_vdom:Fn.id ~selector:"input" ~text:"Alice"; - Handle.show_diff handle; - [%expect - {| -
- - -| hello Bob - +| hello Alice -
- |}] -;; -``` - -The parameters to `Handle.input_text` are - -1. The `handle`. -2. `~get_vdom`: for some components, the result of the computation will - be a record. A function is needed to pick out the vdom node that you - actually care about interacting with. In our case, the computation - returned just a vdom node, so we can use the identity function. -3. `~selector`: this is a css selector that can be used to find the - actual element in the vdom that we are going to type into. -4. `~text`: the text to type into the text box. - -## A component that exposes an injection function - -Many components return a computation whose result contains "inject" -functions (a function which returns `unit Vdom.Effect.t`). These -functions can be used to provide nearby components access to a -principled way of interacting with the state machine internal to that -component. - -A great example of this would be the `Bonsai.state` component, which -returns a value of this type: - -```{=html} - -``` -``` ocaml -('model * ('model -> unit Ui_effect.t)) Computation.t -``` - -The second part of the tuple inside of the result is what we'd call an -"injection function", and it can be called to set the internal state. - -Testing `Bonsai.state` (or really, any component that exposes an -injection function) will require a custom view spec and a new `Handle` -function. - -Without further ado, the test: - -```{=html} - -``` -``` ocaml -module State_view_spec = struct - type t = string * (string -> unit Vdom.Effect.t) - type incoming = string - - let view (view, _) = view - let incoming (_, incoming) = incoming -end - -let%expect_test "test Bonsai.state" = - let component : (string * (string -> unit Vdom.Effect.t)) Computation.t = - Bonsai.state "hello" ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] - in - let handle = Handle.create (module State_view_spec) component in - Handle.show handle; - [%expect {| hello |}]; - Handle.do_actions handle [ "world" ]; - Handle.show handle; - [%expect {| world |}] -;; -``` - -Instead of using the `Result_spec.vdom` helper function like before, we -need to define our view-spec module that caters specifically to the type -returned by `state`. Of note are the functions that extract the "view", -and the inject function. - -Then, in the actual test, we call the new `do_actions` function, which -passes its arguments on to the value setter function that the view spec -extracted. - -## A component which depends on time - -Sometimes the behavior of a component depends on time. For example, a -double-click component will want to check whether two clicks happened -close together. An even simpler example is a basic clock component that -might be implemented in the following way: - -```{=html} - -``` -``` ocaml -let _untestable_component = - let now = Incr.Clock.watch_now Incr.clock |> Bonsai.Incr.to_value in - return (now >>| Time_ns.to_string_utc >>| Vdom.Node.text) -;; -``` - -A large problem with this implementation is that there is no way to -write a test for it. The rendered DOM output will contain a timestamp -from when the test was run. Each time the test runs, the output -timestamp will be different, thus causing the test to always fail. - -The correct solution is to use `Incr.Clock.with_clock` to gain access to -the incremental clock that was passed to the Bonsai graph. - -```{=html} - -``` -``` ocaml -let component = - let%sub now = Bonsai.Incr.with_clock Bonsai.Time_source.watch_now in - return (now >>| Time_ns.to_string_utc >>| Vdom.Node.text) -;; -``` - -To test this component reliably, we must use \[Handle.advance_clock_by\] -for the Bonsai graph to use. - -```{=html} - -``` -``` ocaml -let%expect_test _ = - let handle = Handle.create (Result_spec.vdom Fn.id) component in - Handle.show handle; - [%expect {| 1970-01-01 00:00:00.000000000Z |}]; - Handle.advance_clock_by handle (Time_ns.Span.of_sec 2.0); - Handle.show handle; - [%expect {| 1970-01-01 00:00:02.000000000Z |}] -;; -``` - -The `with_clock` function has the following signature: - - val with_clock : (Incr.Clock.t -> 'a Incr.t) -> 'a Computation.t - -It allows for the incremental computation of some value that depends on -the current time, while still keeping the logic testable. - -## Summary - -So far we've learned how to test a number of different aspects of Bonsai -components: - -1. Components that have dynamic input -2. Stateful components that have an interactive view -3. Stateful components that return an inject function -4. Components that depend on time - -It should go without saying that you could have a component with all -four, and you'd be able to write comprehensive and deterministic tests. diff --git a/docs/blogs/why_no_bind.md b/docs/blogs/why_no_bind.md deleted file mode 100644 index f51ba8cd..00000000 --- a/docs/blogs/why_no_bind.md +++ /dev/null @@ -1,47 +0,0 @@ -Many have asked the question "`Value.t` is an Applicative, why isn't it -a Monad?" Certainly, for the users of a library, having a Monad gives -the user much more power. For example, take a look at this code using -monadic bind: - -``` ocaml -val x: bool t -val y: 'a t -val z: 'a t - -val bind: 'a t -> f:('a -> 'b t) -> 'b t - -bind x ~f:(fun x -> if x then y else z) -``` - -Look at that, we've built a `'a t` that dynamically chooses between -either `y` or `z` depending on the value of `x`. Try to do the same with -a type that is only an Applicative! (Do not try to do the same with an -Applicative, it is impossible.) - -The dynamism on display is quite useful, but sadly, it's dynamism that -would hurt Bonsai if `Value.t` were a Monad. The main issue is that -dynamism would prevent "whole-program analysis" of the app, and this -would hurt a few neat features of Bonsai: - -1. Because we have "whole-program analysis," Bonsai runs an - optimization pass on the program, resulting in a seriously condensed - incremental graph. If an app were to dynamically generate bonsai - values, we would potentially have to run the optimization many times - a second, slowing things down considerably. -2. Another benefit (that we haven't taken advantage of yet) is that if - we know about the whole program, we could insert "debug nodes" into - the graph and present a "bonsai debugger" that shows the live values - of any component's model or input. - -Another issue that comes with dynamism is that it becomes difficult to -reason about the "stateful" bits of Bonsai with components that have -models. If a dynamic node were to toggle between components, the models -for the entire dynamic sub-tree would be lost during the transition. -Going even further, dynamism is fairly slow in Incremental, the library -that Bonsai is built on top of. - -In practice, preventing Bonsai.t from implementing the Monad interface -has worked out pretty well. Every time that someone has wanted a feature -that they could get via Monads, the Bonsai-dev team has found an -alternative API that solved their need. For example `Bonsai.if_` is a -great substitute for the code provided at the top of this post. diff --git a/docs/getting_started/counters.md b/docs/getting_started/counters.md deleted file mode 100644 index b770a346..00000000 --- a/docs/getting_started/counters.md +++ /dev/null @@ -1,272 +0,0 @@ -# Bonsai by Example: Web Counters - -This app just puts counters on the page, each with buttons to increment -and decrement the number. - -## Building the example - -You'll notice that there are two directories there: `bin/`, where the -`main.bc.js` file that you're building lives, and `lib/`, which houses -the important application code. As in [the "hello world" -example](./hello_world.mdx), the "app" consists of an *index.html* page -that includes the JS file for the app; the app is attached to the "app" -div on that page. - -To run: - -`sh skip $ cd lib/bonsai/examples/counters/bin; python3 -m http.server` - -Then navigate to http://localhost:8000 - -## Your first components - -In Bonsai, you'll hear a lot about "components." What is a component? - -A component is an encapsulated bit of UI logic. It has **input** -- -immutable data that comes from "outside" that component, say from -another component or from an RPC. It has a **model**, which is the -mutable internal state of that component. And it has a **result**, which -can be of any type, but eventually, for the top-level component of your -app, will include a `Vdom.Node.t`. - -The clearest API for building a component is via `Bonsai.of_module`. -Inspecting its type, you can see that it takes a module requiring these -three parts: an input, a model, and a result: - -``` ocaml -# Bonsai.of_module0 -- : ?sexp_of_model:('m -> Sexplib0.Sexp.t) -> - ?equal:('m -> 'm -> bool) -> - (unit, 'm, 'a, 'r) Bonsai__Import.component_s -> - default_model:'m -> 'r Bonsai.Computation.t -= -``` - -As examples, in the Counters app, the "Add Another Counter" button is a -component, and each counter--a number, plus two buttons to increment and -decrement it--is another component. - -### Counter_component - -```{=html} - -``` -``` sh -$ sed -n -e '/\[CODE_EXCERPT_BEGIN 1\]/,/\[CODE_EXCERPT_END 1\]/p' ../../examples/counters/lib/bonsai_web_counters_example.ml | tail -n +2 | head -n -2 - -module Action = struct - type t = - | Increment - | Decrement - [@@deriving sexp_of] -end - -let single_counter = - let%sub counter_state = - Bonsai.state_machine0 - () - ~sexp_of_model:[%sexp_of: Int.t] - ~equal:[%equal: Int.t] - ~sexp_of_action:[%sexp_of: Action.t] - ~default_model:0 - ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model -> function - | Action.Increment -> model + 1 - | Action.Decrement -> model - 1) - in - let%arr state, inject = counter_state in - let button label action = - Vdom.Node.button - ~attrs:[ Vdom.Attr.on_click (fun _ -> inject action) ] - [ Vdom.Node.text label ] - in - Vdom.Node.div - [ button "-1" Action.Decrement - ; Vdom.Node.text (Int.to_string state) - ; button "+1" Action.Increment - ] -;; -``` - -Its model -- the component's state machine -- is an int. That's the -number we increment and decrement. Finally, its result is a -`Vdom.Node.t`, the little bit of DOM that renders that individual -counter: - -``` html -
- - 4 - -
-``` - -All of the interesting code here has to do with actions, which you can -think of as defining the state machine's transitions: here, just -`Increment` and `Decrement`. - -**The compute function** - -The body of the `let%arr` expression is the heart of the component. In -this component, it looks at the state and injection function and builds -the view out of them. - -**The inject function** - -`compute`'s inject function is just a callback that converts actions -into DOM events. These events are how Bonsai communicates to the browser -to actually do something when, say, a user clicks a button. Here, we -hook up the `"on_click'` attribute of the "+1" and "-1" buttons to the -corresponding DOM events: - -`ocaml skip let button label action = let on_click = Vdom.Attr.on_click (fun _ -> inject action) in Vdom.Node.button [ on_click ] [ Vdom.Node.text label ] in` - -**The apply_action function** - -When an action is raised by a component via a `unit Ui_effect.t`, Bonsai -will eventually pass that action back to the component's `apply_action` -function. This function is responsible for looking at the model and the -incoming action and producing a new model. - -`ocaml skip val apply_action : inject:(Action.t -> unit Ui_effect.t) -> schedule_event:(unit Ui_effect.t -> unit) -> Input.t -> Model.t -> Action.t -> Model.t` - -During the transformation, the component can also emit more actions via -`schedule_event` or use Async to arrange for `schedule_event` to be -called later. (For this it will use the same `inject` callback as -before.) This enables quite a bit of UI dynamism. Here, we don't emit -any further actions; we just increment or decrement the model: - -`ocaml skip let apply_action ~inject:_ ~schedule_event:_ () model = function | Action.Increment -> model + 1 | Action.Decrement -> model - 1 ;;` - -### Add_counter_component - -```{=html} - -``` -``` sh -$ sed -n -e '/\[CODE_EXCERPT_BEGIN 2\]/,/\[CODE_EXCERPT_END 2\]/p' ../../examples/counters/lib/bonsai_web_counters_example.ml | tail -n +2 | head -n -2 -module Model = struct - type t = unit Int.Map.t [@@deriving sexp, equal] -end - -let add_counter_component = - let%sub add_counter_state = - Bonsai.state_machine0 - () - ~sexp_of_model:[%sexp_of: Model.t] - ~equal:[%equal: Model.t] - ~sexp_of_action:[%sexp_of: Unit.t] - ~default_model:Int.Map.empty - ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model () -> - let key = Map.length model in - Map.add_exn model ~key ~data:()) - in - let%arr state, inject = add_counter_state in - let view = - Vdom.Node.button - ~attrs:[ Vdom.Attr.on_click (fun _ -> inject ()) ] - [ Vdom.Node.text "Add Another Counter" ] - in - state, view -;; -``` - -In Bonsai, "everything is a component," including the app itself. That -is, the app that we'll attach to this example's *index.html* page is a -component made of other components. In that component, the model is a -map from ints to units. The keys are just indexes: 0, 1, 2, etc. And -each value in the map is a placeholder for one of the little gizmos -defined by the Counter_component and added one at a time by pressing the -button defined by the Add_counter_component. - -Notice that in the `apply_action` function immediately above, we just -initialize a new counter's model to `()` and set its index to the -current length of the map (a trick for getting auto-incrementing -indexes). - -#### Using assoc_model to make a single component out of a map - -Turning a map of individual counters into a single component that -governs all of them is easy in Bonsai. Indeed, this is where you start -seeing Bonsai's comparative advantage over Incr_dom, our previous web -framework. Bonsai was designed precisely with this sort of "projection" -in mind. - -All it takes is this bit of code: - -`ocaml skip let%sub counters = Bonsai.assoc (module Int) map ~f:(fun _key _data -> single_counter) in` - -let%sub is a ppx for variable substitution, very similar to the standard -monadic `bind`, but with this signature: - -``` ocaml -# Bonsai.Let_syntax.Let_syntax.sub -- : ?here:Lexing.position -> - 'a Bonsai.Computation.t -> - f:('a Bonsai.Cont.t -> 'b Bonsai.Computation.t) -> - 'b Bonsai.Computation.t -= -``` - -Then the `assoc` function is used to "project" the int map over the -counter component to yield a map of counter components. Its signature -is: - -``` ocaml -# Bonsai.assoc -- : ('key, 'cmp) Bonsai.comparator -> - ('key, 'data, 'cmp) Base.Map.t Bonsai.Cont.t -> - f:('key Bonsai.Cont.t -> - 'data Bonsai.Cont.t -> 'result Bonsai.Computation.t) -> - ('key, 'result, 'cmp) Base.Map.t Bonsai.Computation.t -= -``` - -You can think of it as taking a single inner component and "projecting" -it into the map---i.e., returning a map-ish supercomponent. The -supercomponent can be used to easily add new copies of the inner -component (here, the individual counters), remove them, count over them, -etc. - -Here, all we do with `counters` is take its `Map.data`--a list of -`Vdom.Node.t`'s--and plunk them in a div: - -`ocaml skip Vdom.Node.div [] [ add_button; Vdom.Node.div [] (Map.data counters) ]` - -#### Composing components using Bonsai.map2, sub, and the Let_syntax - -It all comes together in the last few lines of the program, which -produce our toplevel component: - -```{=html} - -``` -``` sh -$ sed -n -e '/\[CODE_EXCERPT_BEGIN 3\]/,/\[CODE_EXCERPT_END 3\]/p' ../../examples/counters/lib/bonsai_web_counters_example.ml | tail -n +2 | head -n -2 -let application = - let open Bonsai.Let_syntax in - let%sub map, add_button = add_counter_component in - let%sub counters = - Bonsai.assoc (module Int) map ~f:(fun _key _data -> single_counter) - in - let%arr add_button = add_button - and counters = counters in - Vdom.Node.div [ add_button; Vdom.Node.div (Map.data counters) ] -;; -``` - -The application component is what we ultimately plug into the "app" div -of our *index.html* file. It's a `Vdom.Node.t Computation.t`, i.e., its -result is the DOM for the whole app. - -You can get a better sense of how Bonsai's composition works by looking -at a de-sugared version of the `application` function: - -`ocaml skip let application_sugar_free = let open Bonsai.Let_syntax in Let_syntax.sub (Bonsai.of_module0 (module Add_counter_component) ~default_model:Model.default) ~f:(fun add_counter -> let map = Value.map add_counter ~f:(fun (map, _) -> map) in let add_button = Value.map add_counter ~f:(fun (_, add_button) -> add_button) in Let_syntax.sub (Bonsai.assoc (module Int) map ~f:(fun _key _data -> single_counter)) ~f:(fun counters -> return (Value.map2 add_button counters ~f:(fun add_button counters -> Vdom.Node.div [] [ add_button; Vdom.Node.div [] (Map.data counters) ])))) ;;` - -The `Value.map` is used twice on the `add_counter` to destructure its -parts--the add button itself, and the map of counters. The -`Bonsai.assoc` projects the `single_counter` component over this map. -And finally, the `Value.map2` allows us to combine components. diff --git a/docs/getting_started/hello_world.md b/docs/getting_started/hello_world.md deleted file mode 100644 index 04504f22..00000000 --- a/docs/getting_started/hello_world.md +++ /dev/null @@ -1,95 +0,0 @@ -# Building the "Hello world" Bonsai app - -Most OCaml programs result in an .exe file that you run via the command -line. But Bonsai uses -[js_of_ocaml](https://github.com/ocsigen/js_of_ocaml) to produce a -JavaScript file with the extension `.bc.js` (the "bc" for "bytecode") -that is then included in an HTML page. - -To see this in action, let's walk through the process of running the -simple example in ../../examples/hello_world. - -## Step 1: The build - -Follow the Dune instructions for [building javascript -executables](https://dune.readthedocs.io/en/stable/jsoo.html) - -Once you mark a library or executable with `js_of_ocaml`, you are in -effect declaring that your only dependencies will also be -`js_of_ocaml`-compatible libraries. In particular, you're disallowed -from using libraries that won't work in a Javascript runtime, like -Core_unix or Async -- instead, you have to use their non-Unix versions -Core and Async_kernel. - -## Step 2: The HTML scaffold - -Remember that Bonsai emits a Javascript file with your app's logic. To -get this Javascript to run, we have to include it on the HTML page that -will ultimately be served to your browser. We do that by putting a -` - - -
- - -``` - -Notice that "app" `
`. That's where Bonsai will "attach" itself, -i.e., that is the element that you will be putting under Bonsai's -control. You'll see in a minute how this is wired up. - -## Step 3: Start the server - -For this example you can start any old HTML server. Here's an easy way: - -`sh skip $ cd lib/bonsai/examples/hello_world; python -m SimpleHTTPServer` - -The command will pick a random available port and print out a link you -should to navigate to in your browser. - -**Note:** Once you start the server, you don't need to restart it -whenever you make changes. Instead, the javascript file will be -recompiled by Jenga, and when you reload the web page, the browser -fetches the new copy. - -## Step 4: Walking through the code - -How do the words "hello world" get on the page? Let's look at the code. - -```{=html} - -``` -``` sh -$ cat ../../examples/hello_world/main.ml -open! Core -open! Bonsai_web - -let component = Bonsai.const (Vdom.Node.text "hello world") -let () = Bonsai_web.Start.start component -``` - -The component is a `Vdom.Node.t Bonsai_web.Computation.t` constructed -with the simplest possible constructor, `Bonsai.const`, which simply -returns as a result whatever it's passed. - -By default, `start` binds to the to the element with id "app" in our -HTML page. - -A Vdom node could contain a deeply nested tree full of elements and -callbacks; but in this case, it's just the single text node with the -contents "hello world". (Just about every branch of the DOM on any web -page bottoms out in a text node or image.) - -That's it! diff --git a/docs/getting_started/index.md b/docs/getting_started/index.md deleted file mode 100644 index 788d294a..00000000 --- a/docs/getting_started/index.md +++ /dev/null @@ -1,9 +0,0 @@ -# Getting Started with Bonsai - -1. [Building the "Hello world" Bonsai app.](./hello_world.mdx) Learn - how an OCaml program can be built into a Javascript file that - manipulates a web page. -2. [Bonsai by Example Part 1: Web Counters.](./counters.mdx) Here - you'll encounter the fundamentals of Bonsai development: how to make - components and compose/project/combine them into more complex apps. -3. Read the rest of the docs! diff --git a/docs/guide/00-introduction.md b/docs/guide/00-introduction.md index 92364c59..ac2943bb 100644 --- a/docs/guide/00-introduction.md +++ b/docs/guide/00-introduction.md @@ -1,95 +1,251 @@ -# 00 - Introduction +# Bonsai Docs: Guide (Introduction) + +This guide will teach you how to build web UIs in OCaml. We'll learn how +to: + +- Write [HTML with `virtual_dom`](./01-virtual_dom.mdx), with + interactivity powered by side-effects [encapsulated as + `Effect.t`s](./02-effects.mdx) +- Structure our web UI as a graph of composable, [incremental + computations with `Bonsai.t`](./03-incrementality.mdx) +- Instantiate and use [state](./04-state.mdx) +- Conditionally evaluate Bonsai code, or create a dynamic number of + `Bonsai.t`s, with [`match%sub` and `assoc`](./05-control_flow.mdx) + +These are the basic tools of writing OCaml web UIs. To learn how to +[style with ppx_css](../how_to/css.mdx), [send RPCs to a +server](../how_to/rpcs.mdx), [test your Bonsai +code](../how_to/testing.mdx), and more, see the [Bonsai +how-tos](../how_to/readme.md). -This guide will teach you how to build user interfaces in the browser -using several libraries, primarily `Bonsai` and `Virtual_dom`. Together, -these libraries allow you to build applications in a functional style -instead of the imperative style encouraged by the browser's API. +```{=html} + +``` +This guide is not intended to replace +[bonsai.mli](https://github.com/janestreet/bonsai/blob/master/src/bonsai.mli), +which lists and documents everything that Bonsai provides. -In this guide we aim to explain how to use Bonsai, and, to a lesser -degree, how Bonsai works under the hood. We hope that the latter will -equip you with the knowledge necessary to tune the performance of your -applications. +The rest of this intro previews building a simple web app in OCaml. +We'll discuss each step in depth in the guide chapters. -# Web Apps at 10,000 Feet +## OCaml Web Apps at 10,000 Feet -The browser understands three languages: Javascript, HTML, CSS. Jane -Street programmers only understand one language: OCaml. Thus, we've made -it possible to write all three of the browser languages using OCaml. +Functional web UIs are functions from *data* to a *view*. -- `js_of_ocaml` is an OCaml-to-Javascript compiler. -- `virtual_dom` is a library for building values that represent a - chunk of HTML. -- `css_gen` is a library for writing CSS styles in a type safe manner. +The *data* can be client-side state, data embedded in the URL, data from +the server, etc. -The CSS situation is a little more nuanced, since we actually recommend -writing CSS directly using `ppx_css`. +The *view* is the part users see. In web UIs, the view is HTML (provided +by `virtual_dom`), styled by CSS. -A user interface is a function from *data* to *view*. In types: +For example, a web UI that tells a user how many unread emails they have +might look like: ```{=html} - + ``` ``` ocaml -(* Virtual_dom.Vdom.Node.t represents your application's view *) +val message_vdom : name:string -> new_emails:int -> Vdom.Node.t +``` + +```{=html} + +``` +``` ocaml +open! Core open Virtual_dom -val ui : Your_input_type_here.t -> Vdom.Node.t +let message_vdom ~name ~new_emails = + Vdom.Node.div + ~attrs:[ [%css {|font-size: 16px;|}] ] + [ Vdom.Node.textf "hello %s! you have %d new emails" name new_emails ] +;; +``` + +```{=html} + +``` +User interactions, state updates, and RPC server calls are just *side +effects* of an otherwise pure function. We wrap these side effects in an +`Effect.t` type. + +For example, we could add a button that "reads" an email to our UI: + +```{=html} + +``` +``` ocaml +val read_email_button : on_click:unit Effect.t -> Vdom.Node.t +``` + +```{=html} + +``` +``` ocaml +let read_email_button ~on_click = + Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> on_click) ] + [ Vdom.Node.text "Read an email!" ] +;; +``` + +```{=html} + +``` +A desirable property is incrementality: when something changes, we only +recompute stuff that depends on it. We can do so by wrapping our inputs +and outputs in the incremental `Bonsai.t` type: + +```{=html} + +``` +``` ocaml +val emails_bonsai + : name:string Bonsai.t + -> new_emails:int Bonsai.t + -> read_email_effect:unit Effect.t Bonsai.t + -> Vdom.Node.t Bonsai.t +``` + +We can compose `Bonsai.t`s with the `let%arr` operator: + +```{=html} + +``` +``` ocaml +open! Bonsai_web.Cont +open Bonsai.Let_syntax + +let emails_bonsai ~name ~new_emails ~read_email_effect = + let message = + let%arr name = name + and new_emails = new_emails in + message_vdom ~name ~new_emails + in + let%arr message = message + and read_email_effect = read_email_effect in + Vdom.Node.div [ message; read_email_button ~on_click:read_email_effect ] +;; +``` + +```{=html} + ``` +In the code above, `message` will not be recomputed if only +`read_email_effect` changes. -It's easy to write composable views with such functions, since all you -need to return is a plain old OCaml value. A small amount of boilerplate -can turn this function into a simple web app that continuously displays -the result of the function. +But incrementality doesn't matter if we only have constants. Interesting +apps are stateful. We can use `Bonsai.state` to create a simple +getter/setter state. To use `Bonsai.state` and other `Bonsai.*` +primitives, we need a `local_ Bonsai.graph` "graph-builder", which +Bonsai will pass into your top-level `app` function. -Of course, this is a huge simplification; in a real app, you usually -want: +In our email example, we can use `Bonsai.state` to keep track of how +many unread emails the user has and modify that count whenever they +"read" one: -- *Interactivity*, so the user can click on, type into, and navigate - through things. -- *Incrementality*, so that large amounts of highly dynamically data - can be displayed without the interface lagging. +```{=html} + +``` +``` ocaml +val emails_stateful : name:string Bonsai.t -> local_ Bonsai.graph -> Vdom.Node.t Bonsai.t +``` -Bonsai provides these features while still encouraging the composition -and abstraction properties of regular OCaml code. Bonsai wants you to -forget it is there. The signature of a Bonsai app looks a bit like this: +```{=html} + +``` +``` ocaml +let emails_stateful ~name (local_ graph) = + let default_count = 999 in + let (count : int Bonsai.t), (set_count : (int -> unit Effect.t) Bonsai.t) = + Bonsai.state default_count graph + in + let read_email_effect = + let%arr count = count + and set_count = set_count in + set_count (count - 1) + in + emails_bonsai ~name ~new_emails:count ~read_email_effect +;; +``` + +```{=html} + +``` +Note that the state "setter" is an incrementally computed function that +produces a `unit Effect.t`. When this effect is scheduled via an event +handler, the state will update. + +And since our ultimate goal is to produce a single +incrementally-computed `Vdom.Node.t`, with state managed by Bonsai, a +complete app looks like: + +```{=html} + +``` +``` ocaml +val app : local_ Bonsai.graph -> Vdom.Node.t Bonsai.t +``` + +```{=html} + +``` +``` ocaml +let app (local_ graph) = emails_stateful ~name:(Bonsai.return "User") graph +``` + +```{=html} + +``` +We can run it with: ```{=html} ``` ``` ocaml -open Bonsai_web - -val ui : Your_input_type_here.t Value.t -> Vdom.Node.t Computation.t -``` - -It's just like before, except the input is wrapped with `Value.t` and -the output is wrapped with `Computation.t`. While there is slightly more -friction, writing re-usable UI components is just as easy. In addition, -we've expanded the kinds of components you can write, since -`Computation.t` encapsulates incremental state machines, which is how -interactivity is added to an interface. - -Both these types are covered in detail in chapters -[2](./02-dynamism.mdx) and [3](./03-state.mdx). - -# The Underlying Machinery - -The incrementality in Bonsai comes from the `Incremental` library. When -a web page loads, Bonsai compiles the top-level -`Vdom.Node.t Computation.t` into something akin to `Vdom.Node.t Incr.t`. -Then the `Incr_dom` library handles running the main loop to keep the -incremental graph stabilized (i.e. up-to-date). - -The `Vdom.Node.t` representing the current view gets put onto the screen -via a diff-and-patch process. The `virtual_dom` library always keeps -track of the previous `Vdom.Node.t` that it told the browser to display. -Whenever we request a *new* `Vdom.Node.t` to be displayed on the screen, -the library first compares it to the previous view to see what changed, -and then it applies *just those changes* to what the browser is -displaying. - -Details regarding Incremental, and the virtual-dom diff-and-patch -strategy are abstracted away so you'll rarely need to think about them. -However, a good cost model will help you to avoid or debug performance -pitfalls. Throughout the rest of this guide, we will endeavor to provide -such a cost model. +let () = Bonsai_web.Start.start app +``` + +## Bonsai is Generic + +Bonsai isn't actually web-specific: it's a library for building, +composing, and running pure, incremental, state-machines. It works +particularly well for web UIs, but it could also power other UI +backends, or even stateful, incremental computation on servers. + +That's why instead of `open! Bonsai`, you'll `open! Bonsai_web`: +`Bonsai_web` contains a bunch of web-specific utils and helpers, in +addition to the core functionality in `Bonsai`. + +## The Underlying Machinery + +Browsers can only really run +[JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) +and [WebAssembly](https://developer.mozilla.org/en-US/docs/WebAssembly). +That's why we need +[js_of_ocaml](https://ocsigen.org/js_of_ocaml/latest/manual/overview), +which compiles OCaml bytecode to JavaScript, and provides bindings for +browser APIs. diff --git a/docs/guide/01-virtual_dom.md b/docs/guide/01-virtual_dom.md index 31b1688d..8f0d0cf8 100644 --- a/docs/guide/01-virtual_dom.md +++ b/docs/guide/01-virtual_dom.md @@ -1,10 +1,13 @@ -# 01 - Virtual-dom +# 01 - Virtual-DOM Browser interfaces are described by a tree of HTML *elements*, each of which can have some *attributes* attached. The `virtual_dom` library provides an OCaml interface for constructing these trees. -# Vdom.Node.t +In this chapter, we'll learn how to write HTML in OCaml using +`virtual_dom` and `ppx_css`. + +## Vdom.Node.t This wouldn't be a programming tutorial without a hello world example, which introduces the `Vdom.Node.text` node constructor. @@ -22,6 +25,16 @@ let hello_world : Vdom.Node.t = Vdom.Node.text "hello world!" ```{=html} ``` +```{=html} + +``` The text node will frequently be the "leaf" of a view (there are no "children" of a text node). Let's put some text inside a bulleted list by using some more node constructors: @@ -67,14 +80,14 @@ and serves as a useful wrapper for the rest of the content. ``` -# Vdom.Attr.t +## Vdom.Attr.t An optional argument to the `Vdom.Node.*` constructor functions is a `Vdom.Attr.t list`. These `Attr.t` correspond to [DOM @@ -103,31 +116,48 @@ let input_placeholder : Vdom.Node.t = ```{=html} ``` -Or coloring text with inline css: +Or color text with inline css: ```{=html} - + ``` ``` ocaml -let css_gen : Vdom.Node.t = - Vdom.Node.span - ~attrs:[ Vdom.Attr.style (Css_gen.color (`Name "red")) ] - [ Vdom.Node.text "this text is red" ] +let css : Vdom.Node.t = + Vdom.Node.span ~attrs:[ [%css {|color: red;|}] ] [ Vdom.Node.text "this text is red" ] ;; ``` ```{=html} - ``` -[The 8th chapter "css"](./08-css.mdx) goes into much more depth on the -styling attributes. +```{=html} + +``` +### Event Handlers -Finally, there are "event handler" attributes which register functions -that are called when a user interacts with the element (like clicking on -buttons or typing into a text box). +An important group of `Vdom.Attr.t`s register "event handlers" for user +interaction (like clicking on buttons or typing into a text box). + +They usually receive a browser-level event value (which is almost always +ignored), alongside any useful data extracted from that event. For +example: + +```{=html} + +``` +``` ocaml +val Vdom.Attr.on_click : (Dom_html.mouseEvent Js.t -> unit Effect.t) -> Vdom.Attr.t +val Vdom.Attr.on_input : (Dom_html.event Js.t -> string -> unit Effect.t) -> Vdom.Attr.t +``` + +Here's how we can use `on_click`: ```{=html} @@ -137,8 +167,8 @@ let clicky : Vdom.Node.t = Vdom.Node.button ~attrs: [ Vdom.Attr.on_click (fun (_evt : mouse_event) -> - alert "hello there!"; - Ui_effect.Ignore) + (* Alerts are generally bad UI; there's an `Effect.print_s` for logging *) + Effect.alert "hello there!") ] [ Vdom.Node.text "click me!" ] ;; @@ -150,60 +180,22 @@ let clicky : Vdom.Node.t = ```{=html} ``` -These functions usually receive a browser-level event value (ignored in -the above example as `_evt`) alongside any useful data extracted from -that event. For example, see the following event-handler attributes for -mouse-clicks and typing into a textbox: - -`ocaml skip val Vdom.Attr.on_click : (mouse_event -> unit Vdom.Effect.t) -> Vdom.Attr.t val Vdom.Attr.on_input : (input_event -> string -> unit Vdom.Effect.t) -> Vdom.Attr.t` - -You'll notice that because `on_input` is used to respond to users typing -into a textbox, the handler function is also given a string that holds -the current contents of that textbox. - -The browser-level event-values like `mouse_event` and `input_event` are -almost always ignored in Bonsai apps. - -The return type for these event handler functions is -`unit Vdom.Effect.t`, which is the final type that we care about in the -`Virtual_dom` library. - -# unit Vdom.Effect.t - -In the example above, the `on_click` handler function returned -`Vdom.Effect.Ignore`. However, the alert definitely fires when you click -on it, so what is this value doing, and why must these event-handlers -return values of type `unit Vdom.Effect.t` in the first place? - -In reality, values of type `unit Vdom.Effect.t` are used to schedule -work on Bonsai's event-queue. `Vdom.Effect.Ignore` is the no-op event, -and it schedules no work on the event-queue. -`Vdom.Effect.Many [a; b; c]` wraps up multiple events, scheduling them -all in order. - -That leaves us with two more question: - -1. How do I get values of type `unit Vdom.Effect.t` that aren't just - `Ignore` and `Many` -2. Why would I want to use the Bonsai event queue anyway? - -Both of which will be answered in [Bonsai Guide Part 3: -State](./03-state.mdx). +We'll learn about `Effect.t` --- our abstraction for side effects --- in +[chapter 2](./02-effects.mdx). ```{=html} ``` -# The Underlying Machinery +## The Underlying Machinery -A virtual-DOM is an immutable tree of immutable UI elements that +A virtual-DOM is an immutable tree of immutable data structures that represents the view of the application at a point in time. This is in contrast to [the DOM (Document Object Model)](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model), @@ -212,26 +204,50 @@ which is a mutable tree of mutable UI elements. ```{=html} +``` +When we first compute our `Vdom.Node.t`, `virtual_dom` creates a +matching DOM tree in the browser. On further recomputations, +`virtual_dom` diffs the new virtual-DOM tree against its previous +version, and updates the DOM elements that have changed. Bonsai +schedules this diffing for you, so all you need to worry about is +producing your desired `Vdom.Node.t`. + +```{=html} + ``` -With the (not-virtual) DOM, the program mutates the tree of UI -components in order to update the view, but with the virtual-DOM, the -program produces a new tree every time the view changes. While this may -appear to be a performance nightmare, many of the tools that we use to -reduce duplication of work in regular programs also work well to prevent -re-computing parts of this sub-view. - -The `Virtual_dom` library also contains functions that diff two versions -of a virtual-dom tree. The diff can be used as instructions for mutating -the DOM to reflect the contents of the "next" virtual-DOM node. These -functions are quite fundamental, but Bonsai handles the calls to these -functions, so application developers are solely concerned with producing -new vdom trees. - -Let's continue to [Bonsai Guide Part 2: Dynamism](./02-dynamism.mdx). +Creating virtual-DOM is much, much cheaper than real DOM, so only +modifying the DOM we need to is a big performance win. But since +virtual-DOM is immutable, doesn't that mean we need to create an entire +new tree every time we recalculate view? That seems scary, but because +Bonsai computes view *incrementally*, and shares work between +subcomputations, we can build pretty big and complicated web apps with +great performance. + +### Diffing Lists + +Diffing vdom produced from dynamic lists can be tricky. Because elements +can move around, be added, or removed, we need to re-diff the entire +list whenever anything in it changes. If we have big lists, this can be +expensive. + +More concerningly, the virtual-DOM diffing algorithm won't associate +list elements with specific DOM nodes: if two elements in an input list +swap places, virtual-DOM will likely patch the two corresponding DOM +nodes to swap their content, instead of swapping the nodes themselves. +This can cause bugs if event listeners aren't properly moved around. + +The `vdom_node_with_map_children` allows you to provide a +`Vdom.Node.t Map.t` instead of a `Vdom.Node.t list`, and will perform +efficient diffing and stable association of input elements to DOM nodes. diff --git a/docs/guide/02-dynamism.md b/docs/guide/02-dynamism.md deleted file mode 100644 index bde4bbfd..00000000 --- a/docs/guide/02-dynamism.md +++ /dev/null @@ -1,372 +0,0 @@ -# 02 - Dynamism - -Dynamism is central to engaging applications: as the state of the world -changes, so should the UI. - -The previous chapter introduced an immutable view type, `Vdom.Node.t` -along with the idea that the UI is a function from data to view. For -large and dynamic input data, this function is expensive and must run -quite often. To keep up with quickly changing data, we would like to -only re-compute the parts of the view that depend on newly changed data. - -This chapter takes a detour from the theme of computing web UIs to -investigate the core Bonsai abstractions. It may be surprising to know -that Bonsai isn't specialized for user interfaces; rather, it answers -the very generic question of how to build composable incremental -state-machines. As it turns out, incremental state-machines are a great -abstraction for building UI! - -```{=html} - -``` -# Values and computations - -Bonsai is all about constructing incremental state machine graphs. A -`'a Value.t` is a node in a graph that represents a `'a` that changes -over time. A `'a Computation.t` is an entire graph that might contain -many `Value.t` of different types, but culminates in a `'a Value.t`. The -motivation for having two types will be thoroughly explored later, but -let us start with something basic: building a graph that computes a -value that depends on two other values. - -```{=html} - -``` -``` ocaml -let juxtapose_digits ~(delimiter : string) (a : int Value.t) (b : int Value.t) - : string Computation.t - = - let%arr a = a - and b = b in - Int.to_string a ^ delimiter ^ Int.to_string b -;; -``` - -The two phrases `a = a` and `b = b` may look a little silly, but they -are necessary. The expression on the right-hand side of both bindings in -the `let%arr` has type `int Value.t`, but the pattern on the left hand -side is a plain old `int` that we can freely pass to `Int.to_string`. So -`let%arr` is useful for "unwrapping" the data inside a `Value.t` so that -we can access it for a limited scope. - -The type of the entire `let%arr` expression, which includes the stuff on -both sides of `in`, is `string Computation.t` rather than -`string Value.t`. This means that the result is a graph and not a node -in a graph. To obtain the final node of a `Computation.t` graph, we can -use a `let%sub` expression. - -```{=html} - -``` -``` ocaml -let _juxtapose_and_sum (a : int Value.t) (b : int Value.t) : string Computation.t = - let%sub juxtaposed = juxtapose_digits ~delimiter:" + " a b in - let%sub sum = - let%arr a = a - and b = b in - Int.to_string (a + b) - in - let%arr juxtaposed = juxtaposed - and sum = sum in - juxtaposed ^ " = " ^ sum -;; -``` - -We provide a computation and `let%sub` provides a name we can use to -refer to the result node of that computation. In the first `let%sub` -above, the computation is `juxtapose_digits a b` and the name is -`juxtaposed`. The important thing about using `let%sub` is that -`juxtaposed` has type `string Value.t`, so we can freely use it in -`let%arr` expressions. - -A subtle, yet extremely important aspect of `let%sub` is that it makes a -copy of the input computation, and the node that the name refers to is -the result node of that copy, rather than of the original. This means -that if you use `let%sub` twice on the same computation, you get access -to the result nodes for two independent copies of the same graph. All -we've encountered so far are pure function computations constructed with -`let%arr`, so having multiple copies of a graph is useless, since all -the copies will always be producing identical results. The ability to -copy is useful when computations contain internal state. - -The following example demonstrates how to use `Bonsai.state`, a -primitive computation for introducing internal state to a computation. -Notice that we get access to two result nodes: `count` is the state's -current value and `set_count` is a function for updating that value. - -```{=html} - -``` -``` ocaml -let (counter_button : Vdom.Node.t Computation.t) = - let%sub count, set_count = Bonsai.state 0 in - let%arr count = count - and set_count = set_count in - (* view-construction logic *) - Vdom.Node.div - [ Vdom.Node.text [%string "Counter value: %{count#Int}"] - ; Vdom.Node.button - ~attrs:[ Vdom.Attr.on_click (fun _ -> set_count (count + 1)) ] - [ Vdom.Node.text "increment count" ] - ] -;; -``` - -```{=html} - -``` -Now we can illustrate the power of being able to instantiate a component -twice. The following code demonstrates that we can use `let%sub` on -`counter_button` to get three independent counters. - -```{=html} - -``` -``` ocaml -let (three_counters : Vdom.Node.t Computation.t) = - let%sub counter1 = counter_button in - let%sub counter2 = counter_button in - let%sub counter3 = counter_button in - let%arr counter1 = counter1 - and counter2 = counter2 - and counter3 = counter3 in - Vdom.Node.div [ counter1; counter2; counter3 ] -;; -``` - -```{=html} - -``` -Every time we instantiate `counter_button` with `let%sub`, we get a -`Vdom.Node.t Value.t` that represents the final result node of a copy of -the `counter_button` computation graph. We use `Vdom.Node.div` to build -a user interface that contains all three buttons so the user can click -on them; however, first we need to use `let%arr` to get access to the -view inside each counter graph node. - -The role of `let%sub` in Bonsai is similar to the `new` keyword in an -object-oriented programming language. Just like `new` makes a brand new -copy of the specified class with its own independent mutable fields, so -also does `let%sub` make a brand new copy of the specified computation -with its own independent internal state. In addition, just like `new` -usually yields a reference/pointer (in languages like C# or Java) -instead of the data itself, so also does `let%sub` yield merely the -result node of the newly copied graph instead of the graph itself. - -We've introduced two basic kinds of computations - state, which may be -introduced by `Bonsai.state`, and work, which may be introduced by -`let%arr`. While these are certainly the most important, Bonsai provides -primitive computations for a few other things, such as time-varying and -edge-triggering computations. - -We've also introduced the primary means by which you construct larger -computations from smaller ones - `let%sub`. Part of the learning curve -of building Bonsai apps is getting comfortable composing together a -bunch of little computations. - -# The scary side of values - -The previous section intentionally did not explain that `Value.t` is an -applicative, which means that it works with the `let%map` syntax, in -addition to the `let%arr` syntax we've already introduced. The -difference between the two is very small: `let%arr` expands to the -expansion of `let%map`, except it wraps the entire thing in a call to -`return`. The following - -`ocaml skip let f (x : int Value.t) : int Computation.t = let%arr x = x in x + 1` - -expands to - -`ocaml skip let f (x : int Value.t) : int Computation.t = return (let%map x = x in x + 1)` - -which further expands to - -`ocaml skip let f (x : int Value.t) : int Computation.t = return (Value.map x ~f:(fun x -> x + 1))` - -The `Value.t` applicative interface is scary because re-using the result -of a `let%map` expression causes the work that it represents to be -duplicated. Consider the following computation. - -```{=html} - -``` -``` ocaml -let component (xs : int list Value.t) : string Computation.t = - let sum = - let%map xs = xs in - List.fold xs ~init:0 ~f:( + ) - in - let average = - let%map sum = sum - and xs = xs in - let length = List.length xs in - if length = 0 then 0 else sum / length - in - let%arr sum = sum - and average = average in - [%string "sum = %{sum#Int}, average = %{average#Int}"] -;; -``` - -We would like this computation to only do the work of computing `sum` -once; however, every usage of `sum` entails an iteration through the -list. Note that the final result depends on `sum` directly, but also -indirectly through `average`; this means that `sum` is computed twice in -order to produce the formatted string. - -This explanation seems to contradict the explanation in the beginning of -this chapter that computations are graphs and values are nodes in the -graph. The truth is that values are also graphs, and re-using a value -entails using another copy of that value's graph, thus duplicating any -work contained in the graph. To avoid this work duplication, we can -instantiate the value with `let%sub`, but since `let%sub` only -instantiates computations, we must wrap the `let%map` inside a call to -`return`. For consistency and robustness, we'll apply this -transformation to `average` as well, even though it is only used once. - -```{=html} - -``` -``` ocaml -let component (xs : int list Value.t) : string Computation.t = - let%sub sum = - return - (let%map xs = xs in - List.fold xs ~init:0 ~f:( + )) - in - let%sub average = - return - (let%map sum = sum - and xs = xs in - let length = List.length xs in - if length = 0 then 0 else sum / length) - in - return - (let%map sum = sum - and average = average in - [%string "sum = %{sum#Int}, average = %{average#Int}"]) -;; -``` - -Before the introduction of `let%arr`, this was the idiomatic way of -using Bonsai. However, now that `let%arr` exists, we can transform the -above code into the following, exactly equivalent, computation: - -```{=html} - -``` -``` ocaml -let component (xs : int list Value.t) : string Computation.t = - let%sub sum = - let%arr xs = xs in - List.fold xs ~init:0 ~f:( + ) - in - let%sub average = - let%arr sum = sum - and xs = xs in - let length = List.length xs in - if length = 0 then 0 else sum / length - in - let%arr sum = sum - and average = average in - [%string "sum = %{sum#Int}, average = %{average#Int}"] -;; -``` - -While the `Value.t` applicative can have surprising behavior, if you -restrict yourself to only use `let%sub` and `let%arr`, then you won't -ever accidentally duplicate work. - -# Inputs to the graph - -Dynamic data flows into the graph through `'a Var.t`, the third main -type in Bonsai. A var is similar to a `ref` or the analogous -`'a Incr.Var.t` from incremental. - -```{=html} - -``` -``` ocaml -type 'a t - -(** Creates a var with an initial value. *) -val create : 'a -> 'a t - -(** Runs a function over the current value and updates it to the result. *) -val update : 'a t -> f:('a -> 'a) -> unit - -(** Change the current value. *) -val set : 'a t -> 'a -> unit - -(** Retrieve the current value. *) -val get : 'a t -> 'a - -(** Get a value that tracks the current value, for use in a computation. *) -val value : 'a t -> 'a Value.t -``` - -The typical use-case for a var is that there is some source of -ever-changing data, such as a `Polling_state_rpc` from a server. The -Bonsai app will subscribe to these changes with a callback that updates -the var with the new data that it received. The main app computation -then receives the value-ified var after it has been passed through -`Var.value`. Here is a concrete example: - -```{=html} - -``` -``` ocaml -let counter_every_second : int Value.t = - let counter_var : int Bonsai.Var.t = Bonsai.Var.create (-1) in - every (Time_ns.Span.of_sec 1.0) (fun () -> - Bonsai.Var.update counter_var ~f:(fun i -> i + 1)); - Bonsai.Var.value counter_var -;; - -let view_for_counter : Vdom.Node.t Computation.t = - let%arr counter = counter_every_second in - Vdom.Node.textf "counter: %d" counter -;; -``` - -```{=html} - -``` -# Bonsai is a compiler - -The `Bonsai` library does not provide the logic for stabilizing an -incremental function and extracting the output value. Instead, it -compiles the value and computation "surface syntax" into the "assembly -language" provided by the `Incremental` library. Compilation happens -once when the app starts up, and thereafter the main program only -interacts with the app in `Incr.t` form. - -The Bonsai API is carefully designed to allow its compiler to statically -analyze the entire graph. This is why we [don't provide -bind](../blogs/why_no_bind.md), since the callback passed to `bind` is -an opaque function. There are few important consequences of the static -analyzability of Bonsai graphs: - -- Compilation to incremental nodes only needs to happen once, at - startup. -- We can run "whole-program analysis" on the graph to optimize and - seriously condense the computation graph. -- We have the ability to instrument each node in a computation with - performance and debugging info. Eventually we plan to use this info - to implement a debugger and profiler for Bonsai computations. diff --git a/docs/guide/02-effects.md b/docs/guide/02-effects.md new file mode 100644 index 00000000..2de4910b --- /dev/null +++ b/docs/guide/02-effects.md @@ -0,0 +1,150 @@ +# 02 - Effects + +In the previous chapter, we built a `clicky` button that used +`Effect.alert` and an `on_click` listener attr to show browser alerts +whenever a user clicks a button. + +This chapter explains the `Effect.t` type. + +```{=html} + +``` +## What Is `Effect.t`? + +A `'a Effect.t` encapsulates some side effect, which may execute +asynchronously and eventually produce a value of type `'a`. Common +effects that you'll likely use include: + +- Setting/updating [state](./04-state.mdx) +- Focusing [form elements](../how_to/forms.mdx) +- Invoking [RPCs](../how_to/rpcs.mdx) + +At first glance, `'a Effect.t` looks very similar to [Async's +`'a Deferred.t`](https://dev.realworldocaml.org/concurrent-programming.html). +Both represent potentially asynchronous side effects, and both are +monadic, which means that they have a `bind` operator which evaluates +the side-effects in sequence. + +The main difference between the two is that `Effect` is "pure", meaning +that: + +1. "making" an `Effect.t` doesn't trigger the side-effect. The actual + side-effect isn't performed until the effect is scheduled. +2. scheduling an `Effect.t` multiple times will trigger the side-effect + multiple times. Contrast this with `Deferred.t`, where you can + `bind` on the same deferred twice and it'll only run once. + +Here's a demonstration: + +```{=html} + +``` +``` ocaml +let clickies : Vdom.Node.t = + (* This won't run until scheduled... + But it will run every time it is scheduled! *) + let greet_effect = Effect.alert "hello there!" in + Vdom.Node.div + [ Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun (_evt : mouse_event) -> greet_effect) ] + [ Vdom.Node.text "click me!" ] + ; Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun (_evt : mouse_event) -> greet_effect) ] + [ Vdom.Node.text "or me!" ] + ] +;; +``` + +```{=html} + +``` +## How to Get `Effect.t`s + +Many Bonsai tools and libraries will return some `'a Effect.t`s. For +example: + +- Bonsai's [state primitives](./04-state.mdx) return `Effect.t`s to + modify the state. +- [Rpc_effect](../how_to/rpcs.mdx) return a `'response Effect.t` for + dispatching an RPC call. +- A modal library might return `Effect.t`s that open/close the modal. +- The Effect module contains some [commonly used effects for browser + APIs](../how_to/effects_for_browser_apis.mdx) + +You can also wrap arbitrary side-effectful OCaml functions in +`Effect.t`s: + +```{=html} + +``` +``` ocaml +val Effect.of_sync_fun : ('query -> 'result) -> 'query -> 'result Effect.t +val Effect.of_thunk : (unit -> 'result) -> 'result Effect.t + +val Effect.of_deferred_fun : ('query -> 'response Deferred.t) -> 'query -> 'response Effect.t +val Effect.of_deferred_thunk : (unit -> 'response Deferred.t) -> 'response Effect.t +``` + +These are most useful for authors of reusable components / libraries; +you generally shouldn't use them in app code. + +## How to Compose `Effect.t`s + +The `Effect.t` type is a +[monad](https://builtin.com/software-engineering-perspectives/monads), +which means we can sequence `Effect.t`s with `let%bind`: + +```{=html} + +``` +``` ocaml +let chain_some_effects + (a : int Effect.t) + (b : int -> bool Effect.t) + (c : unit Effect.t) + (d : unit Effect.t) + : unit Effect.t + = + let%bind.Effect a_return = a in + (* Sometimes we don't care about the effect's return value; + we just want to execute it. *) + let%bind.Effect (_ : bool) = b a_return in + let%bind.Effect () = c in + d +;; +``` + +If you don't care about passing anything between effects, and just want +to run them in sequence, there are some utils implemented via `bind`: + +```{=html} + +``` +``` ocaml +val Effect.all_unit : unit Ui_effect.t list -> unit Ui_effect.t +val Effect.all : 'a Ui_effect.t list -> 'a list Ui_effect.t +``` + +There's also an `Effect.Many`, which takes a list of `unit Effect.t`s, +dispatches them in parallel, and does not wait for any of them to +complete. + +## How to Schedule `Effect.t`s + +Most commonly, effects are scheduled in response to user interactions +with the web UI, using `Vdom.Attr.*` event handlers. + +But you can also schedule `Effect.t`s: + +- When [your code becomes active / inactive](../how_to/lifecycles.mdx) +- When an [incremental value + changes](../how_to/edge_triggered_effects.mdx) +- At a [particular time](../how_to/time.mdx) diff --git a/docs/guide/03-incrementality.md b/docs/guide/03-incrementality.md new file mode 100644 index 00000000..1411a5b3 --- /dev/null +++ b/docs/guide/03-incrementality.md @@ -0,0 +1,94 @@ +# 03 - Incrementality + +In the last 2 chapters, we learned how to build functional web UIs with +`virtual_dom`, and schedule side effects in response to user interaction +with `Effect.t`. + +For applications with a large amount of frequently-changing input data, +it's important that we only re-compute the parts of the application that +actually depend on the new data. In this chapter, we'll: + +- Learn how to build and compose incremental computations via the + `Bonsai.t` type and `let%arr` operator +- Note that the Directed Acyclical Graph (DAG) of `Bonsai.t`s is + actually static + +## `Bonsai.t` + +Bonsai is all about constructing graphs of incremental nodes. Some of +these are stateful, but most are derived as a function of the current +values of other nodes. A good analogy to help understand Bonsai is that +of the spreadsheet. From our blog post introducing the [Incremental +library](https://blog.janestreet.com/introducing-incremental/): + +> In a spreadsheet, each cell contains either simple data, or an +> equation that describes how the value in this cell should be derived +> from values in other cells. Collectively, this amounts to a +> graph-structured computation, and one of the critical optimizations in +> Excel is that when some of the cells change, Excel only recomputes the +> parts of the graph that depend on those changed cells. + +A `'a Bonsai.t` is a node in the incremental graph, kind of like a cell +in a spreadsheet. + +`val Bonsai.return : 'a -> 'a t` wraps a plain OCaml value in a +`Bonsai.t`. This is like an Excel cell that contains some constant +value. + +To create a new `Bonsai.t` as a function of other `Bonsai.t`s, we can +use the `let%arr` operator. It works just like [`ppx_let`'s +`let%map`](https://blog.janestreet.com/let-syntax-and-why-you-should-use-it/), +but with some extra performance optimizations for pattern matching on +incremental values. This is like a formula cell in Excel. + +```{=html} + +``` +``` ocaml +let int_view (a : int Bonsai.t) : Vdom.Node.t Bonsai.t = + let%arr a = a in + Vdom.Node.div [ Vdom.Node.text (Int.to_string a) ] +;; +``` + +```{=html} + +``` +`let%arr` is just pretty syntax for +`val Bonsai.map : 'a t -> f:('a -> 'b) -> 'b t`. It's ok to use +`Bonsai.map` directly, but `let%arr` is usually more ergonomic, +especially when mapping multiple `Bonsai.t`s together: + +```{=html} + +``` +``` ocaml +let sum_and_display (a : int Bonsai.t) (b : int Bonsai.t) : Vdom.Node.t Bonsai.t = + let%arr a = a + and b = b in + Vdom.Node.textf "%d + %d = %d" a b (a + b) +;; +``` + +```{=html} + +``` +For incrementality to be useful, inputs need to actually change. On to +[Chapter 4: state](./04-state.mdx)! + +## The Underlying Machinery + +`Bonsai.t` is actually a wrapper around [Incremental's +`Incr.t`](https://blog.janestreet.com/introducing-incremental/). The +biggest user-facing difference is that there is no `Bonsai.bind`, which +forces the computation graph to have a static shape. This enables some +[useful features and performance +optimizations](../advanced/why_no_bind.mdx). We'll learn how to write +control flow code without `bind` in a [later +chapter](./05-control_flow.mdx). diff --git a/docs/guide/03-state.md b/docs/guide/03-state.md deleted file mode 100644 index 2917d12f..00000000 --- a/docs/guide/03-state.md +++ /dev/null @@ -1,424 +0,0 @@ -# 03 - State - -[Chapter 2](./02-dynamism.mdx) briefly touched on the fact that -computations capture internal state. This chapter takes a deeper look at -the primitives Bonsai provides for introducing and interacting with -local state. - -# Simple State - -The simplest kind of state is `Bonsai.state`. It returns both a value -tracking the state's current model, and also a function for updating -that model. - -`ocaml skip val state : 'model -> ('model * ('model -> unit Effect.t)) Computation.t` - -- `'model`: This is the initial value contained in the state, its - "default" "model". - -Let's break down a simple, yet realistic usage of this computation. - -```{=html} - -``` -``` ocaml -let textbox : (string * Vdom.Node.t) Computation.t = - let%sub state, set_state = Bonsai.state "" in - let%arr state = state - and set_state = set_state in - let view = - Vdom.Node.input - ~attrs: - [ Vdom.Attr.value_prop state - ; Vdom.Attr.on_input (fun _ new_text -> set_state new_text) - ] - () - in - state, view -;; -``` - -```{=html} - -``` -The computation returns the current contents of a textbox, as well as -the textbox view itself. The view could be combined with the views from -other components, eventually becoming the view for the entire -application. The "current value" could be passed on to other components -(like we'll do later). - -```{=html} - -``` -`ocaml skip let%sub state, set_state = Bonsai.state "" in` - -This line creates some string state initially containing the empty -string. We use `let%sub` to instantiate this state, giving us access to -`state` and `set_state`, which have types `string Value.t` and -`(string -> unit Effect.t) Value.t`, respectively. - -The `let%arr` expression maps over two values to produce a computation -containing the string and the view. If we attempted to write this code -using `state` and `set_state` directly instead of through `let%arr`, the -resulting program would not type-check, since both of these variables -have `Value.t` types. `let%arr` is required in order to get access to -the data inside the values. - -The actual construction of the textbox virtual-dom node is quite boring; -we add the `value_prop` property to keep the textbox contents in sync, -and also register an event handler for `on_input`, an event that fires -when the text in the textbox changes. - -```{=html} - -``` -When the event does fire, the `set_state` function is called with the -new string. `set_state` has type `string -> unit Effect.t`, which you -may recognize from the last section in the -[virtual-dom](./01-virtual_dom.mdx) chapter. This function is called -with the new textbox contents, and the event which is returned schedules -the state-setting in the Bonsai event queue. - -This is the payoff for the unanswered questions in [the virtual-dom -Chapter](./01-virtual_dom.mdx): - -1. *How do I get values of type `unit Effect.t` that aren't just - `Ignore` and `Many`*: State-transition functions returned by - stateful Bonsai components will return `unit Effect.t`s. -2. *Why would I want to use the Bonsai event queue anyway*: More - complex stateful components (like `Bonsai.state_machine`) can - witness the changes made to other stateful components, and the - Bonsai event-queue guarantees that these updates occur in a - consistent order and that downstream components witness changes made - to upstream components. - -# Multiple Textboxes - -Now that we've built a single textbox component, let's use it in a -bigger component: - -```{=html} - -``` -``` ocaml -let two_textboxes : Vdom.Node.t Computation.t = - let%sub textbox_a = textbox in - let%sub textbox_b = textbox in - let%arr contents_a, view_a = textbox_a - and contents_b, view_b = textbox_b in - let display = Vdom.Node.textf "a: %s, b: %s" contents_a contents_b in - Vdom.Node.div - ~attrs:[ Vdom.Attr.style (Css_gen.display `Inline_grid) ] - [ view_a; view_b; display ] -;; -``` - -```{=html} - -``` -This code is structurally very similar to the textbox component from -earlier: - -1. It instantiates computations using `let%sub` (this time with the - `textbox` component itself, rather than the primitive `Bonsai.state` - computation). -2. `let%arr` is used to build a computation by mapping over values - previously bound by `let%sub`. - -Of particular note is that the `textbox` component is instantiated twice -(using `let%sub`). Because of this, each textbox will have its own -independent state. - -Just for kicks, it's easy to see what would happen if the computation is -evaluated once but used twice. In the following code, the only -difference between it and the previous example is this line: - -``` diff -- let%sub textbox_b = textbox in -+ let textbox_b = textbox_a in -``` - -```{=html} - -``` -``` ocaml -let two_textboxes_shared_state : Vdom.Node.t Computation.t = - let%sub textbox_a = textbox in - let textbox_b = textbox_a in - let%arr contents_a, view_a = textbox_a - and contents_b, view_b = textbox_b in - let display = Vdom.Node.textf "a: %s, b: %s" contents_a contents_b in - Vdom.Node.div - ~attrs:[ Vdom.Attr.style (Css_gen.display `Inline_grid) ] - [ view_a; view_b; display ] -;; -``` - -```{=html} - -``` -Not very useful, but heartwarming that something sensible happens at -all. - -# State Machine - -While `Bonsai.state` is quite useful, sometimes the state contained -within an application more closely resembles a state-machine with -well-defined transitions between states. - -Consider a "counter" component that stores (and displays) an integer, -alongside buttons which increment and decrement that integer. This -component can easily be implemented using `Bonsai.state`: - -```{=html} - -``` -``` ocaml -let state_based_counter : Vdom.Node.t Computation.t = - let%sub state, set_state = Bonsai.state 0 in - let%arr state = state - and set_state = set_state in - let decrement = - Vdom.Node.button - ~attrs:[ Vdom.Attr.on_click (fun _ -> set_state (state - 1)) ] - [ Vdom.Node.text "-1" ] - in - let increment = - Vdom.Node.button - ~attrs:[ Vdom.Attr.on_click (fun _ -> set_state (state + 1)) ] - [ Vdom.Node.text "+1" ] - in - Vdom.Node.div [ decrement; Vdom.Node.textf "%d" state; increment ] -;; -``` - -```{=html} - -``` -But there's a tricky bug hidden in this implementation: if a user clicks -the button more than once within a span of 16-milliseconds, there's a -chance that both button clicks will set the same value! This is because -the "current state" value is closed over by the event handler, and this -value could be stale because the DOM (including event handlers) is only -updated once per frame (approx every 16ms). - -```{=html} - -``` -Fortunately, `Bonsai.state_machine0` is here to help! It has this type: - -```{=html} - -``` -``` ocaml -val Bonsai.state_machine0 - : (module Action with type t = 'action) - -> default_model:'model - -> apply_action: - (inject:('action -> unit Effect.t) - -> schedule_event:(unit Effect.t -> unit) - -> 'model - -> 'action - -> 'model) - -> ('model * ('action -> unit Effect.t)) Computation.t -``` - -Compared to `Bonsai.state`, there are several similarities: - -1. A "Model" first-class-module is passed in, alongside the initial - model value (`default_model`). -2. The return value is a `Computation.t` that provides the current - state alongside a function which schedules changes to the state. - -The main difference is the additional `Action` module, and -`apply_action`. The apply-action parameter is a function with a fairly -long signature, but can be simplified down to the last section: -`'model -> 'action -> 'model`. This encodes the notion of a -state-machine transition: "given the current model and an action, -produce a new model." - -```{=html} - -``` -So how would we use `state_machine0` to fix the bug in the counter -application? - -```{=html} - -``` -``` ocaml -module Action = struct - type t = - | Increment - | Decrement - [@@deriving sexp_of] -end - -let counter_state_machine : Vdom.Node.t Computation.t = - let%sub state, inject = - Bonsai.state_machine0 - () - ~sexp_of_action:[%sexp_of: Action.t] - ~default_model:0 - ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model action -> - match action with - | Increment -> model + 1 - | Decrement -> model - 1) - in - let%arr state = state - and inject = inject in - let decrement = - Vdom.Node.button - ~attrs:[ Vdom.Attr.on_click (fun _ -> inject Decrement) ] - [ Vdom.Node.text "-1" ] - in - let increment = - Vdom.Node.button - ~attrs:[ Vdom.Attr.on_click (fun _ -> inject Increment) ] - [ Vdom.Node.text "+1" ] - in - Vdom.Node.div [ decrement; Vdom.Node.textf "%d" state; increment ] -;; -``` - -First, an `Action` module is defined as a sum type that lists all the -operations that can be performed on the state-machine. This module is -passed in to the call to `state_machine0`. Then, the `apply_action` -function is defined as a model-transformation function. - -Using the computation returned by `state_machine0` also changes: instead -of a "set-state" function, we get a function that "injects" our -`Action.t` into a `unit Effect.t`. - -Now, when a button is clicked multiple times in quick succession, -instead of calling `set_state` multiple times with the same value, -Bonsai will call `inject` multiple times, and they'll be processed by -`apply_action` in order, producing the correct result. - -```{=html} - -``` -# Why should functional programmers be okay with stateful components? - -UI programming is inherently stateful. Even a UI element as simple as a -textbox needs to keep some state around to store the current contents of -the textbox. - -Many of the tools that functional programmers use for dealing with state -almost exclusively involve moving that state out of their programs into -a database, or by pulling mutable state out into a small part of the -program. These strategies can keep the majority of programs relatively -pure and easy to test, but sadly, they don't scale well to UI components -for a few reasons: - -1. The stateful nature is something that is desirable to test! -2. Bonsai needs to know when the state for a component changes so that - it can re-fire calculations of down-stream components. -3. If every component were provided a state-getter and state-setter, - this would make component composition more manual work for the - programmer; as applications get bigger and bigger, the root element - would need to manage potentially dozens or hundreds of states for - each transitive sub-component. -4. Adding or removing state from a component would be a breaking change - for everyone using it. - -One way to look at UI components is that they are portals through which -an application interacts with the messy world. The job of a component is -to translate the unpredictable user actions into a well-understood piece -of data. - -Although the fact that components are stateful might injure your -functional programming dogmatism, in fact, it is quite in line with -functional programming principles, which aim to isolate effects. The -most common way to isolate effects is by having a small kernel of -effectful code invoke the pure majority of the logic; in other words, we -isolate state by shifting it toward the root of the program. Bonsai -offers an alternative tool for isolation. With Bonsai UI components, -effectful code gets wrapped up and managed so that the interface -provided by the component remains pure; in other words, we isolate state -by shifting it toward the leaves of the program. - -On to [Chapter 4: Forms](./04-forms.mdx). diff --git a/docs/guide/04-state.md b/docs/guide/04-state.md new file mode 100644 index 00000000..65bfae61 --- /dev/null +++ b/docs/guide/04-state.md @@ -0,0 +1,519 @@ +```{=html} + +``` +# 04 - State + +In the previous chapters, we learned how to build and compose +incremental `Bonsai.t` computations via `let%arr`. But we don't yet have +any `Bonsai.t`s that actually change at runtime. We're missing a key +piece: state! + +In this chapter, we'll: + +- Remark on why UI elements should own their state +- Introduce `Bonsai.state`: a simple getter/setter primitive for state +- Emphasize that `let%arr`ing on `Bonsai.t`s does not instantiate + anything +- Explain `Bonsai.state_machine`, which can better model complex state + transition logic, and avoid race condition bugs. +- Emphasize that Bonsai computation graphs have a static shape + +## Why Should Functional Programmers Be Okay With State? + +Many of the tools that functional programmers use for dealing with state +move it out of their programs into a database, or some small "hazmat" +part of the codebase. These strategies can keep most of your code +relatively pure and easy to test, but don't really work well with UIs. + +Most programs produce some single output, updating some global state +through side effects during computation of that output. The view +computed by UIs is actually a structured set of many "leaf" UI elements, +many of which are interactive, and need their own state. + +Explicitly aggregating and distributing this state while composing +elements into a UI would be a nightmare: each one would need to manage +potentially dozens or hundreds of states for each transitive +sub-element. + +Additionally, if state lived outside of UI elements, any implementation +changes that added/removed/changed internal state would be breaking. + +Bonsai's state abstractions provide type-safe wrappers for reading and +changing state, allowing subparts of your UI to own and manage their own +state safely. + +## Simple Getter/Setter State + +The simplest state tool is `Bonsai.state`, which returns a +`'model Bonsai.t` tracking the current value, and a +`('a -> unit Effect.t) Bonsai.t` "setter [effect](./02-effects.mdx)" +producing function. It takes a default starting value and a +`local_ graph`. + +To explore `Bonsai.state`, we'll implement a counter with +increase/decrease buttons. The counter will return a +`Vdom.Node.t Bonsai.t` for the UI, and the current +`count : int Bonsai.t`, which we'll use later. + +```{=html} + +``` +``` ocaml +let counter (local_ graph) : Vdom.Node.t Bonsai.t * int Bonsai.t = + let count, set_count = Bonsai.state 0 graph in + let view = + let%arr count = count + and set_count = set_count in + (* view-construction logic *) + Vdom.Node.div + [ Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> set_count (count - 1)) ] + [ Vdom.Node.text "-1" ] + ; Vdom.Node.text [%string "Counter value: %{count#Int}"] + ; Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> set_count (count + 1)) ] + [ Vdom.Node.text "+1" ] + ] + in + view, count +;; +``` + +```{=html} + +``` +## Instantiating State + +To create several counters, we can simply call `counter` repeatedly: + +```{=html} + +``` +``` ocaml +let two_counters (local_ graph) = + let counter1, _count1 = counter graph in + let counter2, _count2 = counter graph in + let%arr counter1 = counter1 + and counter2 = counter2 in + Vdom.Node.div [ counter1; counter2 ] +;; +``` + +```{=html} + +``` +Critically, instances of state are created when the function is called +with `graph`, **not** when you `let%arr` on the resulting `Bonsai.t`s. +So this: + +```{=html} + +``` +``` ocaml +let two_counters_wrong_1 (local_ graph) = + let counter, _count = counter graph in + let%arr counter1 = counter + and counter2 = counter in + Vdom.Node.div [ counter1; counter2 ] +;; +``` + +```{=html} + +``` +is actually the same as: + +```{=html} + +``` +``` ocaml +let two_counters_wrong_2 (local_ graph) = + let counter, _count = counter graph in + let%arr counter = counter in + Vdom.Node.div [ counter; counter ] +;; +``` + +```{=html} + +``` +In both these cases, all 3 counters share the same state, which probably +isn't what you want. + +```{=html} + +``` +## State Machine + +While `Bonsai.state`'s getter/setter pattern is quite useful, sometimes +your web UI's model more closely resembles a state-machine with +well-defined transitions between states. + +There's a tricky bug hidden in our counter: if a user clicks the buttons +twice before Bonsai gets a chance to process the first click, the first +click will be "lost"! This is because the "count" `Bonsai.t` is closed +over by the event handler, so if the button is clicked again before the +new view is computed, the event handler will still have a stale value. + +```{=html} + +``` +There are some tools to deal with stale values at the [Effect.t +level](./02-effects.mdx), but this case is best solved by using +`Bonsai.state_machine0`: + +```{=html} + +``` +``` ocaml +val state_machine0 + : default_model:'model + -> apply_action:('action Bonsai.Apply_action_context.t -> 'model -> 'action -> 'model) + -> local_ Bonsai.graph + -> 'model Bonsai.t * ('action -> unit Effect.t) Bonsai.t +``` + +```{=html} + +``` +Compared to `Bonsai.state`, there are several similarities: + +1. The default model is required. +2. State is instantiated by passing in a `local_ graph`. +3. The return value is a tuple of `Bonsai.t`s that provides the current + state alongside a function which schedules changes to the state. + +The main difference is `apply_action`, which is the "state transition" +function of the state machine: "given the current model and an action, +produce a new model." The output also changes: instead of a "setter +effect" function, we get a function that takes an `Action.t` and +produces an `unit Effect.t` to "inject" it into our state machine. + +So how would we use `state_machine0` to fix the bug in the counter +application? + +```{=html} + +``` +``` ocaml +let counter_state_machine (local_ graph) : Vdom.Node.t Bonsai.t * int Bonsai.t = + let count, inject = + Bonsai.state_machine0 + ~default_model:0 + ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model action -> + match action with + | `Increment -> model + 1 + | `Decrement -> model - 1) + graph + in + let view = + let%arr count = count + and inject = inject in + Vdom.Node.div + [ Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> inject `Decrement) ] + [ Vdom.Node.text "-1" ] + ; Vdom.Node.text [%string "Counter value: %{count#Int}"] + ; Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> inject `Increment) ] + [ Vdom.Node.text "+1" ] + ] + in + view, count +;; +``` + +```{=html} + +``` +Now, when a button is clicked multiple times in quick succession, +instead of calling `set_state` multiple times with the same value, +Bonsai will call `inject` multiple times, and they'll be processed by +`apply_action` in order, producing the correct result. + +### State Machines with Inputs + +What if we wanted to increment / decrement our count by some dynamic +`step : int Bonsai.t`? Our first attempt might look like this: + +``` ocaml +# let counter_state_machine ~(step : int Bonsai.t) (local_ graph) = + let count, inject = + Bonsai.state_machine0 + ~default_model:0 + ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model action -> + let%arr step = step in + match action with + | `Increment -> model + step + | `Decrement -> model - step) + graph + in + let view = + let%arr count = count + and inject = inject in + Vdom.Node.div + [ Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> inject `Decrement) ] + [ Vdom.Node.text "-1" ] + ; Vdom.Node.text [%string "Counter value: %{count#Int}"] + ; Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> inject `Increment) ] + [ Vdom.Node.text "+1" ] + ] + in + view, count +Lines 6-9, characters 9-37: +Error: This expression has type int Bonsai.t + but an expression was expected of type int +``` + +Unfortunately, the compiler doesn't like that. Recall that +`apply_action` for `Bonsai.state_machine0` produces a `'model`, not a +`'model Bonsai.t`. Instead, we'll need some heavier machinery. + +`state_machine0` has a "0" at the end to indicate that it takes "0" +additional inputs. There's also a `state_machine1`, which allows +`apply_action` to depend on the current value of a `Bonsai.t`: + +``` diff +-val state_machine0 ++val state_machine1 + : default_model:'model + -> apply_action: + ('action Apply_action_context.t ++ -> 'input Computation_status.t + -> 'model + -> 'action + -> 'model) ++ -> 'input Bonsai.t + -> local_ graph + -> 'model Bonsai.t * ('action -> unit Effect.t) Bonsai.t +``` + +```{=html} + +``` +Let's take `step` as an input and update our implementation to use +`state_machine1`: + +```{=html} + +``` +``` ocaml +let counter_state_machine1 ~(step : int Bonsai.t) (local_ graph) = + let count, inject = + Bonsai.state_machine1 + ~default_model:0 + ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) input model action -> + match input with + | Bonsai.Computation_status.Inactive -> + (* This state machine is inactive, so it can't access the current value of [input]. + Just keep the original model *) + model + | Active step -> + (match action with + | `Increment -> model + step + | `Decrement -> model - step)) + step + graph + in + let view = + let%arr step = step + and count = count + and inject = inject in + Vdom.Node.div + [ Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> inject `Decrement) ] + [ Vdom.Node.text [%string "-%{step#Int}"] ] + ; Vdom.Node.text [%string "Counter value: %{count#Int}"] + ; Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> inject `Increment) ] + [ Vdom.Node.text [%string "+%{step#Int}"] ] + ] + in + view, count +;; +``` + +We can even chain our counters together! One counter's `count` can be +used as another counter's `step`, making what can only be described as a +frankencounter: + +```{=html} + +``` +``` ocaml +let counter_state_machine_chained (local_ graph) = + let counter1, count1 = counter_state_machine1 ~step:(Bonsai.return 1) graph in + let counter2, count2 = counter_state_machine1 ~step:count1 graph in + let counter3, _ = counter_state_machine1 ~step:count2 graph in + let%arr counter1 = counter1 + and counter2 = counter2 + and counter3 = counter3 in + Vdom.Node.div [ counter1; counter2; counter3 ] +;; +``` + +```{=html} + +``` +There is no `state_machine2` (or n), because multiple inputs could be +packaged together as a single `Bonsai.t`, and destructured inside +`apply_action`. + +### State Machines can Schedule Effects + +The `apply_action` function also receives an `Apply_action_context.t`, +which can + +1. schedule arbitrary `unit Effect.t`s via + `Apply_action_context.schedule_event` +2. dispatch other actions into itself with + `Apply_action_context.inject` + +This is necessary for any state machine that wants to send messages to +itself, e.g. when dealing with timeouts. + +It's also useful for stitching together components that talk to each +other. + +```{=html} +``` +```{=html} +``` +## Other State Primitives + +Bonsai has some other tools for state, such as `Bonsai.state_opt`, +`Bonsai.toggle`, and `Bonsai.actor`. You should read the [API +Docs](https://github.com/janestreet/bonsai/blob/master/src/bonsai.mli) +to learn more. + +All Bonsai state primitives also take an optional `reset` argument, +which allows you to control what happens when [state is +reset](../how_to/resetting_state.mdx). + +```{=html} +``` +Let's continue to [Bonsai Guide Part 5: Control +Flow](./05-control_flow.mdx). + +## The Underlying Machinery + +### `local_ graph` is a Graph Builder + +`local graph : Bonsai.graph` is used by Bonsai to build a +[static](../advanced/why_no_bind.mdx) computation graph. Most nodes in +the graph come from `let%arr` calls, but many "leaves" of the graph are +"state" nodes. At startup, Bonsai aggregates the entire state of your +app. + +The [`local_` +mode](https://blog.janestreet.com/oxidizing-ocaml-locality/) prevents +`graph` from being closed over / stashed away, so the compiler makes it +impossible to change the computation graph from any runtime code. + +Bonsai analyzes the entire computation at startup time and performs +optimizations to make apps faster! + +```{=html} + +``` +### Most Code Runs Once! + +When writing Bonsai code, you're actually doing 2 different things: + +- Defining the static computation graph; i.e. *what* is computed, and + *which* inputs it has. +- Dictating runtime behavior of the web app; i.e. *how* it is + computed. + +Only the contents of `let%arr` blocks (everything after the `in`), +`apply_action` state transition functions, and functions used to +construct [effects](./02-effects.mdx) are "runtime" code. Everything +else only runs *exactly once* at app startup to construct the +computation graph, before it gets compiled to a `Vdom.Node.t Incr.t`. diff --git a/docs/guide/05-control_flow.md b/docs/guide/05-control_flow.md new file mode 100644 index 00000000..f5edfca5 --- /dev/null +++ b/docs/guide/05-control_flow.md @@ -0,0 +1,352 @@ +```{=html} + +``` +# 05 - Control Flow + +In [chapter 3](./03-incrementality.mdx), we learned how to build and +compose a static graph of incremental `Bonsai.t`s using the `let%arr` +operator. But often, web UIs need to express some dynamic patterns, and +`let%arr` just isn't enough. In this chapter, we'll: + +- Use `match%sub` to conditionally evaluate `Bonsai.t`s +- Evaluate a collection of `Bonsai.t`s separately for each of a + dynamically-sized number of inputs +- Learn what it means for a `Bonsai.t` to be "active" vs "inactive" +- Remark on higher-order functions in Bonsai + +## `match%sub` + +Let's say we want to show the counter we built in [the state +chapter](./04-state.mdx) only when `show: bool Bonsai.t` is true. With +the tools we have today, we might write: + +```{=html} + +``` +``` ocaml +let maybe_show_naive show (local_ graph) = + let counter = counter ~step:(return 1) graph in + let%arr counter = counter + and show = show in + match show with + | false -> Vdom.Node.none + | true -> counter +;; +``` + +```{=html} + +``` +But because we are `let%arr`-ing on `counter`, the incremental runtime +will continuously recompute it, even when we aren't actually using it. + +### Conditional Recomputation + +We can avoid this and get a performance boost using Bonsai's +`match%sub`: + +```{=html} + +``` +``` ocaml +let maybe_show show (local_ graph) = + let counter = counter ~step:(return 1) graph in + match%sub show with + | false -> Bonsai.return Vdom.Node.none + | true -> counter +;; +``` + +```{=html} + +``` +`match%sub` is like `match`, but for `Bonsai.t`s: + +1. The matched value should be a `'a Bonsai.t` or a literal tuple of + `Bonsai.t`s. +2. The values produced by each of the match-arms must be of type + `'b Bonsai.t`. +3. Any identifiers bound during matching are available as + `'c Bonsai.t`s inside the arms. (You can access them as plain `'c` + in guard clauses though.) +4. The overall type of the `match%sub` expression has type + `'b Bonsai.t`. + +### Conditional Instantiation + +`match%sub` has a superpower: you can use `graph` inside its arms. This +means we can instantiate some state that is local to one arm: + +```{=html} + +``` +``` ocaml +let maybe_show_2 show (local_ graph) = + match%sub show with + | `Count_by_1 -> counter ~step:(return 1) graph + | `Count_by_2 -> counter ~step:(return 2) graph + | `No -> Bonsai.return Vdom.Node.none +;; +``` + +```{=html} + +``` +Note that each branch has an independent counter with its own state. +You'll see this if you increment the first counter and then switch to +the second. + +Interestingly, state does not go away when a branch ceases to be active: +as we noted [last chapter](./04-state.mdx), this is because Bonsai +maintains a central copy of the entire application state. + +```{=html} + +``` +### Conditional Data Dependencies + +We can also use `match%sub` to pattern-match just like regular `match`, +allowing us to conditionally access data: + +```{=html} + +``` +``` ocaml +let maybe_show_var show (local_ graph) = + match%sub show with + | `Count_by step -> counter ~step graph + | `No -> Bonsai.return Vdom.Node.none +;; +``` + +```{=html} + +``` +Note that all cases of `Count_by`, share the same counter state. That's +because they all go to the same branch of the `match%sub`. If we wanted +to create separate versions of state for individual cases of `step`, we +could use guard clauses to create multiple branches that match the same +pattern, each with their own locally instantiated state: + +```{=html} + +``` +``` ocaml +let maybe_show_var_guard show (local_ graph) = + match%sub show with + | `Count_by step when Int.equal step 1 -> counter ~step graph + | `Count_by step when Int.equal step 4 -> counter ~step graph + | `Count_by step -> counter ~step graph + | `No -> Bonsai.return Vdom.Node.none +;; +``` + +```{=html} + +``` +This particular case is pretty silly: we're not going to write separate +`match%sub` branches for every potential value of `int`. Instead, we +could use [`scope_model`](../how_to/state_per_key.mdx), which maintains +separate copies of state for some value of a key: + +```{=html} + +``` +``` ocaml +let maybe_show_var_scope_model show (local_ graph) = + match%sub show with + | `Count_by step -> + Bonsai.scope_model + (module Int) + ~on:step + ~for_:(fun (local_ graph) -> counter ~step graph) + graph + | `No -> Bonsai.return Vdom.Node.none +;; +``` + +```{=html} + +``` +## Creating a Dynamic Number of `Bonsai.t`s + +In the [last chapter](./04-state.mdx), we created two separate counters +by calling `counter_ui graph` twice. But what if we want to create `n` +counters, where `n` is an `int Bonsai.t` that can change at runtime? + +Let's try to build this with the tools we have: + +``` ocaml +# let multiple_counters (n : int Bonsai.t) (local_ graph) = + let%arr n = n in + let (counters : Vdom.Node.t Bonsai.t list) = + List.init n ~f:(fun _ -> State_examples.counter_ui graph) + in + let%arr counters = Bonsai.all counters in + Vdom.Node.div counters +Line 4, characters 56-61: +Error: The value graph is local, so cannot be used inside a closure that might escape. +Hint: The closure might escape because it is an argument to a tail call +``` + +As you can see above, this won't even compile: the content of `let%arr` +blocks is runtime code, so the `local_` mode bans you from using `graph` +within them. Furthermore, if this code compiled, the output *would* have +type `Vdom.Node.t Bonsai.t Bonsai.t`, which is illegal: remember, the +Bonsai computation graph has to be static. + +Instead, we can use Bonsai's `assoc` primitive: + +```{=html} + +``` +``` ocaml +val assoc + : ('k, 'cmp) Bonsai.comparator + -> ('k, 'v, 'cmp) Map.t Bonsai.t + -> f:('k Bonsai.t -> 'v Bonsai.t -> local_ Bonsai.graph -> 'result Bonsai.t) + -> local_ Bonsai.graph + -> ('k, 'result, 'cmp) Map.t Bonsai.t +``` + +Bonsai evaluates the body of `f` exactly once when your app starts to +produce a `'v Incr.t -> 'result Incr.t` function. This new function is +then applied to each key/value pair in the input map when your app runs, +and to any new keys added to the input map. + +```{=html} + +``` +Each key/value pair in the output map has its own independent state and +dependencies. This means that if the input map is 100,000 elements +large, but only one of the keys has data that is changing frequently, +only that key's instance will be re-run to recompute the overall output. + +Here's an example, which will make multiple copies of the counter we +implemented [last chapter](./04-state.mdx): + +```{=html} + +``` +``` ocaml +let multiple_counters (input : unit Int.Map.t Bonsai.t) (local_ graph) = + let counters = + Bonsai.assoc + (module Int) + input + ~f:(fun key (_ : unit Bonsai.t) (local_ graph) -> + let%arr key = key + and counter = State_examples.counter_ui graph in + Vdom.Node.tr + [ Vdom.Node.td [ Vdom.Node.textf "counter #%d:" key ] + ; Vdom.Node.td [ counter ] + ]) + graph + in + let%arr counters = counters in + Vdom.Node.table (Map.data counters) +;; +``` + +Let's try it out! + +```{=html} + +``` +``` ocaml +let multiple_counters_dynamic graph = + let counter_view, n = State_examples.counter ~step:(Bonsai.return 1) graph in + let map_containing_n_entries = + let%arr n = n in + if n <= 0 + then Int.Map.empty + else List.init n ~f:(fun i -> i, ()) |> Int.Map.of_alist_exn + in + let%arr counter_view = counter_view + and table = multiple_counters map_containing_n_entries graph in + Vdom.Node.div [ counter_view; table ] +;; +``` + +```{=html} + +``` +Note that if you add, remove, and re-add a counter, it will retain its +state. + +```{=html} + +``` +## Further Reading + +- `match%sub` and `Bonsai.assoc` are [higher-order + functions](../how_to/higher_order_functions.mdx) +- The code inside `match%sub` branches or `assoc` can [become + inactive](../how_to/lifecycles.mdx). diff --git a/docs/guide/05-effect.md b/docs/guide/05-effect.md deleted file mode 100644 index 5e74a1ed..00000000 --- a/docs/guide/05-effect.md +++ /dev/null @@ -1,196 +0,0 @@ -# 05 - Effect - -As we saw back in the [chapter about state](./03-state.mdx), values with -type `unit Vdom.Effect.t` are used to schedule updates to stateful -components. However, the `Effect.t` type can also be used to perform -arbitrary side-effectful actions that return values. Most commonly, -these side effects involve calling RPCs. - -A `'a Effect.t` represents a side effect which, when performed, produces -a value of type `'a`. - -There's a lot of overlap between `'a Effect.t` and `'a Deferred.t`: - -1. Both are (likely) performing side effects (like calling RPCs) -2. They produce values of type `'a` when completed -3. This result can be computed at some point in the future - -So it's important to note one major difference between `'a Effect.t` and -`'a Deferred.t`: when bound (via `let%bind`) multiple times, a -`Deferred` will execute its side effect exactly once, but an `Effect` -will side effect as many times as it is `bound`. - -This difference exists for both theoretical and practical purposes. - -On the theoretical side, `Deferred.t`, at its core, represents a value -that will be computed at some point in the future (and may perform side -effects in order to calculate that value), while `Effect.t` is a -first-class representation of the side effect itself, which happens to -produce a value. - -On the practical side, `Deferred.t` just doesn't mesh with the -incremental computational model that Bonsai provides. In particular, a -value of type `'a Deferred.t Value.t` is quite hard to use correctly, as -Bonsai has no way of knowing that the value contained inside is a -Deferred, and it won't re-compute when the deferred is completed. - -```{=html} - -``` -# Making an Effect - -The main use-case for Effect is for exposing RPCs to the Bonsai -application, so for the rest of this document, we're going to be -interacting with a function that has this type signature, which we'll -pretend is an RPC: - -```{=html} - -``` -``` ocaml -val uppercase : string -> string Deferred.t -``` - -Turning `uppercase` into a function that returns an `Effect` is easy -with `Bonsai_web.Effect.of_deferred_fun` - -```{=html} - -``` -``` ocaml -val of_deferred_fun : ('query -> 'response Deferred.t) -> 'query -> 'response t -``` - -```{=html} - -``` -Using `Bonsai_web.of_deferred_fun`, we can make a new function that -returns an `Effect.t` instead of `Deferred.t` - -```{=html} - -``` -``` ocaml -let uppercase_e : string -> string Effect.t = Bonsai_web.Effect.of_deferred_fun uppercase -``` - -# Using Effects - -By converting a deferred-returning function to return an effect, we can -more easily compose it with other Bonsai APIs, like event handlers. - -In the following example, we have a textbox, a button, and a "results" -display. We want to use the `uppercase_e` event-returning function from -above to compute the uppercased value of the contents of the textbox -when the button is clicked. - -The first implementation looks like this. - -```{=html} - -``` -``` ocaml -module Request_state = struct - type t = - | Empty - | Pending - | Filled of string - [@@deriving sexp, equal] - - let to_string = function - | Empty -> "" - | Pending -> "pending..." - | Filled s -> s - ;; -end - -let uppercase_rpc_sender = - let%sub textbox = - Forms.Elements.Textbox.string ~allow_updates_when_focused:`Always () - in - let%sub result_state = - Bonsai.state - Empty - ~sexp_of_model:[%sexp_of: Request_state.t] - ~equal:[%equal: Request_state.t] - in - let%arr textbox = textbox - and result_state, set_result = result_state in - let on_submit (contents : string) : unit Effect.t = - let%bind.Effect s = uppercase_e contents in - set_result (Filled s) - in - let form_view = - textbox - |> Forms.label "text to capitalize" - |> Forms.view_as_vdom ~on_submit:(Forms.Submit.create ~f:on_submit ()) - in - Vdom.Node.div - ~attrs:[ Vdom.Attr.style (Css_gen.display `Inline_grid) ] - [ form_view; Vdom.Node.text (Request_state.to_string result_state) ] -;; -``` - -```{=html} - -``` -```{=html} - -``` -Let's zoom in on the `on_submit` handler: - -```{=html} - -``` -``` ocaml -let on_submit (contents : string) : unit Effect.t = - let%bind.Effect s = uppercase_e contents in - set_result (Filled s) -``` - -By calling the `uppercase_e` function, a `string Effect.t` is returned. -Binding on that value gives us (at some point in the future) the result -of the operation, which we immediately pass through to update the state -of our component. - -But as mentioned above, the "Pending" state was never used. We can -implement that by adding another bind to the effect, setting "Pending" -immediately. - -```{=html} - -``` -``` ocaml -let on_submit (contents : string) : unit Vdom.Effect.t = - let open Bonsai.Effect.Let_syntax in - let%bind () = set_result Pending in - let%bind s = uppercase_e contents in - set_result (Filled s) -``` - -```{=html} - -``` -Next, read the [chapter on testing](../blogs/testing.mdx). diff --git a/docs/guide/07-flow.md b/docs/guide/07-flow.md deleted file mode 100644 index d47e81c4..00000000 --- a/docs/guide/07-flow.md +++ /dev/null @@ -1,390 +0,0 @@ -# 07 - Control Flow - -This chapter of the guide is a collection of smaller topics that are -valuable for structuring components. - -# Components as DAGs - -One of the biggest differences between Bonsai and other virtual-dom -based UI frameworks (such as React, Vue, or Elm) is that Bonsai -structures the composition of UI components as a Directed Acyclic Graph -instead of as a tree. - -What this means in practice is that the output of one component can be -fed as input to another component. - -To illustrate this, we'll build a textbox component whose placeholder -text is specified dynamically. This textbox component is so similar to -the one constructed in the [state chapter](./03-state.mdx) that the diff -between that version and the new one is shown below for convenience. - -```{=html} - -``` -``` diff --let textbox = -+let textbox ~placeholder = - let%sub state, set_state = Bonsai.state (module String) ~default_model:"" in - (let%arr state = state - and set_state = set_state -+ and placeholder = placeholder in - let view = - Vdom.Node.input - ~attr:(Vdom.Attr.many [ Vdom.Attr.value_prop state - ; Vdom.Attr.on_input (fun _ new_text -> set_state new_text) -+ ; Vdom.Attr.placeholder placeholder - ]) - () - in - state, view) -;; -``` - -And a basic usage of the new component (with a constant placeholder) - -```{=html} - -``` -``` ocaml -let textbox_with_placeholder = textbox ~placeholder:(Value.return "the placeholder") -``` - -```{=html} - -``` -And because of the graph-like structure of a Bonsai app, we can -trivially chain two textboxes together so that the contents of one of -the output of one textbox is used as the placeholder for the next. - -```{=html} - -``` -``` ocaml -let textbox_chaining = - let%sub a_contents, a_view = textbox ~placeholder:(Value.return "") in - let%sub _, b_view = textbox ~placeholder:a_contents in - let%arr a_view = a_view - and b_view = b_view in - let style = Vdom.Attr.style (Css_gen.display `Inline_grid) in - Vdom.Node.div ~attrs:[ style ] [ a_view; b_view ] -;; -``` - -```{=html} - -``` -Clearly, chaining together two textboxes to set the placeholder text -isn't particularly useful (the examples are small though!), but in real -applications, this kind of component dependency structuring is valuable -in a myriad of ways: - -- The output of a "tab-selector" component could include the view for - a tab-bar, but also a value for the currently selected tab. Then - other components could read that value and respond accordingly. -- A form could dynamically change its contents based on the values of - previously filled out form contents. -- At the top of an application component graph, a "light mode or dark - mode" checkbox component could be added, and the current value - (either light or dark) could be passed down to downstream components - to influence the way that they display. - -# match%sub - -`let%sub` should be familiar to you by now, but there's actually a more -powerful form of variable substitution which permits a limited form of -dynamism, match expressions! With `match%sub`, a `'a Value.t` is matched -on, and any bindings in the match arm are projected out into their -`Value.t` form. Let's look at what that means in practice! - -In the following example, we'll avoid building the 2nd textbox if the -first textbox is either empty or only contains whitespace. - -```{=html} - -``` -``` ocaml -let textbox_matching = - let%sub a_contents, a_view = textbox ~placeholder:(Value.return "") in - let%sub a_contents = - let%arr s = a_contents in - let s = String.strip s in - if String.is_empty s then None else Some s - in - match%sub a_contents with - | None -> - let%arr a_view = a_view in - let message = Vdom.Node.div [ Vdom.Node.text "" ] in - Vdom.Node.div [ a_view; message ] - | Some placeholder -> - let%sub _, b_view = textbox ~placeholder in - let%arr a_view = a_view - and b_view = b_view in - let style = Vdom.Attr.style (Css_gen.display `Inline_grid) in - Vdom.Node.div ~attrs:[ style ] [ a_view; b_view ] -;; -``` - -```{=html} - -``` -There are a few details to note about some of the types up above - -1. The matched value has type `'a Value.t` -2. The values produced by each of the match-arms must be of type - `'b Computation.t` -3. The overall type of the `match%sub` expression has type - `'b Computation.t` -4. Any identifiers bound during matching (in the above example, this is - just `placeholder`) are available in `Value.t` form. - -It is important to know that at any point in time, only one of the arms -in the pattern match is active, so the components in the not-matched -arms are not being computed. - -In addition to `match%sub`, `if%sub` also exists, with the exact same -semantics, but specialized for booleans. - -# Bonsai.assoc - -Up until now, Bonsai hasn't had any real tools for dealing with -dynamically sized collections of components. Sure, you could manually -re-use a text-box component twice, but if the number of distinct -components is determined at runtime, writing out a bunch of `let%sub` -won't cut it. - -That's where `Bonsai.assoc` comes in. Let's start by looking at its type -signature: - -```{=html} - -``` -``` ocaml -val assoc - : ('key, 'cmp) comparator - -> ('key, 'data, 'cmp) Map.t Value.t - -> f:('key Value.t -> 'data Value.t -> 'result Computation.t) - -> ('key, 'result, 'cmp) Map.t Computation.t -``` - -Breaking the parts of the signature down one-by-one we have - -1. `('key, 'cmp) comparator`: A comparator is required; this is - typically just `(module Int)` or `(module My_type)` where the type - is comparable, and has the sexp functions defined. -2. `('key, 'data, 'cmp) Map.t Value.t`: A dynamic map from `'key` to - `'data`. -3. a named function `f` with type - `'key Value.t -> 'data Value.t -> 'result Computation.t`; this - function will be called with every key-value pair in the map, and - produces a computation containing `'result`. -4. Finally, the return value of `assoc` is - `('key, 'result, 'cmp) Map.t Computation.t`, a map from the same - key as the input to the `'result` produced in the `f` function. - -This type signature is remarkably close to the regular OCaml function -`Map.mapi`, which has this type signature: - -```{=html} - -``` -``` ocaml -val mapi -: ('key, 'data, 'cmp) Map.t --> f:(key:'key -> data:'data -> 'result) --> ('key, 'result, 'cmp) Map.t -``` - -But of course the Bonsai version has a bunch of `Value.t` and -`Computation.t` in it's type signature, so what are those types giving -us? - -The first benefit to `assoc` is that the computation inside of `f` is -only evaluated once per key/value pair, after which any updates to the -`data` travel through the regular bonsai `Value` graph optimization. -This means that if the input map is 100,000 elements large, but only one -of the keys has data that is changing frequently, only the one component -for that key will be involved in recomputing the eventual result of the -overall function. - -The other benefit to using assoc is apparent from looking at the type of -the function: the `f` function returns a `Computation.t`, which means -that every key/value pair in the output map is its own component, each -with it's own independent state! - -For this example, we'll re-use the "counter" component defined in the -last section of the [state chapter](./03-state.mdx), but this time, -there'll be a bunch of them! - -```{=html} - -``` -``` ocaml -let multiple_counters (input : unit String.Map.t Value.t) = - let%sub counters = - Bonsai.assoc - (module String) - input - ~f:(fun _key (_ : unit Value.t) -> State_examples.counter_state_machine) - in - let%arr counters = counters in - Vdom.Node.table - (counters - |> Map.to_alist - |> List.map ~f:(fun (key, vdom) -> - let open Vdom.Node in - let name = td [ Vdom.Node.text key ] in - let counter = td [ vdom ] in - Vdom.Node.tr [ name; counter ])) -;; -``` - -and to start out with we'll use a constant map as an input to the -component: - -```{=html} - -``` -``` ocaml -let multiple_counters_constant = - multiple_counters - ([ "hello", (); "there", () ] |> Map.of_alist_exn (module String) |> Value.return) -;; -``` - -```{=html} - -``` -and while this does show off how to associate a component across a map, -using `Value.return` makes it hard to see the "dynamic" aspect. So let's -build a dynamically editable map! - -```{=html} - -``` -```{=html} - -``` -``` ocaml -module Model = struct - type t = unit String.Map.t [@@deriving sexp, equal] - - let default = String.Map.of_alist_exn [ "Dave", (); "Jill", () ] -end - -module Action = struct - type t = - | Add of string - | Remove of string - [@@deriving sexp_of] -end - -let people = - Bonsai.state_machine0 - () - ~sexp_of_model:[%sexp_of: Model.t] - ~equal:[%equal: Model.t] - ~sexp_of_action:[%sexp_of: Action.t] - ~default_model:Model.default - ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model action -> - match action with - | Add name -> Map.set model ~key:name ~data:() - | Remove name -> Map.remove model name) -;; - -let add_new_person_form ~inject_add_person = - let%sub form = Form.Elements.Textbox.string ~allow_updates_when_focused:`Always () in - let%arr form = form - and inject_add_person = inject_add_person in - let on_submit name = Vdom.Effect.Many [ Form.set form ""; inject_add_person name ] in - form - |> Form.label "name" - |> Form.validate ~f:(fun name -> - if String.for_all name ~f:Char.is_whitespace - then Error (Error.of_string "name must not be empty") - else Ok ()) - |> Form.view_as_vdom ~on_submit:(Form.Submit.create ~f:on_submit ()) -;; - -let people_table people ~inject_remove_person = - Bonsai.assoc - (module String) - people - ~f:(fun name (_ : unit Value.t) -> - let%sub counter = State_examples.counter_state_machine in - let%arr counter = counter - and name = name - and inject_remove_person = inject_remove_person in - let open Vdom.Node in - let remove_person = - td - [ button - ~attrs:[ Vdom.Attr.on_click (fun _ -> inject_remove_person name) ] - [ text "x" ] - ] - in - let name = td [ text name ] in - let counter = td [ counter ] in - tr [ name; counter; remove_person ]) -;; - -let kudo_tracker = - let%sub people, inject_action = people in - let%sub add_new_person_form = - let%sub inject_add_person = - let%arr inject_action = inject_action in - fun name -> inject_action (Add name) - in - add_new_person_form ~inject_add_person - in - let%sub people_table = - let%sub inject_remove_person = - let%arr inject_action = inject_action in - fun name -> inject_action (Remove name) - in - people_table people ~inject_remove_person - in - let%arr people_table = people_table - and add_new_person_form = add_new_person_form in - let open Vdom.Node in - div - [ h2 [ text "kudos tracker" ] - ; table - [ thead [ tr [ th [ text "Name" ]; th [ text "# Kudos" ]; th [ text "Remove" ] ] ] - ; tbody (Map.data people_table) - ] - ; h2 [ text "Add Person" ] - ; add_new_person_form - ] -;; -``` - -```{=html} - -``` diff --git a/docs/guide/08-css.md b/docs/guide/08-css.md deleted file mode 100644 index 38c77472..00000000 --- a/docs/guide/08-css.md +++ /dev/null @@ -1,599 +0,0 @@ -# 08 - Css - -```{=html} -``` -Styling an application is very important. Not only is it critical for -making an app *look* good, but "styling" is also responsible for -component layout. - -However, there are a few different ways to include style information in -your app or library, each with their own tradeoffs: - -1. Hand-written .css files -2. `style` properties in vdom nodes -3. `ppx_css` for inline stylesheets - -# Handwritten .css Files - -Using .css files to style components and pages is very common in web -development. The Mozilla Developer Network has a very good [introductory -tutorial](https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/CSS_basics) -on CSS if you aren't already familiar with it. Google also has a -[comprehensive tutorial](https://web.dev/learn/css/). - -At its core, a css stylesheet is a language for pattern-matching on DOM -tree structure combined with a set of rules that should be applied to -matching elements. - -With this css: ``{=html} - -``` css -table { - border-collapse: collapse; -} - -table td { - padding: 4px; -} - -table thead { - text-align: center; - background: brown; - color: antiquewhite; - font-weight: bold; -} - -table tr { - background: antiquewhite; -} - -table tr:nth-child(even) { - background: wheat; -} -``` - -And some basic table code, we get a pretty table! - -```{=html} - -``` -``` ocaml -type row = - { id : int - ; name : string - ; age : int - } - -let basic_table rows = - let open Vdom.Node in - let thead = thead [ td [ text "id" ]; td [ text "name" ]; td [ text "age" ] ] in - let tbody = - rows - |> List.map ~f:(fun { id; name; age } -> - tr [ td [ textf "%d" id ]; td [ text name ]; td [ textf "%d" age ] ]) - |> tbody - in - table [ thead; tbody ] -;; - -let politicians = - basic_table - [ { id = 0; name = "George Washington"; age = 67 } - ; { id = 1; name = "Alexander Hamilton"; age = 47 } - ; { id = 2; name = "Abraham Lincoln"; age = 56 } - ] -;; -``` - -```{=html} - -``` -By keeping the styling logic and vdom-node production separate, our css -can be quite high-level. Sadly, the "high level description" of the -pattern for this table might be more broad than we had hoped; this css -will style *every* table in our application, even those created by other -components that you might not own! - -A common fix for this problem is to make the patterns more specific by -adding a unique class name targeting just the tables that we want. For -example: - -```{=html} - -``` -``` diff -- table { -+ table.politicians { - border-collapse: collapse; - } - -- table td { -+ table.politicians td { - padding: 4px; - } - -- table thead { -+ table.politicians thead { - text-align: center; - background: brown; - color: antiquewhite; - font-weight: bold; - } - -- table tr { -+ table.politicians tr { - background: antiquewhite; - } - -- table tr:nth-child(even) { -+ table.politicians tr:nth-child(even) { - background: wheat; - } -``` - -and then in the table production code, - -```{=html} - -``` -``` diff -- table [ thead; tbody ] -+ table ~attr:(Vdom.Attr.class_ "politician") [ thead; tbody ] -``` - -This solves the issue of our styles inadvertantly obliterating other -people's components, but comes at the cost of verbosity. It also isn't -completely foolproof: if two people pick the same identifier (in this -case we went with `politician`), then the clash would still occur. This -leads to people using very long and descriptive identifiers to reduce -the odds of a collision. - -Another downside is that if you're an application author pulling in a UI -component which has a stylesheet, then you need to somehow get that css -file into your application. Typically this is done by writing a dune -rule that concatenates your application's stylesheet with the -stylesheets of any dependencies, like so: - -```{=html} - -``` -``` lisp -(rule ( - (targets (style.css)) - (deps (%{root}/lib/dygraph/dist/dygraph.css ./my_styles.css)) - (action "cat %{deps} > %{target}"))) -``` - -If a component requires a stylesheet, there is no way of knowing that -fact (other than by reading the readmes, but who does that?). - -**Pros** - -1. Full access to CSS language including pseudoselectors (like - `:nth-child(even)`, or `:hover`) -2. Good debugging support in Chrome Devtools - -**Cons** - -1. Identifier collisions can break things in subtle ways -2. Reusable components that rely on a css stylesheet force the - application author to use css files and to build out the dune rule - for concatenating all of their dependencies' css. - -# Vdom.Attr.style - -Another way to add styling to DOM nodes is through an individual DOM -node's `style` property. `Vdom.Attr.style` has this type signature: -`Css_gen.t -> Vdom.Attr.t`, so we'll be primarily looking at the -`Css_gen`. - -A `Css_gen.t` is a collection of key-value pairs of css properties and -their values. As an example, - -```{=html} - -``` -``` ocaml -let style: Css_gen.t = Css_gen.text_align `Center -``` - -is a style that only contains the kv-pair `text-align: center;`. -Meanwhile, - -```{=html} - -``` -``` ocaml -let style: Css_gen.t = - let open Css_gen in - text_align `Center @> background_color (`Name "red") -``` - -makes use of the `@>` operator in order to merge two `Css_gen.t`s, -producing kv-pairs which contain `text-align: center; background: red;`. - -If we ported the table example to use the inline style attribute, our -code would now look like this: - -```{=html} - -``` -``` ocaml -type row2 = - { id : int - ; name : string - ; age : int - } - -let table_styles = - let open Css_gen in - border_collapse `Collapse - @> border ~style:`Solid ~color:(`Name "brown") ~width:(`Px 1) () -;; - -let thead_styles = - let open Css_gen in - text_align `Center - @> background_color (`Name "brown") - @> color (`Name "antiquewhite") - @> font_weight `Bold -;; - -let tr_odd = Css_gen.background_color (`Name "antiquewhite") -let tr_even = Css_gen.background_color (`Name "wheat") - -let td_styles = - Css_gen.padding ~top:(`Px 4) ~bottom:(`Px 4) ~left:(`Px 4) ~right:(`Px 4) () -;; - -let basic_table_attr rows = - let open Vdom.Node in - let thead = - thead - ~attrs:[ Vdom.Attr.style thead_styles ] - [ td [ text "id" ]; td [ text "name" ]; td [ text "age" ] ] - in - let tbody = - rows - |> List.mapi ~f:(fun i { id; name; age } -> - let tr_style = if Int.( % ) i 2 = 0 then tr_even else tr_odd in - tr - ~attrs:[ Vdom.Attr.style tr_style ] - [ td ~attrs:[ Vdom.Attr.style td_styles ] [ textf "%d" id ] - ; td ~attrs:[ Vdom.Attr.style td_styles ] [ text name ] - ; td ~attrs:[ Vdom.Attr.style td_styles ] [ textf "%d" age ] - ]) - |> tbody - in - table ~attrs:[ Vdom.Attr.style table_styles ] [ thead; tbody ] -;; - -let politicians = - basic_table_attr - [ { id = 0; name = "George Washington"; age = 67 } - ; { id = 1; name = "Alexander Hamilton"; age = 47 } - ; { id = 2; name = "Abraham Lincoln"; age = 56 } - ] -;; -``` - -Because we're no longer using a css file, the first part of the file -re-defines all of the styling in OCaml using `Css_gen`. However, these -styles are not applied automatically, so we also need to add calls to -`Vdom.Attr.style` everywhere. - -In a particularly egregious case, we changed from using `List.map` to -`List.mapi`, which allowed us to check if the row is even or odd, -something that was previously done for us by our stylesheet. This brings -us to our first major drawback of using inline style attributes: -pseudo-selectors are unavailable. Some of these, like `:nth-child`, are -implementable in our view-calculation logic because we're manually -constructing the lists anyway. However, other pseudo-selectors like -`:hover` or `:focus` are impossible, and others, like `:nth-of-type` are -possible, but hair-pullingly annoying. - -**Pros** - -1. No need to worry about identifier clashes because styles are stuck - directly on the nodes themselves. -2. If used in a library, the styles come with the library instead of - being another css file that library users need to know about and - manage. - -**Cons** - -1. Much more verbose. -2. Many css-attributes are missing from the `Css_gen` library (you can - work around this with `Css_gen.create`). -3. Pseudo-selectors just aren't available. - -# CSS Ppx - -The third option is to use a brand new ppx: `ppx_css`! With this ppx, -you can write css code in your .ml files, and it will be loaded into the -document at page-load. To use the ppx, add it to your jbuild like so: - -``` diff - (executables ( - (names (main)) - (libraries (bonsai_web)) -+ (preprocess (pps (ppx_jane ppx_css))) - (js_of_ocaml ()))) -``` - -And now you can bind a module to the result of a css ppx invocation: - -```{=html} - -``` -``` ocaml -module Style = - [%css - stylesheet - {| -table.politicians { - border-collapse: collapse; - border: 1px solid brown; -} - -table.politicians td { - padding: 4px; -} - -table.politicians thead { - text-align: center; - background: brown; - color: antiquewhite; - font-weight: bold; -} - -table.politicians tr { - background: antiquewhite; -} - -table.politicians tr:nth-child(even) { - background: wheat; -} - |}] -``` - -This is the exact same css from the second example! Notice that it still -has a "politicians" class before every rule. With this new `Style` -module bound, we can *almost* keep the same ocaml view generation as we -had originally: - -```{=html} - -``` -``` ocaml -let table_with_ppx_css rows = - let open Vdom.Node in - let thead = thead [ td [ text "id" ]; td [ text "name" ]; td [ text "age" ] ] in - let tbody = - rows - |> List.map ~f:(fun { id; name; age } -> - tr [ td [ textf "%d" id ]; td [ text name ]; td [ textf "%d" age ] ]) - |> tbody - in - table ~attrs:[ Style.politicians ] [ thead; tbody ] -;; -``` - -The only difference between this function and the first one we wrote is -this: - -```{=html} - -``` -``` diff -- table ~attr:(Vdom.Attr.class_ "politicians") [ thead; tbody ] -+ table ~attr:(Vdom.Attr.class_ Style.politicians) [ thead; tbody ] -``` - -This `Style.politicians` value was generated by the ppx because it -noticed that we were using it as a class-name. The ppx will also -uniquify all of the class names and ids that it finds in the stylesheet. -As an example, the first rule's selector will become -`table.politicians_hash_e82ee99238`, where the unique name is generated -by hashing the contents of the css string as well as the path to the -file containing the ppx. This means that `Style.politicians` is a string -with the value `politicians_hash_e82ee99238`, which we use for the -classname. - -This kind of identifier hashing is useful because it means that -component authors don't need to worry about collisions. - -# \[\~rewrite\] flag - -Sometimes, like when interacting with customization APIs that require -specific classnames for CSS customization, ppx_css's hygenic identifier -hashing could get in your way. With \[\~rewrite\], you get to choose the -name for an identifier rather than ppx_css choosing - or rather -hashing - it for you. - -```{=html} - -``` -``` ocaml -(* A table library's customization API _needs_ users to style the "table-header" class. *) -stylesheet {|.table-header {...}|} -``` - -You can disable hashing when needed by using the optional `~rewrite` -parameter. - -```{=html} - -``` -``` ocaml -(* Scenario: A table library's customization API _needs_ users to style the "table-header" class. *) -stylesheet ~rewrite:["table-header", "table-header"] {|.table-header {...}|} -``` - -The above segment will "rewrite" `table-header` into `table-header` -overriding the default hashing behavior. - -Some other times, you might still want hygenic hashing, but need the -same identifier to have the same hash between two specific \[%css -stylesheet\] invocations. Here are some examples of the rewrite flag in -action: - -```{=html} - -``` -``` ocaml -stylesheet ~rewrite:[ "table-header", "table-header"; "table_row", "table-row" ] {|...|} (* Rewrites multiple identifiers at once. *) -stylesheet ~rewrite:[ "my_table", My_table_component.table ] {|...|} (* References an identifier defined in another module *) |xxx}]; -``` - -# Theming through PPX CSS - -One additional benefit is that this ppx/inliner opens up doors for -allowing components to be customized by their users. The `Style` module -that the ppx derived actually has this signature: - -```{=html} - -``` -``` ocaml -sig - module type S = sig - val politicians : string - end - - type t = (module S) - - val default : t - val politicians : string -end -``` - -While we were just using the default `politicians` value, the module -type and a default packed module implementing that module type are -intended to be used for making it easy for component authors to allow -their users to theme the components. - -By tweaking the code slightly we can see how this is possible: - -```{=html} - -``` -``` diff -- let table_with_ppx_css rows = -+ let table_with_ppx_css ?(theme=Style.default) rows = -+ let module Style = (val theme) in - let open Vdom.Node in - let thead = - thead [ td [ text "id" ]; td [ text "name" ]; td [ text "age" ] ] - in - let tbody = - rows - |> List.map ~f:(fun { id; name; age } -> - tr - [ td [ textf "%d" id ]; td [ text name ]; td [ textf "%d" age ] ]) - |> tbody - in - table ~attr:(Vdom.Attr.class_ Style.politicians) [ thead; tbody ] - ;; -``` - -now someone could make a new module which implements that type: - -```{=html} - -``` -``` ocaml -module My_theme = - [%css - stylesheet - {| -table.politicians { - border-collapse: collapse; - border: 1px solid black; -} - -table.politicians td { - padding: 4px; -} - -table.politicians thead { - text-align: center; - background: black; - color: white; - font-weight: bold; -} - -table.politicians td { - border: 1px solid black; -} - - |}] - -let table = - themeable_table - ~theme:(module My_theme) - [ { id = 0; name = "George Washington"; age = 67 } - ; { id = 1; name = "Alexander Hamilton"; age = 47 } - ; { id = 2; name = "Abraham Lincoln"; age = 56 } - ] -;; -``` - -```{=html} - -``` -Due to the nature of the generated code, there are a few restrictions on -the person building a custom theme: - -1. No new identifiers are allowed. This means that if a component - author uses a dumb name like "politicians", then a theme author - needs to as well. -2. *All* identifiers present in the component definition must be used. - You can use an empty rule like `.politicians {}` in order to meet - this requirement if you wanted to avoid themeing something. - -These restrictions may seem onerous, but it's a lot like using a record -in OCaml: the author of the record gets to decide what the names of the -fields are, and if someone wants to construct a value of that record, -they can't just leave out the fields that they don't care about. This -means that component authors should think ahead and ask themselves "what -kind of customizability will users of this component want?" before -settling on a set of identifiers and picking which nodes they're -attached to. - -There are (plans for) other kinds of ppx transformations other than just -the identifier re-writing, such as automatically introducing the -namespacing classnames for you. Those aren't here yet, but they might be -soon. - -It should be noted that even with the classname hashing, it's still -possible to write rules that affect more than you might like. I actually -included a number of bugs of this form in the examples so far (!). -`.table.politicians td` affects *every* td inside of the table, even -tables within tables. So if one of the cells of this table contained -another table that was styled differently, we'd run the risk of -overwriting it. The "correct" rule in this case would be to fully -qualify the paths to the elements, so `table.politicians td` would -become `table.politicians > tbody > tr > td`. - -**Pros** - -1. Full access to CSS language including pseudoselectors (like - `:nth-child(even)`, or `:hover`) -2. Good debugging support in Chrome Devtools -3. No need to worry about identifier clashes because these identifiers - are hashed before being used. -4. If used in a library, the styles come with the library instead of - being another css file that library users need to know about and - manage. - -**Cons** - -1. It's still up to you to make sure that styles aren't too general - (this is going to be addresed in further releases of the ppx. diff --git a/docs/guide/09-edge-triggering.md b/docs/guide/09-edge-triggering.md deleted file mode 100644 index a89dc478..00000000 --- a/docs/guide/09-edge-triggering.md +++ /dev/null @@ -1,420 +0,0 @@ -# 09 - Edge Triggering - -Bonsai encourages declarative UI construction. A computation is defined -as a list of dependencies and a function which reads the current value -of those dependencies, producing a new value. A computation defined in -this way doesn't care what the previous values of its dependencies were; -it always operates on their current value. - -However, sometimes it can be helpful to witness a transition from one -value to another. In Bonsai, we have the -[`Bonsai.Edge`](https://ocaml.org/p/bonsai/v0.16.0/doc/Bonsai/Edge/index.html) -module, which has a collection of functions which can notice things like - -1. the passage of time -2. the activation and deactivation of components -3. changing of the contents of a Value.t - -and schedule Effects when they occur. - -## `after_display` - -The main `Edge` function we'll take a look at is -`Bonsai.Edge.lifecycle`, which takes a number of optional parameters of -type `unit Effect.t Value.t`. The first of these is `after_display`. -`Edge.lifecycle` schedules the effect passed in via `after_display` as -the last operation in the Bonsai render-loop, right after the DOM has -been updated. - -```{=html} - -``` -``` ocaml -let frame_counter = - let%sub frames, set_frames = Bonsai.state 0 in - let%sub () = - Bonsai.Edge.lifecycle - ~after_display: - (let%map frames = frames - and set_frames = set_frames in - set_frames (frames + 1)) - () - in - let%arr frames = frames in - Vdom.Node.textf "this component has been alive for %d frames" frames -;; -``` - -```{=html} - -``` -The text I chose for that component was very intentional. I wrote "this -component has been alive for {n} frames" instead of "the application has -been running for {n} frames". This is because Edge functions only run if -their computation is active. Let's start with a demo, and then discuss -what "active" means. - -```{=html} - -``` -``` ocaml -let frame_toggler = - let%sub showing, set_showing = Bonsai.state false in - let%sub output = - match%sub showing with - | true -> frame_counter - | false -> Bonsai.const Vdom.Node.none - in - let%arr showing = showing - and set_showing = set_showing - and output = output in - let toggle_showing = set_showing (not showing) in - let button_text = if showing then "disable counter" else "enable counter" in - let toggle_button = - Vdom.Node.button - ~attrs:[ Vdom.Attr.on_click (fun _ -> toggle_showing) ] - [ Vdom.Node.text button_text ] - in - Vdom.Node.div [ toggle_button; output ] -;; -``` - -```{=html} - -``` -If you disable the component (wait a few seconds), you'll notice that -the counter picks up where it left off rather than continuing in the -background. - -As mentioned earlier, `after_display` only runs when the computation is -"active", and as this example demonstrates, being inside of a -`match%sub` is one way to change the activity status of a computation. - -In fact, aside from `match%sub`, there's only one other combinator that -influences the active status: `Bonsai.assoc`. - -```{=html} - -``` -`Bonsai.assoc` is used to build a dynamic number of instances of a -computation. - -Just like how - -```{=html} - -``` -``` ocaml -let%sub a = my_component in -let%sub b = my_component in -``` - -will create two *distinct* instances of `my_component`, each with their -own state, `Bonsai.assoc` can instantiate a dynamic number of -computations, one instantiation per key-value pair from an incoming -`_ Map.t Value.t`. - -I have a small library, -[Bonsai_web_ui_extendy](https://ocaml.org/p/bonsai/v0.16.0/doc/Bonsai_web_ui_extendy/index.html), -which uses `assoc` to implement a component for easily creating and -deleting instances of another component. - -We'll reuse the `frame_counter` component built in the first example, -and combine it with `extendy` to get multiple `frame_counter`s. - -Let's see it in use: -``{=html} - -``` ocaml -let wrap_remove frame_counter remove = - let x_button = - Vdom.Node.button - ~attrs:[ Vdom.Attr.on_click (fun _ -> remove) ] - [ Vdom.Node.text "x" ] - in - Vdom.Node.div [ x_button; frame_counter ] -;; - -let many_frame_watches = - let%sub { contents; append; _ } = extendy frame_counter ~wrap_remove in - let%arr contents = contents - and append = append in - let append_button = - Vdom.Node.button - ~attrs:[ Vdom.Attr.on_click (fun _ -> append) ] - [ Vdom.Node.text "add" ] - in - Vdom.Node.div (append_button :: Map.data contents) -;; -``` - -```{=html} - -``` -By clicking on the "add" button, we create multiple frame-counters, each -with their own state, each which began counting at the moment of their -creation. It might not be obvious, but clicking on the `x` button not -only removes the component from the view, but from the entire Bonsai -computation graph, so the `on_display` effect is also stopped entirely. - -## `on_activate` / `on_deactivate` - -The other two optional parameters to `Bonsai.Edge.lifecycle` are -`on_activate` and `on_deactivate`, both of which share the same type as -`after_display`: `unit Effect.t Value.t`. These effects are run whenever -the lifecycle computation becomes active or inactive. - -```{=html} - -``` -Let's modify the lifecycle component to use these new functions. First, -though, we'll want to do something when the activation/deactivation -occurs. For that, I built a tiny logging component which will let me -append a list of strings sent by the `frame_counter` component. - -Ok, on to the extension of `frame_counter`: - -```{=html} - -``` -``` ocaml -let frame_counter (log : (string -> unit Ui_effect.t) Value.t) = - let%sub frames, set_frames = Bonsai.state 0 in - let%sub () = - Bonsai.Edge.lifecycle - ~on_activate: - (let%map log = log in - log "🚀") - ~on_deactivate: - (let%map log = log in - log "🔥") - ~after_display: - (let%map frames = frames - and set_frames = set_frames in - set_frames (frames + 1)) - () - in - let%arr frames = frames in - Vdom.Node.textf "this component has been alive for %d frames" frames -;; -``` - -```{=html} - -``` -```{=html} - -``` -## `on_change` - -With the `lifecycle` function as a primitive, we can implement other -useful edge-triggering functions. One of these is also included in the -`Bonsai.Edge` module: `on_change'`. - -`on_change'` monitors a `'a Value.t`, and when that value changes, it -calls a user-provided function, giving that function both the previous -and current value. This user-provided function returns an `Effect.t`, -which will be scheduled whenever the value changes. - -```{=html} - -``` -Combining the counter-component from [Chapter 3](./03-state.mdx) and the -logging component that I used above, we can write a component which -contains both a counter and a log, where the log is updated when the -value changes. - -```{=html} - -``` -``` ocaml -let logging_counter = - let%sub log_view, log = logger in - let%sub counter_view, counter = counter in - let%sub () = - let callback = - let%map log = log in - fun prev cur -> - match prev with - | None -> Ui_effect.Ignore - | Some prev -> log (if prev < cur then "🚀" else "🔥") - in - Bonsai.Edge.on_change' - ~sexp_of_model:[%sexp_of: Int.t] - ~equal:[%equal: Int.t] - counter - ~callback - in - let%arr log_view = log_view - and counter_view = counter_view in - Vdom.Node.div [ counter_view; log_view ] -;; -``` - -```{=html} - -``` -## Implications for intelligibility and testing - -Declarative programs are easy to reason about and test. Extensive use of -the `Edge` module will make your program less and less declarative. - -Every time that you have the opportunity, you should opt for using -anything other than an `Edge.*` function. - -However, sometimes it's necessary, and we have testing helpers to make -your life a bit easier when you do use edge triggering. Because -`after_display` runs its effect, well, after the display has occurred, -how would this interact with Bonsai testing functions, like -`Handle.show`? - -To demonstrate, we'll build an *awful* Bonsai component: a linear chain -of `on_changes`: - -```{=html} - -``` -``` ocaml -let chain_computation = - let%sub a = Bonsai.const "x" in - let%sub b, set_b = - Bonsai.state " " ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] - in - let%sub c, set_c = - Bonsai.state " " ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] - in - let%sub d, set_d = - Bonsai.state " " ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] - in - let%sub () = - Bonsai.Edge.on_change - ~sexp_of_model:[%sexp_of: String.t] - ~equal:[%equal: String.t] - a - ~callback:set_b - in - let%sub () = - Bonsai.Edge.on_change - ~sexp_of_model:[%sexp_of: String.t] - ~equal:[%equal: String.t] - b - ~callback:set_c - in - let%sub () = - Bonsai.Edge.on_change - ~sexp_of_model:[%sexp_of: String.t] - ~equal:[%equal: String.t] - c - ~callback:set_d - in - return (Value.map4 a b c d ~f:(sprintf "a:%s b:%s c:%s d:%s")) -;; -``` - -Because `on_change` triggers at the end of each frame, it should take 4 -frames to settle. And indeed, in a unit test, that's exactly what we'll -see: - -```{=html} - -``` -``` ocaml -let%expect_test "chained on_change" = - let handle = Handle.create (Result_spec.string (module String)) chain_computation in - Handle.show handle; - [%expect {| a:x b: c: d: |}]; - Handle.show handle; - [%expect {| a:x b:x c: d: |}]; - Handle.show handle; - [%expect {| a:x b:x c:x d: |}]; - Handle.show handle; - [%expect {| a:x b:x c:x d:x |}]; - Handle.show handle; - [%expect {| a:x b:x c:x d:x |}] -;; -``` - -But `Bonsai_web_test.Handle` has a function that makes this a bit nicer: -`recompute_view_until_stable`, so we can rewrite the test in a way that -skips all the intermediate frames: - -```{=html} - -``` -``` ocaml -let%expect_test "chained on_change with recompute_view_until_stable" = - let handle = Handle.create (Result_spec.string (module String)) chain_computation in - Handle.recompute_view_until_stable handle; - Handle.show handle; - [%expect {| a:x b:x c:x d:x |}] -;; -``` - -`recompute_view_until_stable` is handy, but it's hiding intermediate -states. If those intermediate states allow for logical bugs in your -application, then you might miss them. As mentioned above: avoid `Edge` -if you can; it's a *sharp* tool. diff --git a/docs/guide/10-url-routing.md b/docs/guide/10-url-routing.md deleted file mode 100644 index 8eec7a5b..00000000 --- a/docs/guide/10-url-routing.md +++ /dev/null @@ -1,822 +0,0 @@ -# 10 - URL Parsing / Routing - -For any web application that supports navigation between different -pages, URLs can be an easy way to dramatically improve the user -experience: they automatically give your app some very useful features - -1. Users can save and share links to specific navigational states - within the app -2. The browser's forward and back buttons allow quick historical - navigation -3. Reading and editing URLs can be used (as a last resort maybe) for - precise navigation - -It might not be obvious at first, but URL integration is bidirectional: - -- when the URL changes, the application responds by updating its - navigation -- when the user navigates in the app, the URL should be updated to - match - -In Bonsai, the `'a Url_var.t` type is used to manage this -synchronization; when given a parsing function (that parses a URL into -values of your domain-specific `'a` type) and an "unparsing" function -(which turns `'a` back into a URL). It'll handle all the -browser-specific url management for you! - -In this guide, we'll start by building a url-var by implementing parse -and unparse by hand, but the rest of this chapter will focus on the API -built for the typed-fields ppx, which will simultaneously generate the -parser *and* unparser functions for you (and ensure that the printer and -parser round-trip)! - -# Handwritten Parse / Unparse - -Imagine you have a site with the following URLs: - -- `/search?q=capybara` -- `/settings` - -URL Var's previous API allowed you to parse/unparse this URL by you -manually implementing its parse and unparse functions: - -```{=html} - -``` -``` ocaml - module My_google_clone = struct - type t = - | Homepage - | Search of string - [@@deriving sexp, equal] - - let parse_exn ({ path; query; _ } : Url_var.Components.t) : t = - let path = String.split path ~on:'/' in - match path with - | [ "home" ] -> Homepage - | [ "search" ] -> - (match Map.find (query : _ String.Map.t) "q" with - | Some [ query ] -> Search query - | None | Some [] -> failwith "search missing query param" - | Some (_ :: _ :: _) -> failwith "search with too many query params") - | _ -> failwith "unknown path" - ;; - - let unparse (t : t) : Url_var.Components.t = - match t with - | Homepage -> Url_var.Components.create ~path:"home" () - | Search query -> - Url_var.Components.create - ~path:"search" - ~query:(String.Map.singleton "q" [ query ]) - () - ;; - end -``` - -You would have needed to write your own tests in order verify that your -parser/unparser successfully round-trip (`parse_exn(unparse(x))` gives -you back the original `x`). - -# Typed field Parse / Unparse - -With URL Var's typed field API, you can define a module that is able to -parse a URL into `My_google_clone.t` and unparse `My_google_clone.t` -into a URL: - -```{=html} - -``` -``` ocaml -module My_google_clone = struct - type t = - | Homepage - | Search of string - [@@deriving typed_variants, sexp, equal] - - let parser_for_variant : type a. a Typed_variant.t -> a Parser.t = function - | Homepage -> Parser.unit - | Search -> Parser.from_query_required ~key:"q" Value_parser.string - ;; -end - -let parser = Parser.Variant.make (module My_google_clone) -let versioned_parser = Versioned_parser.first_parser parser - -let%expect_test _ = - Parser.check_ok_and_print_urls_or_errors parser; - [%expect - {| - URL parser looks good! - ┌────────────────────┐ - │ All urls │ - ├────────────────────┤ - │ /homepage │ - │ /search?q= │ - └────────────────────┘ - |}] -;; -``` - -After creating a `My_google_clone.t Url_var.t` like this: - - let url_var = - Url_var.Typed.make - (module My_google_clone) - versioned_parser - ~fallback:(fun _exn _components -> My_google_clone.Whoops) - ;; - -You can then get an automatically-updating `My_google_clone.t Value.t` -with `Url_var.value` which updates any time that the url changes and can -be threaded into the Bonsai computation like any other `Value.t`. - -On the other side, `Url_var.set_effect` can be called to to change the -content of the url-var, and in doing so, will update the URL to reflect -it. - -The rest of the operations for `Url_var.t` are intended to be used -outside of a bonsai app: - - type 'a t - - (* for use in bonsai computation *) - val value : 'a t -> 'a Value.t - val set_effect : 'a t -> 'a -> unit Effect.t - - (* for use outside of bonsai computation *) - val update : 'a t -> f:('a -> 'a) -> unit - val set : 'a t -> 'a -> unit - val get : 'a t -> 'a - -```{=html} - -``` -```{=html} -
|}]; - let%bind () = Async_kernel_scheduler.yield_until_no_jobs_remain () in + let%bind () = Handle.flush_async_and_bonsai handle in [%expect {| (Failure "BUG: no bonsai-rpc handler installed") |}]; return () ;; @@ -106,7 +79,7 @@ let%expect_test "Clicking the button should double the number" = Handle.create ~rpc_implementations:[ double_implementation ] (Result_spec.vdom Fn.id) - app + double_number_app in Handle.show handle; [%expect @@ -117,10 +90,11 @@ let%expect_test "Clicking the button should double the number" = |}]; Handle.click_on handle ~get_vdom:Fn.id ~selector:"button"; - let%bind () = Async_kernel_scheduler.yield_until_no_jobs_remain () in + let%bind () = Handle.flush_async_and_bonsai handle in Handle.show handle; [%expect {| + ------ between bonsai frame ------
The number is: 2
diff --git a/examples/bonsai_guide_code/lib/rpc_examples_test.mli b/examples/bonsai_guide_code/lib/rpc_examples_test.mli new file mode 100644 index 00000000..bc0db47b --- /dev/null +++ b/examples/bonsai_guide_code/lib/rpc_examples_test.mli @@ -0,0 +1 @@ +(* this module is used only for side-effects *) diff --git a/examples/bonsai_guide_code/lib/testing_examples.ml b/examples/bonsai_guide_code/lib/testing_examples.ml new file mode 100644 index 00000000..f0106b8a --- /dev/null +++ b/examples/bonsai_guide_code/lib/testing_examples.ml @@ -0,0 +1,141 @@ +open! Core +open! Bonsai_web.Cont +open Bonsai.Let_syntax + +(* $MDX part-begin=hello-world *) +let hello_world = Vdom.Node.span [ Vdom.Node.text "hello world" ] + +(* $MDX part-end *) + +(* $MDX part-begin=hello-user *) +let hello_user (name : string Bonsai.t) : Vdom.Node.t Bonsai.t = + let%arr name = name in + Vdom.Node.span [ Vdom.Node.textf "hello %s" name ] +;; + +(* $MDX part-end *) + +(* $MDX part-begin=hello-text-box *) +let hello_textbox graph : Vdom.Node.t Bonsai.t = + let state, set = Bonsai.state "" graph in + let%arr message = hello_user state + and set = set in + Vdom.Node.div + [ Vdom.Node.input ~attrs:[ Vdom.Attr.on_input (fun _ text -> set text) ] (); message ] +;; + +(* $MDX part-end *) + +(* $MDX part-begin=hello-world-test *) +module Handle = Bonsai_web_test.Handle +module Result_spec = Bonsai_web_test.Result_spec + +let%expect_test "it shows hello world" = + let handle = Handle.create (Result_spec.vdom Fn.id) (fun _ -> return hello_world) in + Handle.show handle; + [%expect {| hello world |}] +;; + +(* $MDX part-end *) + +(* $MDX part-begin=hello-user-test *) +let%expect_test "shows hello to a user" = + let user_var = Bonsai.Expert.Var.create "Bob" in + let user = Bonsai.Expert.Var.value user_var in + let handle = Handle.create (Result_spec.vdom Fn.id) (fun _ -> hello_user user) in + Handle.show handle; + [%expect {| hello Bob |}]; + Bonsai.Expert.Var.set user_var "Alice"; + Handle.show handle; + [%expect {| hello Alice |}] +;; + +(* $MDX part-end *) + +(* $MDX part-begin=hello-user-diff-test *) +let%expect_test "shows hello to a user" = + let user_var = Bonsai.Expert.Var.create "Bob" in + let user = Bonsai.Expert.Var.value user_var in + let handle = Handle.create (Result_spec.vdom Fn.id) (fun _ -> hello_user user) in + Handle.show handle; + [%expect {| hello Bob |}]; + Bonsai.Expert.Var.set user_var "Alice"; + Handle.show_diff handle; + [%expect {| + -| hello Bob + +| hello Alice + |}] +;; + +(* $MDX part-end *) + +(* $MDX part-begin=hello-text-box-diff-test *) +let%expect_test "shows hello to a specified user" = + let handle = Handle.create (Result_spec.vdom Fn.id) hello_textbox in + Handle.show handle; + [%expect + {| +
+ + hello +
+ |}]; + Handle.input_text handle ~get_vdom:Fn.id ~selector:"input" ~text:"Bob"; + Handle.show_diff handle; + [%expect + {| +
+ + -| hello + +| hello Bob +
+ |}]; + Handle.input_text handle ~get_vdom:Fn.id ~selector:"input" ~text:"Alice"; + Handle.show_diff handle; + [%expect + {| +
+ + -| hello Bob + +| hello Alice +
+ |}] +;; + +(* $MDX part-end *) + +(* $MDX part-begin=state-test *) +module State_view_spec = struct + type t = string * (string -> unit Effect.t) + type incoming = string + + let view : t -> string = fun (view, _) -> view + let incoming : t -> incoming -> unit Effect.t = fun (_, incoming) -> incoming +end + +let%expect_test "test Bonsai.state" = + let state_single_bonsai graph : (string * (string -> unit Vdom.Effect.t)) Bonsai.t = + let state, inject = Bonsai.state "hello" graph in + Bonsai.both state inject + in + let handle = Handle.create (module State_view_spec) state_single_bonsai in + Handle.show handle; + [%expect {| hello |}]; + Handle.do_actions handle [ "world" ]; + Handle.show handle; + [%expect {| world |}] +;; + +(* $MDX part-end *) + +(* $MDX part-begin=test-clock *) +let%expect_test "test clock" = + let handle = Handle.create (Result_spec.vdom Fn.id) Time_examples.current_time in + Handle.show handle; + [%expect {| 1970-01-01 00:00:00.000000000Z |}]; + Handle.advance_clock_by handle (Time_ns.Span.of_sec 2.0); + Handle.show handle; + [%expect {| 1970-01-01 00:00:02.000000000Z |}] +;; + +(* $MDX part-end *) diff --git a/examples/bonsai_guide_code/lib/testing_examples.mli b/examples/bonsai_guide_code/lib/testing_examples.mli new file mode 100644 index 00000000..bc0db47b --- /dev/null +++ b/examples/bonsai_guide_code/lib/testing_examples.mli @@ -0,0 +1 @@ +(* this module is used only for side-effects *) diff --git a/examples/bonsai_guide_code/lib/time_examples.ml b/examples/bonsai_guide_code/lib/time_examples.ml new file mode 100644 index 00000000..e42d004e --- /dev/null +++ b/examples/bonsai_guide_code/lib/time_examples.ml @@ -0,0 +1,73 @@ +open! Core +open! Bonsai_web.Cont +open! Bonsai.Let_syntax + +(* $MDX part-begin=clock_now *) +let current_time graph = + let%arr now = Bonsai.Clock.now graph in + Vdom.Node.text (Time_ns.to_string_utc now) +;; + +(* $MDX part-end *) + +(* $MDX part-begin=clock_approx_now *) +let approx_current_time graph = + let%arr now = Bonsai.Clock.approx_now ~tick_every:(Time_ns.Span.of_sec 1.) graph in + Vdom.Node.text (Time_ns.to_string_utc now) +;; + +(* $MDX part-end *) + +let long_effect = + Effect.of_deferred_thunk (fun () -> + Async_kernel.after (Time_ns.Span.of_int_ms (Random.int 2000))) +;; + +(* $MDX part-begin=current_time_effect *) + +let measure_time graph = + let%arr get_time = Bonsai.Clock.get_current_time graph in + Vdom.Node.button + ~attrs: + [ Vdom.Attr.on_click (fun _ -> + let%bind.Effect start = get_time in + let%bind.Effect () = long_effect in + let%bind.Effect end_ = get_time in + let diff = Time_ns.diff end_ start |> Time_ns.Span.to_string_hum in + Effect.alert [%string "that took: %{diff}"]) + ] + [ Vdom.Node.text "Click to measure a long effect." ] +;; + +(* $MDX part-end *) + +(* $MDX part-begin=clock_sleep *) +let clock_sleep_demo graph = + let%arr sleep = Bonsai.Clock.sleep graph in + Vdom.Node.button + ~attrs: + [ Vdom.Attr.on_click (fun _ -> + let%bind.Effect () = sleep (Time_ns.Span.of_sec 2.) in + Effect.alert "... 2 seconds later...") + ] + [ Vdom.Node.text "delayed alert" ] +;; + +(* $MDX part-end *) + +(* $MDX part-begin=clock_every *) +let clock_every_demo graph = + let count, set_count = Bonsai.state 0 graph in + Bonsai.Clock.every + ~when_to_start_next_effect:`Every_multiple_of_period_blocking + ~trigger_on_activate:false + (Time_ns.Span.of_sec 1.0) + (let%arr count = count + and set_count = set_count in + set_count (count + 1)) + graph; + let%arr count = count in + Vdom.Node.text [%string "Seconds since you opened the page: %{count#Int}"] +;; + +(* $MDX part-end *) diff --git a/examples/bonsai_guide_code/lib/time_examples.mli b/examples/bonsai_guide_code/lib/time_examples.mli new file mode 100644 index 00000000..22b0cf1f --- /dev/null +++ b/examples/bonsai_guide_code/lib/time_examples.mli @@ -0,0 +1,8 @@ +open! Core +open! Bonsai_web.Cont + +val current_time : Bonsai.graph -> Vdom.Node.t Bonsai.t +val approx_current_time : Bonsai.graph -> Vdom.Node.t Bonsai.t +val measure_time : Bonsai.graph -> Vdom.Node.t Bonsai.t +val clock_sleep_demo : Bonsai.graph -> Vdom.Node.t Bonsai.t +val clock_every_demo : Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/bonsai_guide_code/lib/uri_parsing_examples.ml b/examples/bonsai_guide_code/lib/uri_parsing_examples.ml new file mode 100644 index 00000000..879d2cd6 --- /dev/null +++ b/examples/bonsai_guide_code/lib/uri_parsing_examples.ml @@ -0,0 +1,298 @@ +open! Core +open Uri_parsing +module Id = String + +(* $MDX part-begin=string_parser *) +let (parser : string Parser.t) = Parser.from_path Value_parser.string + +let%expect_test _ = + Parser.check_ok_and_print_urls_or_errors parser; + [%expect + {| + URL parser looks good! + ┌───────────┐ + │ All urls │ + ├───────────┤ + │ / │ + └───────────┘ + |}] +;; + +(* $MDX part-end *) +(* $MDX part-begin=int_parser *) +let (parser : int Parser.t) = Parser.from_query_required ~key:"q" Value_parser.int + +let%expect_test _ = + Parser.check_ok_and_print_urls_or_errors parser; + [%expect + {| + URL parser looks good! + ┌───────────┐ + │ All urls │ + ├───────────┤ + │ /?q= │ + └───────────┘ + |}] +;; + +(* $MDX part-end *) + +(* $MDX part-begin=many_id_parser *) +let (parser : Id.t list Parser.t) = + Parser.from_remaining_path (Value_parser.sexpable (module Id)) + |> Parser.with_prefix [ "With"; "a"; "Prefix" ] +;; + +let%expect_test _ = + Parser.check_ok_and_print_urls_or_errors parser; + [%expect + {| + URL parser looks good! + ┌─────────────────────────────────────┐ + │ All urls │ + ├─────────────────────────────────────┤ + │ /With/a/Prefix/> │ + └─────────────────────────────────────┘ + |}] +;; + +(* $MDX part-end *) + +(* $MDX part-begin=forum_search_params *) +module Search_params = struct + type t = + { query : string + ; author_id : Id.t option + ; categories : Id.t list + } + [@@deriving typed_fields, sexp, equal] + + let parser_for_field : type a. a Typed_field.t -> a Parser.t = function + | Query -> Parser.from_path Value_parser.string + | Author_id -> Parser.from_query_optional (Value_parser.sexpable (module Id)) + | Categories -> Parser.from_query_many (Value_parser.sexpable (module Id)) + ;; + + module Path_order = Parser.Record.Path_order (Typed_field) + + let path_order = Path_order.T [ Query ] +end + +let search_params_parser = Parser.Record.make (module Search_params) + +let%expect_test _ = + Parser.check_ok_and_print_urls_or_errors search_params_parser; + [%expect + {| + URL parser looks good! + ┌──────────────────────────────────────────────────────────────────────────┐ + │ All urls │ + ├──────────────────────────────────────────────────────────────────────────┤ + │ /?author_id=>&categories=> │ + └──────────────────────────────────────────────────────────────────────────┘ + |}] +;; + +(* $MDX part-end *) + +(* $MDX part-begin=forum_admin_page *) +module Admin_page = struct + type t = + | Settings + | Edit_user of Id.t + [@@deriving typed_variants, sexp, equal] + + let parser_for_variant : type a. a Typed_variant.t -> a Parser.t = function + | Settings -> Parser.unit + | Edit_user -> Parser.from_path (Value_parser.sexpable (module Id)) + ;; +end + +let admin_page_parser = Parser.Variant.make (module Admin_page) + +let%expect_test _ = + Parser.check_ok_and_print_urls_or_errors admin_page_parser; + [%expect + {| + URL parser looks good! + ┌───────────────────────┐ + │ All urls │ + ├───────────────────────┤ + │ /edit_user/ │ + │ /settings │ + └───────────────────────┘ + |}] +;; + +(* $MDX part-end *) + +(* $MDX part-begin=forum_user_page *) + +module User_page = struct + module Subpage = struct + type t = + | Profile + | Posts + [@@deriving typed_variants, sexp, equal] + + let parser_for_variant : type a. a Typed_variant.t -> a Parser.t = function + | Profile -> Parser.unit + | Posts -> Parser.unit + ;; + end + + type t = + { user_id : Id.t + ; subpage : Subpage.t + } + [@@deriving typed_fields, sexp, equal] + + let parser_for_field : type a. a Typed_field.t -> a Parser.t = function + | User_id -> Parser.from_path (Value_parser.sexpable (module Id)) + | Subpage -> Parser.Variant.make (module Subpage) + ;; + + module Path_order = Parser.Record.Path_order (Typed_field) + + let path_order = Path_order.T [ User_id; Subpage ] +end + +let user_page_parser = Parser.Record.make (module User_page) + +let%expect_test _ = + Parser.check_ok_and_print_urls_or_errors user_page_parser; + [%expect + {| + URL parser looks good! + ┌─────────────────────┐ + │ All urls │ + ├─────────────────────┤ + │ //posts │ + │ //profile │ + └─────────────────────┘ + |}] +;; + +(* $MDX part-end *) + +(* $MDX part-begin=forum_url *) +module Url = struct + type t = + | Homepage + | Discussion of Id.t + | Search of Search_params.t + | User of User_page.t + | Admin of Admin_page.t + [@@deriving typed_variants, sexp, equal] + + let parser_for_variant : type a. a Typed_variant.t -> a Parser.t = function + | Homepage -> Parser.end_of_path Parser.unit + | Discussion -> Parser.from_path (Value_parser.sexpable (module Id)) + | Search -> search_params_parser + | User -> user_page_parser + | Admin -> admin_page_parser + ;; +end + +let parser = Parser.Variant.make (module Url) + +let%expect_test _ = + Parser.check_ok_and_print_urls_or_errors parser; + [%expect + {| + URL parser looks good! + ┌──────────────────────────────────────────────────────────────────────────────────────────┐ + │ All urls │ + ├──────────────────────────────────────────────────────────────────────────────────────────┤ + │ / │ + │ /admin/edit_user/ │ + │ /admin/settings │ + │ /discussion/ │ + │ /search/?search.author_id=>&search.categories=> │ + │ /user//posts │ + │ /user//profile │ + └──────────────────────────────────────────────────────────────────────────────────────────┘ + |}] +;; + +(* $MDX part-end *) + +(* $MDX part-begin=forum_old *) +module Old_url = struct + type t = + | Homepage + | Post of Id.t + | Search of string + [@@deriving typed_variants, sexp, equal] + + let parser_for_variant : type a. a Typed_variant.t -> a Parser.t = function + | Homepage -> Parser.end_of_path Parser.unit + | Post -> Parser.from_path (Value_parser.sexpable (module Id)) + | Search -> Parser.from_path Value_parser.string + ;; +end + +let old_parser = Parser.Variant.make (module Old_url) + +let%expect_test _ = + Parser.check_ok_and_print_urls_or_errors old_parser; + [%expect + {| + URL parser looks good! + ┌──────────────────┐ + │ All urls │ + ├──────────────────┤ + │ / │ + │ /post/ │ + │ /search/ │ + └──────────────────┘ + |}] +;; + +(* $MDX part-end *) + +(* $MDX part-begin=forum_versioned *) +let v1_parser = Versioned_parser.first_parser old_parser + +let v2_parser = + Versioned_parser.new_parser parser ~previous:v1_parser ~f:(function + | Homepage -> Homepage + | Post id -> Discussion id + | Search query -> Search { query; author_id = None; categories = [] }) +;; + +let%expect_test _ = + Versioned_parser.check_ok_and_print_urls_or_errors v2_parser; + [%expect + {| + URL parser looks good! + ┌──────────────────────────────────────────────────────────────────────────────────────────┐ + │ All urls │ + ├──────────────────────────────────────────────────────────────────────────────────────────┤ + │ / │ + │ /admin/edit_user/ │ + │ /admin/settings │ + │ /discussion/ │ + │ /search/?search.author_id=>&search.categories=> │ + │ /user//posts │ + │ /user//profile │ + └──────────────────────────────────────────────────────────────────────────────────────────┘ + + | + falls back to + | + v + + URL parser looks good! + ┌──────────────────┐ + │ All urls │ + ├──────────────────┤ + │ / │ + │ /post/ │ + │ /search/ │ + └──────────────────┘ + |}] +;; +(* $MDX part-end *) diff --git a/examples/bonsai_guide_code/lib/uri_parsing_examples.mli b/examples/bonsai_guide_code/lib/uri_parsing_examples.mli new file mode 100644 index 00000000..bc0db47b --- /dev/null +++ b/examples/bonsai_guide_code/lib/uri_parsing_examples.mli @@ -0,0 +1 @@ +(* this module is used only for side-effects *) diff --git a/examples/bonsai_guide_code/lifecycle_examples.ml b/examples/bonsai_guide_code/lifecycle_examples.ml new file mode 100644 index 00000000..d956d294 --- /dev/null +++ b/examples/bonsai_guide_code/lifecycle_examples.ml @@ -0,0 +1,45 @@ +open! Core +open! Bonsai_web.Cont +open! Bonsai.Let_syntax + +(* $MDX part-begin=lifecycle *) +let lifecycle_demo graph = + let log_val, log = + Bonsai.state_machine0 + ~default_model:"" + ~apply_action:(fun _ curr new_ -> curr ^ new_) + graph + in + let show, toggle_show = Bonsai.toggle ~default_model:false graph in + let main_view = + match%sub show with + | true -> + Bonsai.Edge.lifecycle + ~on_activate: + (let%arr log = log in + log "🚀") + ~on_deactivate: + (let%arr log = log in + log "🔥") + graph; + Vdom.Node.text [%string "Active!!!!"] |> Bonsai.return + | false -> Vdom.Node.text "Nothing to see here..." |> Bonsai.return + in + let%arr log_val = log_val + and toggle_show = toggle_show + and main_view = main_view in + Vdom.Node.( + div + [ div + [ button + ~attrs:[ Vdom.Attr.on_click (fun _ -> Effect.all_unit [ toggle_show ]) ] + [ text "toggle show" ] + ; text log_val + ] + ; main_view + ]) +;; + +(* $MDX part-end *) + +let () = Util.run lifecycle_demo ~id:"lifecycle" diff --git a/examples/bonsai_guide_code/lifecycle_examples.mli b/examples/bonsai_guide_code/lifecycle_examples.mli new file mode 100644 index 00000000..bc0db47b --- /dev/null +++ b/examples/bonsai_guide_code/lifecycle_examples.mli @@ -0,0 +1 @@ +(* this module is used only for side-effects *) diff --git a/examples/bonsai_guide_code/main.ml b/examples/bonsai_guide_code/main.ml index db2992ef..95f8fd4e 100644 --- a/examples/bonsai_guide_code/main.ml +++ b/examples/bonsai_guide_code/main.ml @@ -11,12 +11,24 @@ open! Bonsai_web - Some code is written non-idiomatically because the ordering of the guide makes it undesierable to use concepts that weren't explained yet *) +module _ = Bonsai_types +module _ = Intro_examples module _ = Vdom_examples -module _ = Dynamism_examples +module _ = Incrementality_examples module _ = State_examples -module _ = Form_examples module _ = Effect_examples -module _ = Flow_examples +module _ = Control_flow_examples + +(* How-tos *) + module _ = Css_examples -module _ = Edge_examples +module _ = Edge_triggered_examples +module _ = Effect_stale_examples +module _ = Form_examples +module _ = Higher_order_examples +module _ = Lifecycle_examples module _ = Rpc_examples +module _ = Scope_model_examples +module _ = State_reset_examples +module _ = Time_examples +module _ = Url_var_examples diff --git a/examples/bonsai_guide_code/resize_iframe.js b/examples/bonsai_guide_code/resize_iframe.js index 47442a6d..ae08b5b4 100644 --- a/examples/bonsai_guide_code/resize_iframe.js +++ b/examples/bonsai_guide_code/resize_iframe.js @@ -1,9 +1,7 @@ window.addEventListener('DOMContentLoaded', function () { - var resizeObserver = new ResizeObserver(function (e) { - console.log(e); + var resizeObserver = new ResizeObserver(function () { var height = document.body.scrollHeight; - if (height == 0) {return;} - console.log(height); + if (height == 0) { return; } var message = { height: height, hash: window.location.hash diff --git a/examples/bonsai_guide_code/rpc_examples.ml b/examples/bonsai_guide_code/rpc_examples.ml index 5f975864..85929dd9 100644 --- a/examples/bonsai_guide_code/rpc_examples.ml +++ b/examples/bonsai_guide_code/rpc_examples.ml @@ -1,182 +1,21 @@ open! Core open! Async_kernel -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax -open Async_rpc_kernel +module Lib = Bonsai_guide_code_lib.Rpc_examples -(* $MDX part-begin=protocol-1 *) -let double_rpc = - Rpc.Rpc.create - ~name:"double" - ~version:0 - ~bin_query:[%bin_type_class: int] - ~bin_response:[%bin_type_class: int] - ~include_in_error_count:Only_on_exn -;; - -(* $MDX part-end *) - -(* $MDX part-begin=implementation-1 *) -let double_implementation = - Rpc.Rpc.implement' double_rpc (fun _connection_state query -> Int.max 1 (query * 2)) -;; - -(* $MDX part-end *) - -let connector = - Rpc_effect.Connector.for_test - (Rpc.Implementations.create_exn - ~implementations:[ double_implementation ] - ~on_unknown_rpc:`Raise) - ~connection_state:Fn.id -;; - -type Rpc_effect.Where_to_connect.Custom.t += Connection - -(* Below is some sleight-of-hand. We want the readers of the guide to think that - we are using [Self], but we don't *actually* want to do that, since it would - require having a server to connect to. Thus, we shadow the text we want to - display with the value we want use. *) - -(* $MDX part-begin=where_to_connect *) -let where_to_connect : Rpc_effect.Where_to_connect.t = Self -(* $MDX part-end *) - -let () = ignore (where_to_connect : Rpc_effect.Where_to_connect.t) -let where_to_connect : Rpc_effect.Where_to_connect.t = Custom Connection - -(* $MDX part-begin=client-1 *) -let app = - let%sub dispatch_double_rpc = Rpc_effect.Rpc.dispatcher double_rpc ~where_to_connect in - let%sub number, set_number = Bonsai.state ~equal:[%equal: int] 1 in - let%arr dispatch_double_rpc = dispatch_double_rpc - and number = number - and set_number = set_number in - Vdom.Node.div - [ Vdom.Node.div [ Vdom.Node.text [%string "The number is: %{number#Int}"] ] - ; Vdom.Node.button - ~attrs: - [ Vdom.Attr.on_click (fun _ -> - match%bind.Effect dispatch_double_rpc number with - | Ok doubled_number -> set_number doubled_number - | Error error -> Effect.of_sync_fun eprint_s [%sexp (error : Error.t)]) - ] - [ Vdom.Node.text "Double the number" ] - ] -;; - -(* $MDX part-end *) +(* The code for these examples is defined inside `lib`, since we want to test it. *) let () = Util.run - ~custom_connector:(function - | Connection -> connector - | _ -> Rpc_effect.Connector.test_fallback) - app + ~custom_connector:Lib.custom_connector + Lib.double_number_app ~id:"double-the-number-rpc" ;; -(* $MDX part-begin=protocol-2 *) -module Current_time = struct - include String - include Legacy_diffable.Atomic.Make (String) -end - -let current_time_rpc = - Polling_state_rpc.create - ~name:"current_time" - ~version:0 - ~query_equal:[%equal: string] - ~bin_query:[%bin_type_class: string] - (module Current_time) -;; - -(* $MDX part-end *) - -(* $MDX part-begin=implementation-2 *) -let current_time_implementation = - Polling_state_rpc.implement - ~on_client_and_server_out_of_sync:print_s - current_time_rpc - (fun _connection_state zone -> - Deferred.return - (Time_ns.to_string_trimmed ~zone:(Timezone.of_string zone) (Time_ns.now ()))) - |> Rpc.Implementation.lift ~f:(fun connection_state -> - connection_state, connection_state) -;; - -(* $MDX part-end *) - -let connector = - Rpc_effect.Connector.for_test - (Rpc.Implementations.create_exn - ~implementations:[ current_time_implementation ] - ~on_unknown_rpc:`Raise) - ~connection_state:Fn.id -;; - -module Css = [%css stylesheet {| -.error_text { - color: red; -} -|}] - -let zone_form = - let module Form = Bonsai_web_ui_form.With_automatic_view in - let%sub form = - Form.Elements.Textbox.string - ~placeholder:(Value.return "timezone") - ~allow_updates_when_focused:`Always - () - in - let%sub form = Form.Dynamic.with_default (Value.return "America/New_York") form in - let%arr form = form in - let value = Form.value_or_default ~default:"America/New_York" form in - let view = Form.view_as_vdom form in - value, view -;; - -(* $MDX part-begin=client-2 *) -let app = - let%sub zone, zone_view = zone_form in - let%sub { last_ok_response; last_error; inflight_query = _; refresh = _ } = - Rpc_effect.Polling_state_rpc.poll - current_time_rpc - ~equal_query:[%equal: string] - ~equal_response:[%equal: Current_time.t] - ~where_to_connect - ~every:(Time_ns.Span.of_sec 0.1) - zone - in - let%arr last_ok_response = last_ok_response - and last_error = last_error - and zone_view = zone_view in - let text = - match last_ok_response with - | Some (zone, current_time) -> - [%string "The current time in the zone '%{zone}' is %{current_time}"] - | None -> "Loading..." - in - let error_view = - match last_error with - | Some (zone, error) -> - Vdom.Node.div - ~attrs:[ Css.error_text ] - [ Vdom.Node.text [%string "Got error when requesting time in zone '%{zone}'"] - ; Vdom.Node.pre [ Vdom.Node.text (Error.to_string_hum error) ] - ] - | None -> Vdom.Node.none - in - Vdom.Node.div [ zone_view; Vdom.Node.div [ Vdom.Node.text text ]; error_view ] -;; - -(* $MDX part-end *) - let () = Util.run - ~custom_connector:(function - | Connection -> connector - | _ -> Rpc_effect.Connector.test_fallback) - app + ~custom_connector:Lib.custom_connector + Lib.current_time_app ~id:"poll-the-current-time" ;; diff --git a/examples/bonsai_guide_code/scope_model_examples.ml b/examples/bonsai_guide_code/scope_model_examples.ml new file mode 100644 index 00000000..c642c2a4 --- /dev/null +++ b/examples/bonsai_guide_code/scope_model_examples.ml @@ -0,0 +1,63 @@ +open! Core +open! Async_kernel +open! Bonsai_web.Cont +open Bonsai.Let_syntax + +(* $MDX part-begin=counters_for_users_assoc *) + +let counters_for_users_assoc graph : Vdom.Node.t Bonsai.t = + let users = + [ "Alice", (); "Bob", (); "Charlie", () ] |> String.Map.of_alist_exn |> Bonsai.return + in + let counters = + Bonsai.assoc + (module String) + users + ~f:(fun _ _ graph -> State_examples.counter_ui graph) + graph + in + let%arr counters = counters in + Vdom.Node.table + (counters + |> Map.to_alist + |> List.map ~f:(fun (key, vdom) -> + let open Vdom.Node in + let name = td [ Vdom.Node.text key ] in + let counter = td [ vdom ] in + Vdom.Node.tr [ name; counter ])) +;; + +(* $MDX part-end *) + +let () = Util.run counters_for_users_assoc ~id:"counters_for_users_assoc" + +(* $MDX part-begin=counters_for_users_scoped *) +module Form = Bonsai_web_ui_form.With_automatic_view + +let counters_for_users_scoped graph : Vdom.Node.t Bonsai.t = + let form = + Form.Elements.Dropdown.list + (module String) + ~equal:[%equal: String.t] + (Bonsai.return [ "Alice"; "Bob"; "Charlie" ]) + graph + in + let active_user = + let%arr form = form in + Form.value_or_default form ~default:"Alice" + in + Bonsai.scope_model + (module String) + ~on:active_user + graph + ~for_:(fun graph -> + let%arr counter = State_examples.counter_ui graph + and name = active_user + and form = form in + Vdom.Node.div + [ Form.view_as_vdom form; Vdom.Node.p [ Vdom.Node.text name ]; counter ]) +;; + +(* $MDX part-end *) + +let () = Util.run counters_for_users_scoped ~id:"counters_for_users_scoped" diff --git a/examples/bonsai_guide_code/scope_model_examples.mli b/examples/bonsai_guide_code/scope_model_examples.mli new file mode 100644 index 00000000..bc0db47b --- /dev/null +++ b/examples/bonsai_guide_code/scope_model_examples.mli @@ -0,0 +1 @@ +(* this module is used only for side-effects *) diff --git a/examples/bonsai_guide_code/state_examples.ml b/examples/bonsai_guide_code/state_examples.ml index 7a16cd93..1675034b 100644 --- a/examples/bonsai_guide_code/state_examples.ml +++ b/examples/bonsai_guide_code/state_examples.ml @@ -1,117 +1,159 @@ open! Core open! Async_kernel -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax -(* $MDX part-begin=textbox *) -let textbox : (string * Vdom.Node.t) Computation.t = - let%sub state, set_state = Bonsai.state "" in - let%arr state = state - and set_state = set_state in +(* $MDX part-begin=counter *) +let counter graph : Vdom.Node.t Bonsai.t * int Bonsai.t = + let count, set_count = Bonsai.state 0 graph in let view = - Vdom.Node.input - ~attrs: - [ Vdom.Attr.value_prop state - ; Vdom.Attr.on_input (fun _ new_text -> set_state new_text) - ] - () + let%arr count = count + and set_count = set_count in + (* view-construction logic *) + Vdom.Node.div + [ Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> set_count (count - 1)) ] + [ Vdom.Node.text "-1" ] + ; Vdom.Node.text [%string "Counter value: %{count#Int}"] + ; Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> set_count (count + 1)) ] + [ Vdom.Node.text "+1" ] + ] in - state, view + view, count ;; (* $MDX part-end *) -let () = Util.run (textbox |> Computation.map ~f:snd) ~id:"textbox" - -(* $MDX part-begin=two_textboxes *) -let two_textboxes : Vdom.Node.t Computation.t = - let%sub textbox_a = textbox in - let%sub textbox_b = textbox in - let%arr contents_a, view_a = textbox_a - and contents_b, view_b = textbox_b in - let display = Vdom.Node.textf "a: %s, b: %s" contents_a contents_b in - Vdom.Node.div - ~attrs:[ Vdom.Attr.style (Css_gen.display `Inline_grid) ] - [ view_a; view_b; display ] +let counter_ui graph = + let view, _ = counter graph in + view +;; + +let () = Util.run counter_ui ~id:"counter_ui" + +(* $MDX part-begin=two_counters_correct *) +let two_counters graph = + let counter1, _count1 = counter graph in + let counter2, _count2 = counter graph in + let%arr counter1 = counter1 + and counter2 = counter2 in + Vdom.Node.div [ counter1; counter2 ] ;; (* $MDX part-end *) -let () = Util.run two_textboxes ~id:"two_textboxes" - -(* $MDX part-begin=two_textboxes_shared_state *) -let two_textboxes_shared_state : Vdom.Node.t Computation.t = - let%sub textbox_a = textbox in - let textbox_b = textbox_a in - let%arr contents_a, view_a = textbox_a - and contents_b, view_b = textbox_b in - let display = Vdom.Node.textf "a: %s, b: %s" contents_a contents_b in - Vdom.Node.div - ~attrs:[ Vdom.Attr.style (Css_gen.display `Inline_grid) ] - [ view_a; view_b; display ] +let () = Util.run two_counters ~id:"two_counters_correct" + +(* $MDX part-begin=two_counters_wrong_1 *) +let two_counters_wrong_1 graph = + let counter, _count = counter graph in + let%arr counter1 = counter + and counter2 = counter in + Vdom.Node.div [ counter1; counter2 ] ;; (* $MDX part-end *) -let () = Util.run two_textboxes_shared_state ~id:"two_textboxes_shared_state" - -(* $MDX part-begin=counter_state *) -let state_based_counter : Vdom.Node.t Computation.t = - let%sub state, set_state = Bonsai.state 0 in - let%arr state = state - and set_state = set_state in - let decrement = - Vdom.Node.button - ~attrs:[ Vdom.Attr.on_click (fun _ -> set_state (state - 1)) ] - [ Vdom.Node.text "-1" ] - in - let increment = - Vdom.Node.button - ~attrs:[ Vdom.Attr.on_click (fun _ -> set_state (state + 1)) ] - [ Vdom.Node.text "+1" ] - in - Vdom.Node.div [ decrement; Vdom.Node.textf "%d" state; increment ] +let () = Util.run two_counters_wrong_1 ~id:"two_counters_wrong_1" + +(* $MDX part-begin=two_counters_wrong_2 *) +let two_counters_wrong_2 graph = + let counter, _count = counter graph in + let%arr counter = counter in + Vdom.Node.div [ counter; counter ] ;; (* $MDX part-end *) -let () = Util.run state_based_counter ~id:"state_based_counter" +let () = Util.run two_counters_wrong_2 ~id:"two_counters_wrong_2" (* $MDX part-begin=counter_state_machine *) -module Action = struct - type t = - | Increment - | Decrement - [@@deriving sexp_of] -end - -let counter_state_machine : Vdom.Node.t Computation.t = - let%sub state, inject = +let counter_state_machine graph : Vdom.Node.t Bonsai.t * int Bonsai.t = + let count, inject = Bonsai.state_machine0 - () - ~sexp_of_action:[%sexp_of: Action.t] ~default_model:0 ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model action -> - match action with - | Increment -> model + 1 - | Decrement -> model - 1) + match action with + | `Increment -> model + 1 + | `Decrement -> model - 1) + graph in - let%arr state = state - and inject = inject in - let decrement = - Vdom.Node.button - ~attrs:[ Vdom.Attr.on_click (fun _ -> inject Decrement) ] - [ Vdom.Node.text "-1" ] + let view = + let%arr count = count + and inject = inject in + Vdom.Node.div + [ Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> inject `Decrement) ] + [ Vdom.Node.text "-1" ] + ; Vdom.Node.text [%string "Counter value: %{count#Int}"] + ; Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> inject `Increment) ] + [ Vdom.Node.text "+1" ] + ] in - let increment = - Vdom.Node.button - ~attrs:[ Vdom.Attr.on_click (fun _ -> inject Increment) ] - [ Vdom.Node.text "+1" ] + view, count +;; + +(* $MDX part-end *) + +let () = + Util.run + (fun graph -> counter_state_machine graph |> Tuple2.get1) + ~id:"counter_state_machine" +;; + +(* $MDX part-begin=counter_state_machine1 *) + +let counter_state_machine1 ~(step : int Bonsai.t) graph = + let count, inject = + Bonsai.state_machine1 + ~default_model:0 + ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) input model action -> + match input with + | Bonsai.Computation_status.Inactive -> + (* This state machine is inactive, so it can't access the current value of [input]. + Just keep the original model *) + model + | Active step -> + (match action with + | `Increment -> model + step + | `Decrement -> model - step)) + step + graph in - Vdom.Node.div [ decrement; Vdom.Node.textf "%d" state; increment ] + let view = + let%arr step = step + and count = count + and inject = inject in + Vdom.Node.div + [ Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> inject `Decrement) ] + [ Vdom.Node.text [%string "-%{step#Int}"] ] + ; Vdom.Node.text [%string "Counter value: %{count#Int}"] + ; Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> inject `Increment) ] + [ Vdom.Node.text [%string "+%{step#Int}"] ] + ] + in + view, count +;; + +(* $MDX part-end *) + +(* $MDX part-begin=counter_state_machine_chained *) +let counter_state_machine_chained graph = + let counter1, count1 = counter_state_machine1 ~step:(Bonsai.return 1) graph in + let counter2, count2 = counter_state_machine1 ~step:count1 graph in + let counter3, _ = counter_state_machine1 ~step:count2 graph in + let%arr counter1 = counter1 + and counter2 = counter2 + and counter3 = counter3 in + Vdom.Node.div [ counter1; counter2; counter3 ] ;; (* $MDX part-end *) -let () = Util.run counter_state_machine ~id:"counter_state_machine" +let () = Util.run counter_state_machine_chained ~id:"counter_state_machine_chained" +let counter = counter_state_machine1 diff --git a/examples/bonsai_guide_code/state_examples.mli b/examples/bonsai_guide_code/state_examples.mli index bb80f8b7..ca248cc4 100644 --- a/examples/bonsai_guide_code/state_examples.mli +++ b/examples/bonsai_guide_code/state_examples.mli @@ -1,5 +1,6 @@ open! Core open! Async_kernel -open! Bonsai_web +open! Bonsai_web.Cont -val counter_state_machine : Vdom.Node.t Computation.t +val counter : step:int Bonsai.t -> Bonsai.graph -> Vdom.Node.t Bonsai.t * int Bonsai.t +val counter_ui : Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/bonsai_guide_code/state_reset_examples.ml b/examples/bonsai_guide_code/state_reset_examples.ml new file mode 100644 index 00000000..932fe305 --- /dev/null +++ b/examples/bonsai_guide_code/state_reset_examples.ml @@ -0,0 +1,392 @@ +open! Core +open! Bonsai_web.Cont +open! Bonsai.Let_syntax +module Apply_action_context = Bonsai.Apply_action_context + +(* $MDX part-begin=resettable_counters *) +let two_counters graph = + let%arr counter1 = State_examples.counter_ui graph + and counter2 = State_examples.counter_ui graph in + Vdom.Node.div + ~attrs:[ [%css {|border: 1px solid black; padding: 4px|}] ] + [ counter1; counter2 ] +;; + +let reset_ui graph ~f = + let view, reset = Bonsai.with_model_resetter ~f graph in + let%arr view = view + and reset = reset in + Vdom.Node.div + [ view + ; Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> reset) ] + [ Vdom.Node.text "Reset" ] + ] +;; + +let resettable_counters = reset_ui ~f:two_counters + +(* $MDX part-end *) + +let () = Util.run resettable_counters ~id:"resettable_counters" + +(* $MDX part-begin=resettable_counters_from_inside *) +let resettable_counters_from_inside graph = + Bonsai.with_model_resetter' + ~f:(fun ~reset graph -> + let%arr counter1 = State_examples.counter_ui graph + and counter2 = State_examples.counter_ui graph + and reset = reset in + Vdom.Node.div + ~attrs:[ [%css {|border: 1px solid black; padding: 4px|}] ] + [ counter1 + ; Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> reset) ] + [ Vdom.Node.text "Reset" ] + ; counter2 + ]) + graph +;; + +(* $MDX part-end *) + +let () = Util.run resettable_counters_from_inside ~id:"resettable_counters_from_inside" + +(* $MDX part-begin=connection_type *) +module Connection : sig + (* Some generic connection type that doesn't know of Bonsai *) + type t + + (* Connect to some resource *) + val create : uri:string -> t + + (* Notify subscribers whenever the connection status changes *) + val on_status_change + : t + -> ([ `Connected | `Connecting | `Disconnected ] -> unit Effect.t) + -> unit Effect.t +end +(* $MDX part-end *) +(*_ This comment used for spacing MDX comment above *) = struct + open Async_kernel + + type t = unit + + let create ~uri:_ = () + + let on_status_change () f = + Effect.of_thunk (fun () -> + Clock_ns.every' (Time_ns.Span.of_int_sec 1) (fun () -> + Effect.Expert.handle_non_dom_event_exn (f `Connecting); + let%bind.Deferred () = Clock_ns.after (Time_ns.Span.of_int_sec 1) in + Effect.Expert.handle_non_dom_event_exn (f `Connected); + let%bind.Deferred () = Clock_ns.after (Time_ns.Span.of_int_sec 3) in + Effect.Expert.handle_non_dom_event_exn (f `Disconnected); + return ())) + ;; +end + +(* $MDX part-begin=connection_status *) +let connection_status graph conn : [ `Connected | `Connecting | `Disconnected ] Bonsai.t = + let status, set_status = Bonsai.state `Disconnected graph in + let register_status_change = + let%arr set_status = set_status in + Connection.on_status_change conn set_status + in + let () = Bonsai.Edge.lifecycle ~on_activate:register_status_change graph in + status +;; + +(* $MDX part-end *) + +(* $MDX part-begin=connection_status_ui *) +let conn = Connection.create ~uri:"https://google.com" + +let connection_status_ui graph = + let connection_status = + match%sub connection_status graph conn with + | `Connected -> Bonsai.return (Vdom.Node.div [ Vdom.Node.text "Connected" ]) + | `Connecting -> Bonsai.return (Vdom.Node.div [ Vdom.Node.text "Connecting" ]) + | `Disconnected -> Bonsai.return (Vdom.Node.div [ Vdom.Node.text "Disconnected" ]) + in + let%arr connection_status = connection_status in + Vdom.Node.div [ connection_status ] +;; + +(* $MDX part-end *) +let () = Util.run connection_status_ui ~id:"connection_status_ui" + +(* $MDX part-begin=resettable_connection_and_counters *) +let connection_and_counters graph = + let%arr connection_status_ui = connection_status_ui graph + and counters = two_counters graph in + Vdom.Node.div [ connection_status_ui; counters ] +;; + +let resettable_ui = reset_ui ~f:connection_and_counters + +(* $MDX part-end *) + +let () = Util.run resettable_ui ~id:"resettable_connection_and_counters" + +(* $MDX part-begin=connection_status_reset *) +let connection_status graph conn : [ `Connected | `Connecting | `Disconnected ] Bonsai.t = + let status, set_status = + Bonsai.state ~reset:(fun model_when_reset -> model_when_reset) `Disconnected graph + in + let register_status_change = + let%arr set_status = set_status in + Connection.on_status_change conn set_status + in + let () = Bonsai.Edge.lifecycle ~on_activate:register_status_change graph in + status +;; + +(* $MDX part-end *) + +let connection_status_ui graph = + let connection_status = + match%sub connection_status graph conn with + | `Connected -> Bonsai.return (Vdom.Node.div [ Vdom.Node.text "Connected" ]) + | `Connecting -> Bonsai.return (Vdom.Node.div [ Vdom.Node.text "Connecting" ]) + | `Disconnected -> Bonsai.return (Vdom.Node.div [ Vdom.Node.text "Disconnected" ]) + in + let%arr connection_status = connection_status in + Vdom.Node.div [ connection_status ] +;; + +let connection_and_counters graph = + let%arr connection_status_ui = connection_status_ui graph + and counters = two_counters graph in + Vdom.Node.div [ connection_status_ui; counters ] +;; + +let () = + Util.run + (reset_ui ~f:connection_and_counters) + ~id:"proper_reset_ui_with_connection_status" +;; + +(* $MDX part-begin=exchange_type *) +module Exchange : sig + module Order_id = Int + + (* A connection to an exchange *) + type t + type event = Fill of Order_id.t + + val create : unit -> t + + (* Sends an order to the exchange *) + val send_order : t -> Order_id.t -> unit Effect.t + + (* Cancels an open order on the exchange *) + val cancel_order : t -> Order_id.t -> unit Effect.t + + (* Subscribe to notifications of which orders have been filled *) + val subscribe : t -> (event -> unit Effect.t) -> unit Effect.t +end +(* $MDX part-end *) +(* _ This comment used for spacing MDX comment above *) = struct + open Async_kernel + module Order_id = Int + + (* All of this code is pretty hacky and only to enable the demo *) + + type event = Fill of Order_id.t + + type t = + { orders : int Order_id.Table.t + ; mutable subscription : event -> unit Effect.t + } + + let create () = + { orders = Order_id.Table.create (); subscription = (fun _ -> Effect.Ignore) } + ;; + + let seq = ref 0 + + let next_seq () = + let res = !seq in + incr seq; + res + ;; + + let fill_in_3_seconds t (order_id, seq) = + upon + (Clock_ns.after (Time_ns.Span.of_int_sec 3)) + (fun () -> + match Hashtbl.find t.orders order_id with + | Some seq' when seq' = seq -> + Effect.Expert.handle_non_dom_event_exn (t.subscription (Fill order_id)) + | _ -> ()) + ;; + + (* No checking for duplicate order ids, seems fine. *) + let send_order t id = + Effect.of_thunk (fun () -> + let seq = next_seq () in + Hashtbl.set t.orders ~key:id ~data:seq; + fill_in_3_seconds t (id, seq)) + ;; + + let cancel_order t id = Effect.of_thunk (fun () -> Hashtbl.remove t.orders id) + let subscribe t f = Effect.of_thunk (fun () -> t.subscription <- f) +end + +open Exchange + +(* $MDX part-begin=order_manager_definition *) +module Model = struct + type t = + { open_orders : Order_id.Set.t + ; filled_orders : Order_id.t list + ; next_order_id : int + } + + let empty = { open_orders = Order_id.Set.empty; filled_orders = []; next_order_id = 0 } +end + +module Action = struct + type t = + | Create_ui_order + | Received_fill of Exchange.Order_id.t +end + +let order_manager (exchange : Exchange.t) graph = + let model, inject_action = + Bonsai.state_machine0 + ~default_model:Model.empty + ~apply_action: + (fun + context { open_orders; next_order_id; filled_orders } (action : Action.t) -> + match action with + | Create_ui_order -> + let this_order_id = next_order_id in + let open_orders = Set.add open_orders this_order_id in + Apply_action_context.schedule_event + context + (Exchange.send_order exchange this_order_id); + { open_orders; next_order_id = this_order_id + 1; filled_orders } + | Received_fill order_id -> + let filled_orders = filled_orders @ [ order_id ] in + let open_orders = Set.remove open_orders order_id in + { open_orders; next_order_id; filled_orders }) + graph + in + let subscribe_to_exchange = + let%arr inject_action = inject_action in + Exchange.subscribe exchange (fun (Fill order_id) -> + inject_action (Received_fill order_id)) + in + let () = Bonsai.Edge.lifecycle ~on_activate:subscribe_to_exchange graph in + let inject_new_order = + let%arr inject_action = inject_action in + inject_action Create_ui_order + in + let open_orders = + let%arr { open_orders; _ } = model in + open_orders + in + let filled_orders = + let%arr { filled_orders; _ } = model in + filled_orders + in + open_orders, filled_orders, inject_new_order +;; + +(* $MDX part-end *) + +(* $MDX part-begin=trading_ui *) +let trading_ui exchange graph = + let open_orders, filled_orders, inject_new_order = order_manager exchange graph in + let%arr open_orders = open_orders + and filled_orders = filled_orders + and inject_new_order = inject_new_order in + Vdom.Node.div + [ Vdom.Node.text [%string "Open orders:"] + ; Vdom.Node.sexp_for_debugging [%sexp (open_orders : Order_id.Set.t)] + ; Vdom.Node.text [%string "Filled orders:"] + ; Vdom.Node.sexp_for_debugging [%sexp (filled_orders : Order_id.t list)] + ; Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> inject_new_order) ] + [ Vdom.Node.text "New order" ] + ] +;; + +(* $MDX part-end *) + +let exchange = Exchange.create () +let () = Util.run (trading_ui exchange) ~id:"trading_ui" +let () = Util.run (reset_ui ~f:(trading_ui exchange)) ~id:"trading_ui_reset" + +let order_manager (exchange : Exchange.t) graph = + (* $MDX part-begin=order_manager_with_reset *) + let model, inject_action = + Bonsai.state_machine0 + ~default_model:Model.empty + ~apply_action: + (fun + context { open_orders; next_order_id; filled_orders } (action : Action.t) -> + match action with + | Create_ui_order -> + let this_order_id = next_order_id in + let open_orders = Set.add open_orders this_order_id in + Apply_action_context.schedule_event + context + (Exchange.send_order exchange this_order_id); + { open_orders; next_order_id = this_order_id + 1; filled_orders } + | Received_fill order_id -> + let filled_orders = filled_orders @ [ order_id ] in + let open_orders = Set.remove open_orders order_id in + { open_orders; next_order_id; filled_orders }) + ~reset:(fun context (model : Model.t) -> + Set.iter model.open_orders ~f:(fun order_id -> + Apply_action_context.schedule_event + context + (Exchange.cancel_order exchange order_id)); + Model.empty) + graph + in + (* $MDX part-end *) + let subscribe_to_exchange = + let%arr inject_action = inject_action in + Exchange.subscribe exchange (fun (Fill order_id) -> + inject_action (Received_fill order_id)) + in + let () = Bonsai.Edge.lifecycle ~on_activate:subscribe_to_exchange graph in + let inject_new_order = + let%arr inject_action = inject_action in + inject_action Create_ui_order + in + let open_orders = + let%arr { open_orders; _ } = model in + open_orders + in + let filled_orders = + let%arr { filled_orders; _ } = model in + filled_orders + in + open_orders, filled_orders, inject_new_order +;; + +(* $MDX part-begin=trading_ui *) +let trading_ui exchange graph = + let open_orders, filled_orders, inject_new_order = order_manager exchange graph in + let%arr open_orders = open_orders + and filled_orders = filled_orders + and inject_new_order = inject_new_order in + Vdom.Node.div + [ Vdom.Node.text [%string "Open orders:"] + ; Vdom.Node.sexp_for_debugging [%sexp (open_orders : Order_id.Set.t)] + ; Vdom.Node.text [%string "Filled orders:"] + ; Vdom.Node.sexp_for_debugging [%sexp (filled_orders : Order_id.t list)] + ; Vdom.Node.button + ~attrs:[ Vdom.Attr.on_click (fun _ -> inject_new_order) ] + [ Vdom.Node.text "New order" ] + ] +;; + +(* $MDX part-end *) + +let () = Util.run (reset_ui ~f:(trading_ui exchange)) ~id:"proper_trading_ui_reset" diff --git a/examples/bonsai_guide_code/state_reset_examples.mli b/examples/bonsai_guide_code/state_reset_examples.mli new file mode 100644 index 00000000..bc0db47b --- /dev/null +++ b/examples/bonsai_guide_code/state_reset_examples.mli @@ -0,0 +1 @@ +(* this module is used only for side-effects *) diff --git a/examples/bonsai_guide_code/style.css b/examples/bonsai_guide_code/style.css deleted file mode 100644 index 5685c0ca..00000000 --- a/examples/bonsai_guide_code/style.css +++ /dev/null @@ -1,32 +0,0 @@ -body { - font-family: sans-serif; - font-size: 1.2em; -} - -ul, pre { - margin: 0; -} - -.politicians table { - border-collapse: collapse; - border: 1px solid brown; -} - -.politicians table td { - padding: 4px; -} - -.politicians table thead { - text-align: center; - background: brown; - color: antiquewhite; - font-weight: bold; -} - -.politicians table tr { - background: antiquewhite; -} - -.politicians table tr:nth-child(even) { - background: wheat; -} diff --git a/examples/bonsai_guide_code/test/dune b/examples/bonsai_guide_code/test/dune deleted file mode 100644 index 414c7362..00000000 --- a/examples/bonsai_guide_code/test/dune +++ /dev/null @@ -1,6 +0,0 @@ -(library - (name bonsai_guide_code_test) - (public_name bonsai.guide_code_test) - (libraries bonsai_web_test async_js.async_test) - (preprocess - (pps ppx_jane ppx_bonsai))) diff --git a/examples/bonsai_guide_code/test/rpc_examples_test.mli b/examples/bonsai_guide_code/test/rpc_examples_test.mli deleted file mode 100644 index e801e1ce..00000000 --- a/examples/bonsai_guide_code/test/rpc_examples_test.mli +++ /dev/null @@ -1 +0,0 @@ -(*_ This file is intentionally blank *) diff --git a/examples/bonsai_guide_code/time_examples.ml b/examples/bonsai_guide_code/time_examples.ml new file mode 100644 index 00000000..8f34d185 --- /dev/null +++ b/examples/bonsai_guide_code/time_examples.ml @@ -0,0 +1,10 @@ +open! Core +open! Bonsai_web.Cont +open! Bonsai.Let_syntax +module Lib = Bonsai_guide_code_lib.Time_examples + +let () = Util.run Lib.current_time ~id:"clock_now" +let () = Util.run Lib.approx_current_time ~id:"clock_approx_now" +let () = Util.run Lib.measure_time ~id:"current_time_effect" +let () = Util.run Lib.clock_sleep_demo ~id:"clock_sleep" +let () = Util.run Lib.clock_every_demo ~id:"clock_every" diff --git a/examples/bonsai_guide_code/time_examples.mli b/examples/bonsai_guide_code/time_examples.mli new file mode 100644 index 00000000..bc0db47b --- /dev/null +++ b/examples/bonsai_guide_code/time_examples.mli @@ -0,0 +1 @@ +(* this module is used only for side-effects *) diff --git a/examples/bonsai_guide_code/url_var_examples.ml b/examples/bonsai_guide_code/url_var_examples.ml new file mode 100644 index 00000000..80c0a42a --- /dev/null +++ b/examples/bonsai_guide_code/url_var_examples.ml @@ -0,0 +1,40 @@ +open! Core +open! Bonsai_web.Cont +open! Bonsai.Let_syntax +module Url_var = Bonsai_web_ui_url_var + +module My_google_clone = struct + (* $MDX part-begin=type *) + type t = + | Homepage + | Search of string + [@@deriving sexp, equal] + (* $MDX part-end *) + + (* $MDX part-begin=parse_unparse *) + let parse_exn ({ path; query; _ } : Url_var.Components.t) : t = + let path = String.split path ~on:'/' in + match path with + | [ "home" ] -> Homepage + | [ "search" ] -> + (match Map.find (query : _ String.Map.t) "q" with + | Some [ query ] -> Search query + | None | Some [] -> failwith "search missing query param" + | Some (_ :: _ :: _) -> failwith "search with too many query params") + | _ -> failwith "unknown path" + ;; + + let unparse (t : t) : Url_var.Components.t = + match t with + | Homepage -> Url_var.Components.create ~path:"home" () + | Search query -> + Url_var.Components.create + ~path:"search" + ~query:(String.Map.singleton "q" [ query ]) + () + ;; + (* $MDX part-end *) +end + +let () = ignore My_google_clone.parse_exn +let () = ignore My_google_clone.unparse diff --git a/examples/bonsai_guide_code/url_var_examples.mli b/examples/bonsai_guide_code/url_var_examples.mli new file mode 100644 index 00000000..bc0db47b --- /dev/null +++ b/examples/bonsai_guide_code/url_var_examples.mli @@ -0,0 +1 @@ +(* this module is used only for side-effects *) diff --git a/examples/bonsai_guide_code/util.ml b/examples/bonsai_guide_code/util.ml index 44b22ce9..ca7d8d89 100644 --- a/examples/bonsai_guide_code/util.ml +++ b/examples/bonsai_guide_code/util.ml @@ -1,6 +1,5 @@ open! Core -open! Bonsai_web -open Virtual_dom.Vdom +open! Bonsai_web.Cont open Js_of_ocaml let () = Async_js.init () @@ -11,20 +10,9 @@ let run ?custom_connector ~id computation = let current_hash = Dom_html.window##.location##.hash |> Js.to_string in print_s [%message (current_hash : string) (id : string)]; if String.equal current_hash ("#" ^ id) - then Bonsai_web.Start.start ?custom_connector computation + then Start.start ?custom_connector computation else () ;; -let run_vdom ?(include_html = false) vdom = - let vdom = - if include_html - then ( - let as_text = - let vdom = Node.span [ vdom ] in - (Node.to_dom vdom)##.innerHTML |> Js.to_string - in - Node.div [ Node.pre [ Node.text as_text ]; vdom ]) - else vdom - in - run (Bonsai.const vdom) -;; +let run_vdom_val vdom = run (fun _ -> vdom) +let run_vdom vdom = run (fun _ -> Bonsai.return vdom) diff --git a/examples/bonsai_guide_code/util.mli b/examples/bonsai_guide_code/util.mli index 09b097d3..5c11da93 100644 --- a/examples/bonsai_guide_code/util.mli +++ b/examples/bonsai_guide_code/util.mli @@ -1,10 +1,11 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont val run : ?custom_connector:(Rpc_effect.Where_to_connect.Custom.t -> Rpc_effect.Connector.t) -> id:string - -> Vdom.Node.t Computation.t + -> (Bonsai.graph -> Vdom.Node.t Bonsai.t) -> unit -val run_vdom : ?include_html:bool -> Vdom.Node.t -> id:string -> unit +val run_vdom_val : Vdom.Node.t Bonsai.t -> id:string -> unit +val run_vdom : Vdom.Node.t -> id:string -> unit diff --git a/examples/bonsai_guide_code/vdom_examples.ml b/examples/bonsai_guide_code/vdom_examples.ml index 4c8e0213..9e21c536 100644 --- a/examples/bonsai_guide_code/vdom_examples.ml +++ b/examples/bonsai_guide_code/vdom_examples.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont (* $MDX part-begin=hello_world *) let hello_world : Vdom.Node.t = Vdom.Node.text "hello world!" @@ -24,7 +24,6 @@ let bulleted_list : Vdom.Node.t = (* $MDX part-end *) let () = Util.run_vdom bulleted_list ~id:"bulleted_list" -let alert s = Js_of_ocaml.Dom_html.window##alert (Js_of_ocaml.Js.string s) (* $MDX part-begin=input_placeholder *) let input_placeholder : Vdom.Node.t = @@ -34,31 +33,23 @@ let input_placeholder : Vdom.Node.t = (* $MDX part-end *) let () = Util.run_vdom input_placeholder ~id:"input_placeholder" -(* $MDX part-begin=css_gen *) -let css_gen : Vdom.Node.t = - Vdom.Node.span - ~attrs:[ Vdom.Attr.style (Css_gen.color (`Name "red")) ] - [ Vdom.Node.text "this text is red" ] +(* $MDX part-begin=css *) +let css : Vdom.Node.t = + Vdom.Node.span ~attrs:[ [%css {|color: red;|}] ] [ Vdom.Node.text "this text is red" ] ;; (* $MDX part-end *) -let () = Util.run_vdom css_gen ~id:"css_gen" +let () = Util.run_vdom css ~id:"css" type mouse_event = Js_of_ocaml.Dom_html.mouseEvent Js_of_ocaml.Js.t -(* Running side-effects directly inside an event-handler is not ideal, but I - didn't want to take a dependency on "Effect"s before introducing Events. - - In real code, prefer passing in an "inject_alert" function that is backed by - [Effect.of_sync_fun]. That would make this component testable ([alert] is not - supported in nodejs) *) (* $MDX part-begin=clicky_button *) let clicky : Vdom.Node.t = Vdom.Node.button ~attrs: [ Vdom.Attr.on_click (fun (_evt : mouse_event) -> - alert "hello there!"; - Ui_effect.Ignore) + (* Alerts are generally bad UI; there's an `Effect.print_s` for logging *) + Effect.alert "hello there!") ] [ Vdom.Node.text "click me!" ] ;; diff --git a/examples/bonsai_view/main.ml b/examples/bonsai_view/main.ml index c6d3e473..bb8bbd0b 100644 --- a/examples/bonsai_view/main.ml +++ b/examples/bonsai_view/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax module Form = Bonsai_web_ui_form.With_automatic_view module Gallery = Bonsai_web_ui_gallery @@ -15,8 +15,8 @@ module Text = struct When invoked without any optional parameters, it will produce a dom Text node. |} ;; - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in [%demo View.themed_text theme "hello world"] ;; @@ -32,8 +32,8 @@ module Text_with_style_and_size = struct that are themable and consistent across different components. |} ;; - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in [%demo vbox [ View.themed_text theme ?style:None ?size:None "default style and size" @@ -58,8 +58,8 @@ module Text_with_intent = struct to give the theme the ability to style it differently. |} ;; - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in [%demo vbox [ View.themed_text theme ?intent:None "no intent" @@ -85,8 +85,8 @@ module Table = struct extracts the cell-specific data, and a renderer for that cell-specific data. |} ;; - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in let module T = [%demo [@@@ocamlformat "disable"] @@ -137,8 +137,8 @@ module Table_with_group = struct {| To make column groups, simply call the "group" function with a list of sub-columns. |} ;; - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in let module T = struct type t = { sym : string @@ -188,8 +188,8 @@ module Table_with_empty_cells = struct through it indicating that it's properly empty (as opposed to containing - for example - the empty string)|} ;; - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in let module T = struct type t = { sym : string @@ -230,8 +230,8 @@ module Tooltip = struct {| The standard tooltip function allows annotating some text with a tooltip containing another string |} ;; - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in [%demo View.tooltip theme @@ -247,8 +247,8 @@ module Tooltip_directions = struct let name = "Tooltip directions" let description = {| Tooltips can be positioned relative to the annotated content |} - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in let vbox = View.vbox ~gap:(`Em 1) in let hbox = View.hbox ~gap:(`Em 1) in [%demo @@ -284,8 +284,8 @@ module Tooltip_with_arbitrary_content = struct {| With `tooltip'` you can add a tooltip to abitrary content, and the tooltip itself can contain arbitrary content |} ;; - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in [%demo let tooltip = View.vbox @@ -308,8 +308,8 @@ module Button = struct as input in the form of a string |} ;; - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in [%demo View.button theme ~on_click:Effect.Ignore "click me"] ;; @@ -321,8 +321,8 @@ module Disabled_button = struct let name = "Disabled button" let description = {| Buttons can be disabled with an optional parameter |} - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in [%demo hbox [ View.button theme ~disabled:false ~on_click:Effect.Ignore "not disabled" @@ -338,8 +338,8 @@ module Buttons_with_intent = struct let name = "Buttons with Intent" let description = {| When given an intent, buttons can change their style |} - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in let on_click = Effect.Ignore in [%demo hbox @@ -363,8 +363,8 @@ module Disabled_buttons_with_intent = struct and their visuals change. |} ;; - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in [%demo let on_click = Effect.Ignore and disabled = true in @@ -389,8 +389,8 @@ module Badge = struct or minor status indicators. |} ;; - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in [%demo hbox [ View.badge theme ?intent:None "0" @@ -413,8 +413,8 @@ module Dismissible_badge = struct have a dismiss button. This is useful for multiselect forms. |} ;; - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in [%demo let on_dismiss = Effect.Ignore in hbox @@ -444,7 +444,7 @@ module Basic_vbox = struct let red = make_box (`Name "red") (`Px 20) (`Px 30) let green = make_box (`Name "green") (`Px 30) (`Px 50) let blue = make_box (`Name "blue") (`Px 50) (`Px 30) - let view = Bonsai.const [%demo View.vbox [ red; green; blue ]] + let view _graph = Bonsai.return [%demo View.vbox [ red; green; blue ]] let selector = None let filter_attrs = @@ -459,7 +459,7 @@ module Basic_hbox = struct let name = "Basic hbox" let description = {| The vbox function builds a horizontally stacking container. |} let red, green, blue, filter_attrs = Basic_vbox.(red, green, blue, filter_attrs) - let view = Bonsai.const [%demo View.hbox [ red; green; blue ]] + let view _graph = Bonsai.return [%demo View.hbox [ red; green; blue ]] let selector = None end @@ -477,9 +477,9 @@ module Interactive_vbox = struct let white_space_pre = Vdom.Attr.style Css_gen.(white_space `Pre) - let dropdown (type a) ?init name (module M : Bonsai.Enum with type t = a) = - Bonsai.Computation.map - (Form.Elements.Dropdown.enumerable_opt ?init (module M)) + let dropdown (type a) ?init name (module M : Bonsai.Enum with type t = a) graph = + Bonsai.map + (Form.Elements.Dropdown.enumerable_opt ?init (module M) graph) ~f:(fun form -> let value = Or_error.ok_exn (Form.value form) in let view = Form.View.to_vdom_plain (Form.view form) |> List.hd_exn in @@ -528,16 +528,17 @@ module Interactive_vbox = struct [@@deriving sexp, enumerate, equal, compare] end - let view = - let%map.Computation axis_v, axis, _ = - dropdown "function" ~init:`First_item (module Axis) - and v_direction_v, v_direction, v_f1 = dropdown "direction" (module Vertical_dir) - and h_direction_v, h_direction, h_f1 = dropdown "direction" (module Horizontal_dir) + let view graph = + let%map axis_v, axis, _ = dropdown "function" ~init:`First_item (module Axis) graph + and v_direction_v, v_direction, v_f1 = + dropdown "direction" (module Vertical_dir) graph + and h_direction_v, h_direction, h_f1 = + dropdown "direction" (module Horizontal_dir) graph and main_axis_alignment_v, main_axis_alignment, f2 = - dropdown "main_axis_alignment" (module Main_axis_alignment) + dropdown "main_axis_alignment" (module Main_axis_alignment) graph and cross_axis_alignment_v, cross_axis_alignment, f3 = - dropdown "cross_axis_alignment" (module Cross_axis_alignment) - and theme = View.Theme.current in + dropdown "cross_axis_alignment" (module Cross_axis_alignment) graph + and theme = View.Theme.current graph in let axis = Option.value axis ~default:Hbox in let (view, text), direction_v, f1 = match axis with @@ -613,8 +614,8 @@ module Basic_tabs = struct let name = "Basic Tabs" let description = "" - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in [%demo let on_change ~from:_ ~to_:_ = Effect.Ignore in View.tabs @@ -639,8 +640,8 @@ module Enumerable_tabs = struct {| If you have an enumerable type, tabs_enum can be used to generate tabs for it |} ;; - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in let module T = [%demo module Pages = struct @@ -667,8 +668,8 @@ module Devbar = struct let name = "Basic Devbar" let description = {| |} - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in let view, text = [%demo View.devbar theme "DEV"] in Vdom.Node.div ~attrs:[ Vdom.Attr.style (Css_gen.max_width (`Px 500)) ] [ view ], text ;; @@ -681,8 +682,8 @@ module Devbar_intent = struct let name = "Devbar with Intent" let description = {| |} - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map theme = View.Theme.current graph in let view, text = [%demo View.vbox @@ -705,8 +706,8 @@ module Basic_card = struct let name = "Basic Card" let description = {| |} - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map.Bonsai theme = View.Theme.current graph in let view, text = [%demo View.card theme "The message is: You are great!"] in Vdom.Node.div ~attrs:[ Vdom.Attr.style (Css_gen.max_width (`Px 500)) ] [ view ], text ;; @@ -719,8 +720,8 @@ module Card_with_title = struct let name = "Card with a title" let description = {| |} - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map.Bonsai theme = View.Theme.current graph in let view, text = [%demo View.card theme ~title:"New message!!" "The message is: You are great!"] in @@ -735,8 +736,8 @@ module Cards_with_intent = struct let name = "Cards with intent" let description = {| |} - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map.Bonsai theme = View.Theme.current graph in let view, text = [%demo View.vbox @@ -759,8 +760,8 @@ module Cards_with_fieldset = struct let name = "Cards with fieldset styling" let description = {| |} - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map.Bonsai theme = View.Theme.current graph in let view, text = [%demo View.vbox @@ -811,8 +812,8 @@ module Card_with_rows = struct {| View.card' allows for the content to be a list of arbitrary vdom nodes which are separated vertically from one another. |} ;; - let view = - let%map.Computation theme = View.Theme.current in + let view graph = + let%map.Bonsai theme = View.Theme.current graph in let view, text = [%demo hbox @@ -836,8 +837,8 @@ module Card_with_rows = struct let filter_attrs = Some (fun k _ -> not (String.is_prefix k ~prefix:"style")) end -let component = - let%sub theme, theme_picker = Gallery.Theme_picker.component () in +let component graph = + let%sub theme, theme_picker = Gallery.Theme_picker.component () graph in View.Theme.set_for_app theme (Gallery.make_sections @@ -904,6 +905,7 @@ let component = ; Gallery.make_demo (module Card_with_rows) ] ) ]) + graph ;; let () = diff --git a/examples/clipboard/main.ml b/examples/clipboard/main.ml index 00ebafa2..cd4f642c 100644 --- a/examples/clipboard/main.ml +++ b/examples/clipboard/main.ml @@ -1,12 +1,12 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Js_of_ocaml open Bonsai.Let_syntax module Form = Bonsai_web_ui_form.With_automatic_view -let component = - let%sub form = Form.Elements.Textbox.string ~allow_updates_when_focused:`Never () in - let%sub copy_button = +let component graph = + let form = Form.Elements.Textbox.string ~allow_updates_when_focused:`Never () graph in + let copy_button = let%arr form = form in Vdom.Node.button ~attrs: @@ -24,7 +24,7 @@ let component = (* NOTE: This button copies from clipboard in a deprecated and non-recommended way. Use the above button implementation as a reference if you'd like clipboard copying behavior in your app. *) - let%sub legacy_copy_button = + let legacy_copy_button = let%arr form = form in Vdom.Node.button ~attrs: @@ -37,7 +37,7 @@ let component = ] [ Vdom.Node.text "copy to clipboard (legacy behaviour)" ] in - let%sub paste_button = + let paste_button = let%arr form = form in Vdom.Node.button ~attrs: diff --git a/examples/clock_every/main.ml b/examples/clock_every/main.ml index 6d1c421e..d219dd8d 100644 --- a/examples/clock_every/main.ml +++ b/examples/clock_every/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Form = Bonsai_web_ui_form.With_automatic_view @@ -84,13 +84,13 @@ module Random_time_span = struct let default_extra_duration = 1.0 let default_chance_of_getting_extra_duration = 0.5 - let form = + let form graph = Form.Typed.Record.make (module struct module Typed_field = Typed_field - let time_span_form ~max ~default = - let%sub form = + let time_span_form ~max ~default graph = + let form = Form.Elements.Range.float ~min:0.0 ~max @@ -98,14 +98,18 @@ module Random_time_span = struct ~default ~allow_updates_when_focused:`Never () + graph in let%arr form = form in Form.project form ~parse_exn:Time_ns.Span.of_sec ~unparse:Time_ns.Span.to_sec ;; - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = function - | Base_duration -> time_span_form ~max:1.0 ~default:default_base_duration - | Extra_duration -> time_span_form ~max:2.0 ~default:default_extra_duration + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with + | Base_duration -> time_span_form ~max:1.0 ~default:default_base_duration graph + | Extra_duration -> + time_span_form ~max:2.0 ~default:default_extra_duration graph | Chance_of_getting_extra_duration -> Form.Elements.Range.float ~allow_updates_when_focused:`Never @@ -114,10 +118,12 @@ module Random_time_span = struct ~step:0.1 ~default:default_chance_of_getting_extra_duration () + graph ;; let label_for_field = `Inferred end) + graph ;; let default = @@ -255,7 +261,7 @@ let time_ns_to_string time = let time_span_to_pixels span = Int.to_float (Time_ns.Span.to_int_ms span) /. 25.0 -let timeline ~now ~(tracks : Bar.t Fdeque.t Track_id.Map.t Value.t) = +let timeline ~now ~(tracks : Bar.t Fdeque.t Track_id.Map.t Bonsai.t) graph = let timeline_width = 200.0 in let timeline_height = 100.0 in let starting_line = @@ -272,15 +278,15 @@ let timeline ~now ~(tracks : Bar.t Fdeque.t Track_id.Map.t Value.t) = ] []) in - let%sub number_of_tracks = + let number_of_tracks = let%arr tracks = tracks in Map.length (tracks : _ Track_id.Map.t) in - let%sub tracks = + let tracks = Bonsai.assoc (module Track_id) tracks - ~f:(fun track_id deque -> + ~f:(fun track_id deque _graph -> let%arr now = now and number_of_tracks = number_of_tracks and deque = deque @@ -304,8 +310,9 @@ let timeline ~now ~(tracks : Bar.t Fdeque.t Track_id.Map.t Value.t) = ; Attr.fill (`Name (bar_id_to_color id)) ] []))) + graph in - let%sub lines = + let lines = let%arr now = now in let times_where_lines_should_be_on = List.init 22 ~f:(fun i -> @@ -353,33 +360,36 @@ let clock ~description ~wait_time ~delta_time:now + graph = - let%sub next_bar_id = Bar_id.component in + let next_bar_id = Bar_id.component graph in let%sub { tracks; last_trigger_time; _ }, update_tracks = - Bonsai.state_machine1 - ~sexp_of_model:[%sexp_of: Tracks.t] - ~equal:[%equal: Tracks.t] - ~sexp_of_action:[%sexp_of: Tracks_action.t] - ~default_model:Tracks.empty - ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) now tracks action -> - match now with - | Active now -> - (match action with - | Add_bar bar_id -> Tracks.add_bar tracks ~now bar_id - | Finish_bar bar_id -> Tracks.finish_bar tracks ~now bar_id) - | Inactive -> - eprint_s - [%message - [%here] - "An action sent to a [state_machine1] has been dropped because its input \ - was not present. This happens when the [state_machine1] is inactive \ - when it receives a message." - (action : Tracks_action.t)]; - tracks) - now + Tuple2.uncurry Bonsai.both + @@ Bonsai.state_machine1 + ~sexp_of_model:[%sexp_of: Tracks.t] + ~equal:[%equal: Tracks.t] + ~sexp_of_action:[%sexp_of: Tracks_action.t] + ~default_model:Tracks.empty + ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) now tracks action -> + match now with + | Active now -> + (match action with + | Add_bar bar_id -> Tracks.add_bar tracks ~now bar_id + | Finish_bar bar_id -> Tracks.finish_bar tracks ~now bar_id) + | Inactive -> + eprint_s + [%message + [%here] + "An action sent to a [state_machine1] has been dropped because its \ + input was not present. This happens when the [state_machine1] is \ + inactive when it receives a message." + (action : Tracks_action.t)]; + tracks) + now + graph in - let%sub timeline = timeline ~now ~tracks in - let%sub clock_action = + let timeline = timeline ~now ~tracks graph in + let clock_action = let%arr wait_time = wait_time and update_tracks = update_tracks and next_bar_id = next_bar_id in @@ -390,12 +400,13 @@ let clock let%bind () = Effect.of_deferred_fun Async_kernel.Clock_ns.after wait_time in update_tracks (Finish_bar bar_id) in - let%sub () = + let () = Bonsai.Clock.every ~trigger_on_activate ~when_to_start_next_effect (Time_ns.Span.of_sec 1.0) clock_action + graph in let%arr last_trigger_time = last_trigger_time and timeline = timeline in @@ -409,7 +420,7 @@ let clock ] ;; -let all_clocks ~trigger_on_activate ~wait_time ~delta_time = +let all_clocks ~trigger_on_activate ~wait_time ~delta_time graph = List.map [ ( `Wait_period_after_previous_effect_finishes_blocking , "`Wait_period_after_previous_effect_finishes_blocking" @@ -433,31 +444,33 @@ let all_clocks ~trigger_on_activate ~wait_time ~delta_time = ~title ~description ~wait_time - ~delta_time) - |> Computation.all + ~delta_time + graph) + |> Bonsai.all ;; let immediate_clocks ~wait_time = all_clocks ~trigger_on_activate:true ~wait_time -let component = - let%sub time_span_form = Random_time_span.form in - let%sub wait_time = +let component graph = + let time_span_form = Random_time_span.form graph in + let wait_time = let%arr time_span_form = time_span_form in Form.value_or_default time_span_form ~default:Random_time_span.default in - let%sub now = Bonsai.Clock.now in - let%sub initial_time = + let now = Bonsai.Clock.now graph in + let initial_time = Bonsai.freeze ~sexp_of_model:[%sexp_of: Time_ns.Alternate_sexp.t] now ~equal:[%equal: Time_ns.Alternate_sexp.t] + graph in - let%sub delta_time = + let delta_time = let%arr initial_time = initial_time and now = now in Time_ns.sub now (Time_ns.to_span_since_epoch initial_time) in - let%sub immediate_clocks = immediate_clocks ~wait_time ~delta_time in + let immediate_clocks = immediate_clocks ~wait_time ~delta_time graph in let%arr immediate_clocks = immediate_clocks and wait_time_form = time_span_form and wait_time = wait_time in diff --git a/examples/codemirror/main.ml b/examples/codemirror/main.ml index 2d20e83d..295cef1b 100644 --- a/examples/codemirror/main.ml +++ b/examples/codemirror/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax open Codemirror open Virtual_dom @@ -41,7 +41,7 @@ module Fruit_sexp_grammar_auto_complete = struct ~extra_extension: (State.Extension.of_list [ Basic_setup.basic_setup; Codemirror_rainbow_parentheses.extension () ]) - (Value.return Query.t_sexp_grammar) + (Bonsai.return Query.t_sexp_grammar) ;; end @@ -79,7 +79,7 @@ let y = (module Codemirror_themes) ~equal:[%equal: Codemirror_themes.t] ~initial_state:(create_state (create_extensions Codemirror_themes.Material_dark)) - ~compute_extensions:(Value.return create_extensions) + ~compute_extensions:(Bonsai.return create_extensions) theme ;; end @@ -304,6 +304,67 @@ fn main() { ;; end +module Common_lisp_syntax_highlighting = struct + let doc = + {|(in-package :cl-postgres) + +;; These are used to synthesize reader and writer names for integer +;; reading/writing functions when the amount of bytes and the +;; signedness is known. Both the macro that creates the functions and +;; some macros that use them create names this way. +(eval-when (:compile-toplevel :load-toplevel :execute) + (defun integer-reader-name (bytes signed) + (intern (with-standard-io-syntax + (format nil "~a~a~a~a" '#:read- (if signed "" '#:u) '#:int bytes)))) + (defun integer-writer-name (bytes signed) + (intern (with-standard-io-syntax + (format nil "~a~a~a~a" '#:write- (if signed "" '#:u) '#:int bytes))))) |} + ;; + + let codemirror_editor = + Codemirror.of_initial_state + (State.Editor_state.create + (State.Editor_state_config.create + ~doc + ~extensions: + [ Basic_setup.basic_setup + ; Commonlisp.common_lisp + |> Stream_parser.Stream_language.define + |> Stream_parser.Stream_language.to_language + |> Language.extension + ] + ())) + ;; +end + +module Scheme_syntax_highlighting = struct + let doc = + {|;; Building a list of squares from 0 to 9: +;; Note: loop is simply an arbitrary symbol used as a label. Any symbol will do. + +(define (list-of-squares n) + (let loop ((i n) (res '())) + (if (< i 0) + res + (loop (- i 1) (cons (* i i) res)))))|} + ;; + + let codemirror_editor = + Codemirror.of_initial_state + (State.Editor_state.create + (State.Editor_state_config.create + ~doc + ~extensions: + [ Basic_setup.basic_setup + ; Scheme.scheme + |> Stream_parser.Stream_language.define + |> Stream_parser.Stream_language.to_language + |> Language.extension + ] + ())) + ;; +end + module Xml_syntax_highlighting = struct let doc = {| @@ -329,6 +390,8 @@ module Which_language = struct | Markdown | Sml | Sql + | Common_lisp + | Scheme | Diff | Html | Css @@ -345,6 +408,8 @@ module Which_language = struct | Ocaml -> "OCaml syntax highlighting" | Sml -> "SML syntax highlighting" | Sql -> "SQL syntax highlighting" + | Common_lisp -> "Common Lisp syntax highlighting" + | Scheme -> "Scheme syntax highlighting" | Diff -> "Diff syntax highlighting" | Html -> "HTML syntax highlighting" | Css -> "CSS syntax highlighting" @@ -355,15 +420,19 @@ module Which_language = struct ;; end -let no_theme_picker = Computation.map ~f:(fun x -> None, x) +let no_theme_picker x = + let%arr x = x in + None, x +;; -let component = - let%sub language_picker = +let component graph = + let language_picker = Form.Elements.Dropdown.enumerable ~to_string:Which_language.to_string (module Which_language) + graph in - let%sub chosen_language = + let chosen_language = let%arr language_picker = language_picker in Form.value language_picker |> Or_error.ok_exn in @@ -374,41 +443,60 @@ let component = codemirror editors, so we do the less-preferred option. *) match%sub chosen_language with | Which_language.Fruit -> - no_theme_picker @@ Fruit_sexp_grammar_auto_complete.codemirror_editor ~name:"fruit" + no_theme_picker + (Fruit_sexp_grammar_auto_complete.codemirror_editor ~name:"fruit" graph) | Fsharp -> - no_theme_picker @@ Fsharp_syntax_highlighting.codemirror_editor ~name:"fsharp" + no_theme_picker (Fsharp_syntax_highlighting.codemirror_editor ~name:"fsharp" graph) | Markdown -> - no_theme_picker @@ Markdown_syntax_highlighting.codemirror_editor ~name:"markdown" + no_theme_picker + (Markdown_syntax_highlighting.codemirror_editor ~name:"markdown" graph) | Ocaml -> - let%sub theme_picker = + let theme_picker = Form.Elements.Dropdown.enumerable ~to_string:Codemirror_themes.to_string (module Codemirror_themes) - |> Computation.map ~f:(Form.label "theme") + graph + |> Bonsai.map ~f:(Form.label "theme") in - let%sub chosen_theme = + let chosen_theme = let%arr theme_picker = theme_picker in Form.value theme_picker |> Or_error.ok_exn in - let%sub c = - Ocaml_syntax_highlighting.codemirror_editor ~name:"ocaml" ~theme:chosen_theme + let c = + Ocaml_syntax_highlighting.codemirror_editor + ~name:"ocaml" + ~theme:chosen_theme + graph in let%arr c = c and theme_picker = theme_picker in Some theme_picker, c - | Sml -> no_theme_picker @@ Sml_syntax_highlighting.codemirror_editor ~name:"sml" - | Sql -> no_theme_picker @@ Sql_syntax_highlighting.codemirror_editor ~name:"sql" - | Diff -> no_theme_picker @@ Diff_syntax_highlighting.codemirror_editor ~name:"diff" - | Html -> no_theme_picker @@ Html_syntax_highlighting.codemirror_editor ~name:"html" - | Css -> no_theme_picker @@ Css_syntax_highlighting.codemirror_editor ~name:"css" + | Sml -> + no_theme_picker @@ Sml_syntax_highlighting.codemirror_editor ~name:"sml" graph + | Sql -> + no_theme_picker @@ Sql_syntax_highlighting.codemirror_editor ~name:"sql" graph + | Common_lisp -> + no_theme_picker + @@ Common_lisp_syntax_highlighting.codemirror_editor ~name:"common lisp" graph + | Scheme -> + no_theme_picker @@ Scheme_syntax_highlighting.codemirror_editor ~name:"scheme" graph + | Diff -> + no_theme_picker @@ Diff_syntax_highlighting.codemirror_editor ~name:"diff" graph + | Html -> + no_theme_picker @@ Html_syntax_highlighting.codemirror_editor ~name:"html" graph + | Css -> + no_theme_picker @@ Css_syntax_highlighting.codemirror_editor ~name:"css" graph | Javascript -> no_theme_picker - @@ Javascript_syntax_highlighting.codemirror_editor ~name:"javascript" - | Php -> no_theme_picker @@ Php_syntax_highlighting.codemirror_editor ~name:"php" - | Rust -> no_theme_picker @@ Rust_syntax_highlighting.codemirror_editor ~name:"rust" - | Xml -> no_theme_picker @@ Xml_syntax_highlighting.codemirror_editor ~name:"xml" + @@ Javascript_syntax_highlighting.codemirror_editor ~name:"javascript" graph + | Php -> + no_theme_picker @@ Php_syntax_highlighting.codemirror_editor ~name:"php" graph + | Rust -> + no_theme_picker @@ Rust_syntax_highlighting.codemirror_editor ~name:"rust" graph + | Xml -> + no_theme_picker @@ Xml_syntax_highlighting.codemirror_editor ~name:"xml" graph in - let%sub codemirror_view = + let codemirror_view = let%arr codemirror = codemirror in Codemirror.view codemirror in diff --git a/examples/codicons/main.ml b/examples/codicons/main.ml index 67bf62c1..ae599661 100644 --- a/examples/codicons/main.ml +++ b/examples/codicons/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax module Form = Bonsai_web_ui_form.With_automatic_view @@ -81,18 +81,20 @@ module Style = struct end module Temporary_toggle = struct - let state ~base ~temporary timeout = - let%sub state = - Bonsai.state - Time_ns.min_value_representable - ~sexp_of_model:[%sexp_of: Time_ns.Alternate_sexp.t] - ~equal:[%equal: Time_ns.Alternate_sexp.t] + let state ~base ~temporary timeout graph = + let state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state + Time_ns.min_value_representable + ~sexp_of_model:[%sexp_of: Time_ns.Alternate_sexp.t] + ~equal:[%equal: Time_ns.Alternate_sexp.t] + graph in let toggle_back_time = - Value.map state ~f:(fun (last_set_time, _) -> Time_ns.add last_set_time timeout) + Bonsai.map state ~f:(fun (last_set_time, _) -> Time_ns.add last_set_time timeout) in - let%sub toggle = Bonsai.Clock.at toggle_back_time in - let%sub get_now = Bonsai.Clock.get_current_time in + let toggle = Bonsai.Clock.at toggle_back_time graph in + let get_now = Bonsai.Clock.get_current_time graph in let%arr _, set_time = state and toggle = toggle and get_now = get_now in @@ -110,9 +112,13 @@ module Temporary_toggle = struct end module Icon_grid = struct - let icon_card icon = - let%sub copied = - Temporary_toggle.state ~base:`Show_icon ~temporary:`Show_copied Time_ns.Span.second + let icon_card icon graph = + let copied = + Temporary_toggle.state + ~base:`Show_icon + ~temporary:`Show_copied + Time_ns.Span.second + graph in let%arr copied, set_copied = copied and icon = icon in @@ -144,23 +150,24 @@ module Icon_grid = struct [ Codicons.svg Copy; Node.p [ Node.text [%string "Copied %{variant_name}!"] ] ]) ;; - let component icons = + let component icons graph = let icons = - Value.map icons ~f:(String.Map.of_list_with_key_exn ~get_key:Codicons.name) + Bonsai.map icons ~f:(String.Map.of_list_with_key_exn ~get_key:Codicons.name) in - let%sub cards = Bonsai.assoc (module String) icons ~f:(fun _ -> icon_card) in + let cards = Bonsai.assoc (module String) icons ~f:(fun _ -> icon_card) graph in let%arr cards = cards in Vdom.Node.div ~attrs:[ Style.grid ] (Map.data cards) ;; end module Search = struct - let component () = - let%sub input = + let component () graph = + let input = Form.Elements.Textbox.string - ~placeholder:(Value.return "Filter icons") + ~placeholder:(Bonsai.return "Filter icons") ~allow_updates_when_focused:`Never () + graph in let%arr input = input in let search = @@ -188,10 +195,10 @@ module Search = struct ;; end -let app = - let%sub search = Search.component () in - let%sub icons = Bonsai.pure fst search in - let%sub grid = Icon_grid.component icons in +let app graph = + let search = Search.component () graph in + let icons = Bonsai.map ~f:fst search in + let grid = Icon_grid.component icons graph in let%arr grid = grid and _, search = search in Vdom.Node.div ~attrs:[ Style.main ] [ search; grid ] diff --git a/examples/counters/lib/bonsai_web_counters_example.ml b/examples/counters/lib/bonsai_web_counters_example.ml index 2ee08922..80eb7131 100644 --- a/examples/counters/lib/bonsai_web_counters_example.ml +++ b/examples/counters/lib/bonsai_web_counters_example.ml @@ -1,5 +1,5 @@ open! Core -open Bonsai_web +open Bonsai_web.Cont open Bonsai.Let_syntax (* [CODE_EXCERPT_BEGIN 2] *) @@ -7,17 +7,18 @@ module Model = struct type t = unit Int.Map.t [@@deriving sexp, equal] end -let add_counter_component = - let%sub add_counter_state = - Bonsai.state_machine0 - () - ~sexp_of_model:[%sexp_of: Model.t] - ~equal:[%equal: Model.t] - ~sexp_of_action:[%sexp_of: Unit.t] - ~default_model:Int.Map.empty - ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model () -> - let key = Map.length model in - Map.add_exn model ~key ~data:()) +let add_counter_component graph = + let add_counter_state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state_machine0 + graph + ~sexp_of_model:[%sexp_of: Model.t] + ~equal:[%equal: Model.t] + ~sexp_of_action:[%sexp_of: Unit.t] + ~default_model:Int.Map.empty + ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model () -> + let key = Map.length model in + Map.add_exn model ~key ~data:()) in let%arr state, inject = add_counter_state in let view = @@ -39,17 +40,18 @@ module Action = struct [@@deriving sexp_of] end -let single_counter = - let%sub counter_state = - Bonsai.state_machine0 - () - ~sexp_of_model:[%sexp_of: Int.t] - ~equal:[%equal: Int.t] - ~sexp_of_action:[%sexp_of: Action.t] - ~default_model:0 - ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model -> function - | Action.Increment -> model + 1 - | Action.Decrement -> model - 1) +let single_counter graph = + let counter_state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state_machine0 + graph + ~sexp_of_model:[%sexp_of: Int.t] + ~equal:[%equal: Int.t] + ~sexp_of_action:[%sexp_of: Action.t] + ~default_model:0 + ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model -> function + | Action.Increment -> model + 1 + | Action.Decrement -> model - 1) in let%arr state, inject = counter_state in let button label action = @@ -67,11 +69,11 @@ let single_counter = (* [CODE_EXCERPT_END 1] *) (* [CODE_EXCERPT_BEGIN 3] *) -let application = +let application graph = let open Bonsai.Let_syntax in - let%sub map, add_button = add_counter_component in - let%sub counters = - Bonsai.assoc (module Int) map ~f:(fun _key _data -> single_counter) + let%sub map, add_button = add_counter_component graph in + let counters = + Bonsai.assoc (module Int) map ~f:(fun _key _data -> single_counter) graph in let%arr add_button = add_button and counters = counters in @@ -80,15 +82,14 @@ let application = (* [CODE_EXCERPT_END 3] *) -let _application_sugar_free = +let _application_sugar_free graph = let open Bonsai.Let_syntax in - Let_syntax.sub add_counter_component ~f:(fun add_counter -> - let map = Value.map add_counter ~f:(fun (map, _) -> map) in - let add_button = Value.map add_counter ~f:(fun (_, add_button) -> add_button) in - Let_syntax.sub - (Bonsai.assoc (module Int) map ~f:(fun _key _data -> single_counter)) - ~f:(fun counters -> - return - (Value.map2 add_button counters ~f:(fun add_button counters -> - Vdom.Node.div [ add_button; Vdom.Node.div (Map.data counters) ])))) + Let_syntax.sub (add_counter_component graph) ~f:(fun add_counter -> + let map = Bonsai.map ~f:fst add_counter in + let add_button = Bonsai.map ~f:snd add_counter in + let counters = + Bonsai.assoc (module Int) map ~f:(fun _key _data -> single_counter) graph + in + Bonsai.map2 add_button counters ~f:(fun add_button counters -> + Vdom.Node.div [ add_button; Vdom.Node.div (Map.data counters) ])) [@nontail] ;; diff --git a/examples/counters/lib/bonsai_web_counters_example.mli b/examples/counters/lib/bonsai_web_counters_example.mli index 91c4f894..a27e228b 100644 --- a/examples/counters/lib/bonsai_web_counters_example.mli +++ b/examples/counters/lib/bonsai_web_counters_example.mli @@ -1,5 +1,5 @@ open! Core -open Bonsai_web +open Bonsai_web.Cont -val single_counter : Vdom.Node.t Computation.t -val application : Vdom.Node.t Computation.t +val single_counter : Bonsai.graph -> Vdom.Node.t Bonsai.t +val application : Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/counters_condensed/main.ml b/examples/counters_condensed/main.ml index a1db1ad1..83491155 100644 --- a/examples/counters_condensed/main.ml +++ b/examples/counters_condensed/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax open Vdom @@ -22,15 +22,16 @@ let apply_action (_ : _ Bonsai.Apply_action_context.t) model = function Map.update model location ~f:(Option.value_map ~default:0 ~f:(( + ) diff)) ;; -let component = - let%sub state = - Bonsai.state_machine0 - () - ~sexp_of_model:[%sexp_of: Model.t] - ~equal:[%equal: Model.t] - ~sexp_of_action:[%sexp_of: Action.t] - ~default_model - ~apply_action +let component graph = + let state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state_machine0 + graph + ~sexp_of_model:[%sexp_of: Model.t] + ~equal:[%equal: Model.t] + ~sexp_of_action:[%sexp_of: Action.t] + ~default_model + ~apply_action in let%arr state, inject = state in let button text action = diff --git a/examples/dagviz/main.ml b/examples/dagviz/main.ml index 00ee72a6..0d8a92b2 100644 --- a/examples/dagviz/main.ml +++ b/examples/dagviz/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Position = Bonsai_web_ui_element_size_hooks.Position_tracker.Position @@ -176,8 +176,10 @@ module Node_data = struct } [@@deriving sexp, compare, quickcheck, fields ~iterators:create] - let to_vdom (id : Id.t Value.t) (t : t Value.t) : Vdom.Node.t Computation.t = - let%sub collapsed, collapse = Bonsai.toggle ~default_model:true in + let to_vdom (id : Id.t Bonsai.t) (t : t Bonsai.t) : Bonsai.graph -> Vdom.Node.t Bonsai.t + = + fun graph -> + let collapsed, collapse = Bonsai.toggle ~default_model:true graph in let%arr collapsed = collapsed and id = id and t = t @@ -347,9 +349,10 @@ let face_point : Position.t -> [ `Top | `Left | `Bottom | `Right ] -> Point.t = let edge_to_svg ~(direction : [ `Top_down | `Left_to_right ]) - ~(edge : Edge.t Value.t) - ~(from : Position.t Value.t) - ~(to_ : Position.t Value.t) + ~(edge : Edge.t Bonsai.t) + ~(from : Position.t Bonsai.t) + ~(to_ : Position.t Bonsai.t) + _graph = let%arr edge = edge and from = from @@ -424,9 +427,9 @@ let edge_to_svg ] ;; -let component = - let curr_id = Value.return Id.Count.zero in - let%sub dag_data = return (Value.return { To_vdom.nodes; edges }) in +let component graph = + let curr_id = Bonsai.return Id.Count.zero in + let dag_data = Bonsai.return { To_vdom.nodes; edges } in let%sub dag, _curr_id = To_vdom.create ~curr_id @@ -434,10 +437,11 @@ let component = ~node_to_vdom:Node_data.to_vdom ~edge_to_svg:(edge_to_svg ~direction:`Top_down) dag_data + graph in - let%sub dag = + let dag = match%sub dag with - | Ok dag -> return dag + | Ok dag -> dag | Error error -> let%arr error = error in Vdom.Node.sexp_for_debugging [%message "" ~_:(error : Error.t)] diff --git a/examples/drag_and_drop/bin/main.ml b/examples/drag_and_drop/bin/main.ml index d7a3c131..303874c9 100644 --- a/examples/drag_and_drop/bin/main.ml +++ b/examples/drag_and_drop/bin/main.ml @@ -1,4 +1,4 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont let () = Bonsai_web.Start.start Bonsai_drag_and_drop_example.app diff --git a/examples/drag_and_drop/lib/bonsai_drag_and_drop_example.ml b/examples/drag_and_drop/lib/bonsai_drag_and_drop_example.ml index 5cbde1a1..4de56ba7 100644 --- a/examples/drag_and_drop/lib/bonsai_drag_and_drop_example.ml +++ b/examples/drag_and_drop/lib/bonsai_drag_and_drop_example.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Drag_and_drop = Bonsai_web_ui_drag_and_drop module Node = Vdom.Node @@ -92,7 +92,7 @@ module Action = struct [@@deriving sexp] end -let kanban_column ~extra_dnd ~dnd ~items ~column ~title = +let kanban_column ~extra_dnd ~dnd ~items ~column ~title graph = let map = let%map items = items and model = dnd >>| Drag_and_drop.model in @@ -110,16 +110,16 @@ let kanban_column ~extra_dnd ~dnd ~items ~column ~title = (from_this_column && not is_the_dragged_item) || (is_the_dragged_item && from_target_column)) in - let%sub extra_source = + let extra_source = match extra_dnd with - | Some dnd -> Bonsai.pure Drag_and_drop.source dnd - | None -> Bonsai.const (fun ~id:_ -> Vdom.Attr.empty) + | Some dnd -> Bonsai.map ~f:Drag_and_drop.source dnd + | None -> Bonsai.return (fun ~id:_ -> Vdom.Attr.empty) in - let%sub items = + let items = Bonsai.assoc (module Item_id) map - ~f:(fun item_id item -> + ~f:(fun item_id item _graph -> let%arr item = item and item_id = item_id and source = dnd >>| Drag_and_drop.source @@ -148,6 +148,7 @@ let kanban_column ~extra_dnd ~dnd ~items ~column ~title = source ~id:item_id @ Style.kanban_item @ extra @ extra_source ~id:item_id) ] [ Node.text contents ]) + graph in let%arr items = items and drop_target = dnd >>| Drag_and_drop.drop_target @@ -175,10 +176,10 @@ let kanban_column ~extra_dnd ~dnd ~items ~column ~title = [ Node.h3 ~attrs:[ Style.centered ] [ Node.text title ]; Node.div (Map.data items) ] ;; -let board ?extra_dnd name = - let%sub items, inject = +let board ?extra_dnd name graph = + let items, inject = Bonsai.state_machine0 - () + graph ~sexp_of_model:[%sexp_of: Kanban_board.t] ~equal:[%equal: Kanban_board.t] ~sexp_of_action:[%sexp_of: Action.t] @@ -200,37 +201,41 @@ let board ?extra_dnd name = let change_col (contents, _) ~new_column = contents, new_column in Map.change model item_id ~f:(Option.map ~f:(change_col ~new_column))) in - let%sub dnd = + let dnd = Drag_and_drop.create ~source_id:(module Item_id) ~target_id:(module Column) ~on_drop: (let%map inject = inject in fun item_id new_column -> inject (Move { item_id; new_column })) + graph in - let%sub todo = kanban_column ~extra_dnd ~dnd ~items ~column:Todo ~title:"Todo" in - let%sub in_progress = - kanban_column ~extra_dnd ~dnd ~items ~column:In_progress ~title:"In Progress" + let todo = kanban_column ~extra_dnd ~dnd ~items ~column:Todo ~title:"Todo" graph in + let in_progress = + kanban_column ~extra_dnd ~dnd ~items ~column:In_progress ~title:"In Progress" graph in - let%sub finished = - kanban_column ~extra_dnd ~dnd ~items ~column:Finished ~title:"Done" + let finished = + kanban_column ~extra_dnd ~dnd ~items ~column:Finished ~title:"Done" graph in - let%sub dragged_element = - Drag_and_drop.dragged_element dnd ~f:(fun item_id -> - let%sub text = - match%sub - let%map item_id = item_id - and items = items in - Map.find items item_id - with - | Some (contents, _) -> return contents - | None -> Bonsai.const "No item exists with that id" - in - let%arr text = text in - Node.div ~attrs:[ Style.kanban_item ] [ Node.text text ]) + let dragged_element = + Drag_and_drop.dragged_element + dnd + ~f:(fun item_id _graph -> + let text = + match%sub + let%map item_id = item_id + and items = items in + Map.find items item_id + with + | Some (contents, _) -> contents + | None -> Bonsai.return "No item exists with that id" + in + let%arr text = text in + Node.div ~attrs:[ Style.kanban_item ] [ Node.text text ]) + graph in - let%sub sentinel = Bonsai.pure Drag_and_drop.sentinel dnd in - let%sub view = + let sentinel = Bonsai.map ~f:Drag_and_drop.sentinel dnd in + let view = let%arr todo = todo and in_progress = in_progress and finished = finished @@ -241,12 +246,12 @@ let board ?extra_dnd name = ~attrs:[ Vdom.Attr.(Style.kanban_container @ sentinel) ] [ todo; in_progress; finished; dragged_element ] in - return (Bonsai.Value.both view dnd) + Bonsai.both view dnd ;; -let app = - let%sub board1, dnd = board "board1" in - let%sub board2, _ = board ~extra_dnd:dnd "board2" in +let app graph = + let%sub board1, dnd = board "board1" graph in + let%sub board2, _ = board ~extra_dnd:dnd "board2" graph in let%arr board1 = board1 and board2 = board2 in Node.div @@ -268,4 +273,4 @@ let app = ] ;; -let board name = Bonsai.Computation.map (board ?extra_dnd:None name) ~f:fst +let board name graph = Bonsai.map (board ?extra_dnd:None name graph) ~f:fst diff --git a/examples/drag_and_drop/lib/bonsai_drag_and_drop_example.mli b/examples/drag_and_drop/lib/bonsai_drag_and_drop_example.mli index 9a3915fa..75e80280 100644 --- a/examples/drag_and_drop/lib/bonsai_drag_and_drop_example.mli +++ b/examples/drag_and_drop/lib/bonsai_drag_and_drop_example.mli @@ -1,4 +1,4 @@ -open Bonsai_web +open Bonsai_web.Cont -val app : Vdom.Node.t Computation.t -val board : string -> Vdom.Node.t Computation.t +val app : Bonsai.graph -> Vdom.Node.t Bonsai.t +val board : string -> Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/drag_and_drop/test/app_test.ml b/examples/drag_and_drop/test/app_test.ml index 05829528..89fec7ee 100644 --- a/examples/drag_and_drop/test/app_test.ml +++ b/examples/drag_and_drop/test/app_test.ml @@ -1,6 +1,6 @@ open! Core open! Bonsai_web_test -open! Bonsai_web +open! Bonsai_web.Cont module Handle = Bonsai_web_test.Handle module Result_spec = Bonsai_web_test.Result_spec module Example = Bonsai_drag_and_drop_example diff --git a/examples/drag_and_drop_list/main.ml b/examples/drag_and_drop_list/main.ml index 723947b5..0fce6d39 100644 --- a/examples/drag_and_drop_list/main.ml +++ b/examples/drag_and_drop_list/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Reorderable_list = Bonsai_web_ui_reorderable_list module Form = Bonsai_web_ui_form.With_automatic_view @@ -29,8 +29,8 @@ stylesheet } |}] -let item ~index:_ ~source _which _data = - let%sub text, set_text = Bonsai.state_opt ~equal:[%equal: string] () in +let item ~index:_ ~source _which _data graph = + let text, set_text = Bonsai.state_opt ~equal:[%equal: string] graph in let%arr source = source and text = text and set_text = set_text in @@ -48,10 +48,10 @@ let item ~index:_ ~source _which _data = (), view ;; -let component = - let%sub input, extend_input = +let component graph = + let input, extend_input = Bonsai.state_machine0 - () + graph ~sexp_of_model:[%sexp_of: Int.Set.t] ~equal:[%equal: Int.Set.t] ~sexp_of_action:[%sexp_of: Unit.t] @@ -59,18 +59,24 @@ let component = ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model () -> Set.add model (Set.length model)) in - let%sub () = + let () = Bonsai.Clock.every ~when_to_start_next_effect:`Every_multiple_of_period_blocking ~trigger_on_activate:true (Time_ns.Span.of_sec 1.0) (let%map extend_input = extend_input in extend_input ()) + graph in - let%sub num_lists = - Form.Elements.Number.int ~default:1 ~step:1 ~allow_updates_when_focused:`Never () + let num_lists = + Form.Elements.Number.int + ~default:1 + ~step:1 + ~allow_updates_when_focused:`Never + () + graph in - let%sub whiches = + let whiches = let%arr num_lists = num_lists in let length = Int.max 0 (Form.value_or_default num_lists ~default:1) in Int.Set.of_list (List.range 0 length) @@ -79,24 +85,26 @@ let component = Reorderable_list.Multi.simple (module Int) (module Int) - ~extra_item_attrs:(Value.return S.transition_transform) + ~extra_item_attrs:(Bonsai.return S.transition_transform) ~default_item_height:40 ~render:item ~lists:whiches - ~default_list:(Value.return 0) + ~default_list:(Bonsai.return 0) input + graph in - let%sub lists = + let lists = Bonsai.assoc (module Int) lists - ~f:(fun which data -> - let%sub _, view = return data in + ~f:(fun which data _graph -> + let%sub _, view = data in let%arr view = view and which = which in Vdom.Node.div ~attrs:[ S.list ] [ Vdom.Node.h3 [ Vdom.Node.text [%string "List %{which#Int}"] ]; view ]) + graph in let%arr lists = lists and dragged_element = dragged_element diff --git a/examples/drilldown/main.ml b/examples/drilldown/main.ml index c0574f09..7eabc1c3 100644 --- a/examples/drilldown/main.ml +++ b/examples/drilldown/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax open! Bonsai_web_ui_drilldown @@ -48,10 +48,11 @@ let tree_layout : Vdom.Node.t = loop example |> to_vdom ;; -let app : Vdom.Node.t Bonsai.Computation.t = +let app : Bonsai.graph -> Vdom.Node.t Bonsai.t = + fun graph -> let module N = Vdom.Node in - let%map.Computation drilldown_with_breadcrumbs = - Drilldown_with_breadcrumbs.component (Value.return example) + let%map drilldown_with_breadcrumbs = + Drilldown_with_breadcrumbs.component (Bonsai.return example) graph in let explanation1 = N.div diff --git a/examples/dygraph/custom_points.ml b/examples/dygraph/custom_points.ml index d2cae740..50e465a6 100644 --- a/examples/dygraph/custom_points.ml +++ b/examples/dygraph/custom_points.ml @@ -1,6 +1,6 @@ open Core open Js_of_ocaml -open Bonsai_web +open Bonsai_web.Cont let side_lengths = Array.init 12 ~f:(fun i -> float i) @@ -75,18 +75,19 @@ let options = ~title:"Custom Drawn Points Example" ;; -let app = +let app graph = let%sub.Bonsai { graph_view; _ } = Dygraph.With_bonsai.create () - ~key:("custom-drawn-points-graph" |> Value.return) - ~x_label:("diameter/side length" |> Value.return) + ~key:("custom-drawn-points-graph" |> Bonsai.return) + ~x_label:("diameter/side length" |> Bonsai.return) ~per_series_info: ([ "circles"; "squares" ] |> Dygraph.Per_series_info.create_all_visible - |> Value.return) - ~options:(options |> Value.return) - ~data:(data |> Value.return) + |> Bonsai.return) + ~options:(options |> Bonsai.return) + ~data:(data |> Bonsai.return) + graph in - Bonsai.read graph_view + graph_view ;; diff --git a/examples/dygraph/custom_points.mli b/examples/dygraph/custom_points.mli index 3ac5143c..2b7088b1 100644 --- a/examples/dygraph/custom_points.mli +++ b/examples/dygraph/custom_points.mli @@ -1,4 +1,4 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -val app : Vdom.Node.t Computation.t +val app : Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/dygraph/hide_overnights.ml b/examples/dygraph/hide_overnights.ml index d4158256..f9c3cd13 100644 --- a/examples/dygraph/hide_overnights.ml +++ b/examples/dygraph/hide_overnights.ml @@ -50,26 +50,27 @@ let options ~title ?value_formatter ?axis_label_formatter () = (Dygraph.Options.Highlight_series_options.create () ~strokeWidth:1.5) ;; -let app = +let app graph = let x_label = "time" in let y_labels = [ "brownian motion" ] in - let make_graph ~name ~title ~data ?value_formatter ?axis_label_formatter () = + let make_graph ~name ~title ~data ?value_formatter ?axis_label_formatter () graph = let options = options ~title ?value_formatter ?axis_label_formatter () in let%sub { graph_view; _ } = Dygraph.With_bonsai.create - ~key:(Value.return name) - ~x_label:(Value.return x_label) + ~key:(Bonsai.return name) + ~x_label:(Bonsai.return x_label) ~per_series_info: - (y_labels |> Dygraph.Per_series_info.create_all_visible |> Value.return) - ~options:(Value.return options) - ~data:(Value.return data) + (y_labels |> Dygraph.Per_series_info.create_all_visible |> Bonsai.return) + ~options:(Bonsai.return options) + ~data:(Bonsai.return data) ~with_graph:(fun graph -> Js.Unsafe.set Dom_html.window (sprintf "g_%s" name) graph) () + graph in - return graph_view + graph_view in - let%sub hide_overnights_graph = + let hide_overnights_graph = let { Dygraph.X_axis_mapping.time_to_x_value ; x_value_to_time = _ ; value_formatter @@ -94,10 +95,16 @@ let app = ~axis_label_formatter ~data () + graph in - let%sub visible_overnights_graphs = + let visible_overnights_graphs = let data = Dygraph.Data.create_time_ns raw_data in - make_graph ~name:"with_overnights_visible" ~title:"With overnights visible" ~data () + make_graph + ~name:"with_overnights_visible" + ~title:"With overnights visible" + ~data + () + graph in let%arr hide_overnights_graph = hide_overnights_graph and visible_overnights_graphs = visible_overnights_graphs in diff --git a/examples/dygraph/hide_overnights.mli b/examples/dygraph/hide_overnights.mli index 3ac5143c..2b7088b1 100644 --- a/examples/dygraph/hide_overnights.mli +++ b/examples/dygraph/hide_overnights.mli @@ -1,4 +1,4 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -val app : Vdom.Node.t Computation.t +val app : Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/dygraph/import.ml b/examples/dygraph/import.ml index a3aaa3fc..701017a7 100644 --- a/examples/dygraph/import.ml +++ b/examples/dygraph/import.ml @@ -1,3 +1,3 @@ include Js_of_ocaml -include Bonsai_web +include Bonsai_web.Cont include Bonsai.Let_syntax diff --git a/examples/dygraph/index.html b/examples/dygraph/index.html index 53365c5f..4283fbe2 100644 --- a/examples/dygraph/index.html +++ b/examples/dygraph/index.html @@ -1,7 +1,6 @@ - diff --git a/examples/dygraph/main.ml b/examples/dygraph/main.ml index ce2820d4..1cbd18cd 100644 --- a/examples/dygraph/main.ml +++ b/examples/dygraph/main.ml @@ -1,12 +1,12 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax -let app = - let%sub hide_overnight = Hide_overnights.app in - let%sub simple = Simple.app in - let%sub stock_chart = Stock_chart.app in - let%sub custom_points = Custom_points.app in +let app graph = + let hide_overnight = Hide_overnights.app graph in + let simple = Simple.app graph in + let stock_chart = Stock_chart.app graph in + let custom_points = Custom_points.app graph in let%arr hide_overnight = hide_overnight and simple = simple and stock_chart = stock_chart diff --git a/examples/dygraph/simple.ml b/examples/dygraph/simple.ml index faaff09d..d2351597 100644 --- a/examples/dygraph/simple.ml +++ b/examples/dygraph/simple.ml @@ -1,6 +1,6 @@ open Core open Js_of_ocaml -open Bonsai_web +open Bonsai_web.Cont (** x, x^2, x^3 *) let data : Dygraph.Data.t = @@ -39,17 +39,18 @@ let options = ~axes ;; -let app = +let app graph = let%sub.Bonsai { graph_view; _ } = Dygraph.With_bonsai.create () - ~key:("graph" |> Value.return) - ~x_label:("x" |> Value.return) + ~key:("graph" |> Bonsai.return) + ~x_label:("x" |> Bonsai.return) ~per_series_info: - ([ "x^2"; "x^3" ] |> Dygraph.Per_series_info.create_all_visible |> Value.return) - ~options:(options |> Value.return) - ~data:(data |> Value.return) + ([ "x^2"; "x^3" ] |> Dygraph.Per_series_info.create_all_visible |> Bonsai.return) + ~options:(options |> Bonsai.return) + ~data:(data |> Bonsai.return) ~with_graph:(fun graph -> Js.Unsafe.set Dom_html.window "g" graph) + graph in - Bonsai.read graph_view + graph_view ;; diff --git a/examples/dygraph/simple.mli b/examples/dygraph/simple.mli index 3ac5143c..2b7088b1 100644 --- a/examples/dygraph/simple.mli +++ b/examples/dygraph/simple.mli @@ -1,4 +1,4 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -val app : Vdom.Node.t Computation.t +val app : Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/dygraph/stock_chart.ml b/examples/dygraph/stock_chart.ml index 1a287169..d3a2e498 100644 --- a/examples/dygraph/stock_chart.ml +++ b/examples/dygraph/stock_chart.ml @@ -11,9 +11,11 @@ module Scale = struct let to_string t = sexp_of_t t |> Sexp.to_string end -let scale : (Scale.t * Vdom.Node.t) Computation.t = - let%sub scale_state = - Bonsai.state `log ~sexp_of_model:[%sexp_of: Scale.t] ~equal:[%equal: Scale.t] +let scale : Bonsai.graph -> (Scale.t * Vdom.Node.t) Bonsai.t = + fun graph -> + let scale_state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state `log ~sexp_of_model:[%sexp_of: Scale.t] ~equal:[%equal: Scale.t] graph in let%arr scale, set_scale = scale_state in let view = @@ -31,7 +33,7 @@ let data = Dygraph.Data.create_date ~zone:local_tz (Array.of_list (List.map Stock_data.data ~f:(fun (date, a, b) -> date, [| a; b |]))) - |> Value.return + |> Bonsai.return ;; let options ~logscale = @@ -49,10 +51,10 @@ let options ~logscale = (Dygraph.Options.Highlight_series_options.create () ~strokeWidth:1.5) ;; -let app = +let app graph = let x_label = "Month" in let y_labels = [ "Nominal"; "Real" ] in - let%sub scale, scale_view = scale in + let%sub scale, scale_view = scale graph in let options = match%map scale with | `log -> options ~logscale:true @@ -60,10 +62,10 @@ let app = in let%sub { graph_view; _ } = Dygraph.With_bonsai.create - ~key:("graph" |> Value.return) - ~x_label:(x_label |> Value.return) + ~key:("graph" |> Bonsai.return) + ~x_label:(x_label |> Bonsai.return) ~per_series_info: - (y_labels |> Dygraph.Per_series_info.create_all_visible |> Value.return) + (y_labels |> Dygraph.Per_series_info.create_all_visible |> Bonsai.return) ~options ~data (* By setting the graph to global variable "g", I'm able to access the graph in the @@ -71,6 +73,7 @@ let app = for debugging/convenience. *) ~with_graph:(fun graph -> Js.Unsafe.set Dom_html.window "g" graph) () + graph in let%arr graph_view = graph_view and scale_view = scale_view in diff --git a/examples/dygraph/stock_chart.mli b/examples/dygraph/stock_chart.mli index 3ac5143c..2b7088b1 100644 --- a/examples/dygraph/stock_chart.mli +++ b/examples/dygraph/stock_chart.mli @@ -1,4 +1,4 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -val app : Vdom.Node.t Computation.t +val app : Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/effect_poller/main.ml b/examples/effect_poller/main.ml index 024d6a4c..06fbde8f 100644 --- a/examples/effect_poller/main.ml +++ b/examples/effect_poller/main.ml @@ -1,6 +1,6 @@ open! Core open! Async_kernel -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax let fake_slow_capitalize_string_rpc = @@ -10,9 +10,10 @@ let fake_slow_capitalize_string_rpc = String.uppercase text) ;; -let textbox = - let%sub state = - Bonsai.state "" ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] +let textbox graph = + let state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state "" ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] graph in let%arr text, set_text = state in let view = @@ -23,9 +24,9 @@ let textbox = text, view ;; -let component = - let%sub text, view = textbox in - let%sub capitalized = +let component graph = + let%sub text, view = textbox graph in + let capitalized = Bonsai.Edge.Poll.( effect_on_change ~sexp_of_input:[%sexp_of: String.t] @@ -34,7 +35,8 @@ let component = ~equal_result:[%equal: String.t] (Starting.initial "") text - ~effect:(Value.return fake_slow_capitalize_string_rpc)) + ~effect:(Bonsai.return fake_slow_capitalize_string_rpc) + graph) in let%arr view = view and capitalized = capitalized in diff --git a/examples/element_size_util/main.ml b/examples/element_size_util/main.ml index ad5511a9..cb35d8ef 100644 --- a/examples/element_size_util/main.ml +++ b/examples/element_size_util/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Size_hooks = Bonsai_web_ui_element_size_hooks @@ -23,8 +23,8 @@ module Size = struct [@@deriving sexp, equal] end -let bulk_size_component = - let%sub state = Size_hooks.Bulk_size_tracker.component (module Int) Prune_stale in +let bulk_size_component graph = + let state = Size_hooks.Bulk_size_tracker.component (module Int) Prune_stale graph in let%arr sizes, size_attr = state in let mk i = let key = sprintf "resizable-using-css-%d" i in @@ -45,27 +45,30 @@ let bulk_size_component = ] ;; -let position = +let position graph = let%sub { positions; get_attr; update } = - Size_hooks.Position_tracker.component (module Int) + Size_hooks.Position_tracker.component (module Int) graph in let module Model = struct type t = Size_hooks.Position_tracker.Position.t Int.Map.t [@@deriving sexp, equal] end in - let%sub () = + let () = Bonsai.Edge.on_change ~sexp_of_model:[%sexp_of: Model.t] ~equal:[%equal: Model.t] positions ~callback: - (Fn.const ((Effect.of_sync_fun print_endline) "position changed!") |> Value.return) + (Fn.const ((Effect.of_sync_fun print_endline) "position changed!") + |> Bonsai.return) + graph in - let%sub () = + let () = Bonsai.Clock.every ~when_to_start_next_effect:`Every_multiple_of_period_blocking (Time_ns.Span.of_sec 2.0) update + graph in let%arr positions = positions and get_attr = get_attr in @@ -88,9 +91,10 @@ let position = ] ;; -let size_component = - let%sub state = - Bonsai.state_opt () ~sexp_of_model:[%sexp_of: Size.t] ~equal:[%equal: Size.t] +let size_component graph = + let state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state_opt graph ~sexp_of_model:[%sexp_of: Size.t] ~equal:[%equal: Size.t] in let%arr size, inject_size = state in Vdom.Node.div @@ -106,7 +110,7 @@ let size_component = ] ;; -let fit = +let fit _graph = let open Vdom in let make s behavior = Node.div @@ -127,7 +131,7 @@ let fit = ] ] in - Bonsai.const + Bonsai.return (Node.div [ make "shrink to avoid overflow" Shrink_to_avoid_overflow ; make "grow to fill" Grow_to_fill @@ -135,12 +139,12 @@ let fit = ]) ;; -let visibility_component = +let visibility_component graph = let open Size_hooks.Visibility_tracker in - let%sub pos_x = Bonsai.state 0.0 in - let%sub pos_y = Bonsai.state 0.0 in - let%sub client_rect, set_client_rect = Bonsai.state_opt () in - let%sub visible_rect, set_visible_rect = Bonsai.state_opt () in + let pos_x = Tuple2.uncurry Bonsai.both @@ Bonsai.state 0.0 graph in + let pos_y = Tuple2.uncurry Bonsai.both @@ Bonsai.state 0.0 graph in + let client_rect, set_client_rect = Bonsai.state_opt graph in + let visible_rect, set_visible_rect = Bonsai.state_opt graph in let%arr pos_x, inject_pos_x = pos_x and pos_y, inject_pos_y = pos_y and client_rect = client_rect @@ -199,8 +203,8 @@ let buttons current inject = Page.all |> List.map ~f:make_button_for_tab |> Vdom.Node.div ;; -let resizer_component = - Bonsai.const +let resizer_component _graph = + Bonsai.return (Vdom.Node.div [ Vdom.Node.h3 [ Vdom.Node.text "Resize me!" ] ; Vdom.Node.div @@ -213,12 +217,13 @@ let resizer_component = ]) ;; -let scroll_tracker_component = - let%sub state = - Bonsai.state_opt - () - ~sexp_of_model:[%sexp_of: Size_hooks.Scroll_tracker.Scrollable.t] - ~equal:[%equal: Size_hooks.Scroll_tracker.Scrollable.t] +let scroll_tracker_component graph = + let state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state_opt + graph + ~sexp_of_model:[%sexp_of: Size_hooks.Scroll_tracker.Scrollable.t] + ~equal:[%equal: Size_hooks.Scroll_tracker.Scrollable.t] in let%arr scrollable, set_scrollable = state in let status = @@ -244,19 +249,19 @@ let scroll_tracker_component = ] ;; -let component = - let%sub page, inject_page = - Bonsai.state Bulk_size ~sexp_of_model:[%sexp_of: Page.t] ~equal:[%equal: Page.t] +let component graph = + let page, inject_page = + Bonsai.state Bulk_size ~sexp_of_model:[%sexp_of: Page.t] ~equal:[%equal: Page.t] graph in - let%sub page_component = + let page_component = match%sub page with - | Bulk_size -> bulk_size_component - | Size -> size_component - | Visibility -> visibility_component - | Resizer -> resizer_component - | Fit -> fit - | Position -> position - | Scroll -> scroll_tracker_component + | Bulk_size -> bulk_size_component graph + | Size -> size_component graph + | Visibility -> visibility_component graph + | Resizer -> resizer_component graph + | Fit -> fit graph + | Position -> position graph + | Scroll -> scroll_tracker_component graph in let%arr page_component = page_component and page = page diff --git a/examples/element_size_util/style.mli b/examples/element_size_util/style.mli index 7f8e2ed7..62998a77 100644 --- a/examples/element_size_util/style.mli +++ b/examples/element_size_util/style.mli @@ -1,5 +1,5 @@ open! Core -open Bonsai_web +open Bonsai_web.Cont val primary : Vdom.Attr.t val resizable_using_css : Vdom.Attr.t diff --git a/examples/extensible_list/main.ml b/examples/extensible_list/main.ml index 0972a0e1..4f4a21e4 100644 --- a/examples/extensible_list/main.ml +++ b/examples/extensible_list/main.ml @@ -1,9 +1,9 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Extendy = Bonsai_web_ui_extendy -let component = +let component graph = let wrap_remove view remove_event = Vdom.Node.div [ Vdom.Node.button @@ -13,7 +13,7 @@ let component = ] in let%sub { contents; append; _ } = - Extendy.component' Bonsai_web_counters_example.single_counter ~wrap_remove + Extendy.component' Bonsai_web_counters_example.single_counter ~wrap_remove graph in let%arr contents = contents and append = append in diff --git a/examples/favicon_svg/main.ml b/examples/favicon_svg/main.ml index d294d6d5..30689e1d 100644 --- a/examples/favicon_svg/main.ml +++ b/examples/favicon_svg/main.ml @@ -1,5 +1,5 @@ open! Core -open Bonsai_web +open Bonsai_web.Cont module Style = [%css @@ -68,29 +68,43 @@ let slider ~min ~max ~value ~inject = () ;; -let component = +let component graph = let open Bonsai.Let_syntax in - let%sub text = - Bonsai.state_opt - () - ~sexp_of_model:[%sexp_of: String.t] - ~equal:[%equal: String.t] - ~default_model:"🤯" + let text = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state_opt + graph + ~sexp_of_model:[%sexp_of: String.t] + ~equal:[%equal: String.t] + ~default_model:"🤯" in - let%sub size = - Bonsai.state 80 ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] + let size = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state 80 ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] graph in - let%sub pos_x = - Bonsai.state 50 ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] + let pos_x = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state 50 ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] graph in - let%sub pos_y = - Bonsai.state 50 ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] + let pos_y = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state 50 ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] graph in - let%sub fg_color = - Bonsai.state "#000000" ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] + let fg_color = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state + "#000000" + ~sexp_of_model:[%sexp_of: String.t] + ~equal:[%equal: String.t] + graph in - let%sub bg_color = - Bonsai.state "#ffffff" ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] + let bg_color = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state + "#ffffff" + ~sexp_of_model:[%sexp_of: String.t] + ~equal:[%equal: String.t] + graph in let%arr text, inject_text = text and size, inject_size = size diff --git a/examples/feather_icons/controls.ml b/examples/feather_icons/controls.ml index a6c2f3ff..7fdf4b12 100644 --- a/examples/feather_icons/controls.ml +++ b/examples/feather_icons/controls.ml @@ -26,7 +26,7 @@ stylesheet let size_slider = Form.Elements.Range.int - ~extra_attrs:(Value.return [ Range.class_ ]) + ~extra_attrs:(Bonsai.return [ Range.class_ ]) ~min:12 ~max:100 ~default:default.size @@ -37,7 +37,7 @@ let size_slider = let stroke_width_slider = Form.Elements.Range.float - ~extra_attrs:(Value.return [ Range.class_ ]) + ~extra_attrs:(Bonsai.return [ Range.class_ ]) ~min:0.5 ~max:3. ~default:default.stroke_width @@ -58,13 +58,13 @@ stylesheet {| let display_none = Vdom.Attr.style (Css_gen.display `None) -let color_input ?(display = Value.return true) () = +let color_input ?(display = Bonsai.return true) () graph = let classes_ = Vdom.Attr.many [ Card_like.class_; Color_input.class_ ] in - let%sub extra_attr = + let extra_attr = let%arr display = display in if display then classes_ else Vdom.Attr.(classes_ @ display_none) in - Form.Elements.Color_picker.hex ~extra_attr () + Form.Elements.Color_picker.hex ~extra_attr () graph ;; module Style = @@ -116,17 +116,18 @@ module Fill = struct } |}] - let component : t Computation.t = + let component : Bonsai.graph -> t Bonsai.t = (* Equal to --js-primary-color *) + fun graph -> let default_fill_color = `Hex "#2085ef" in - let%sub fill_toggle = Form.Elements.Toggle.bool ~default:false () in - let%sub fill_on = + let fill_toggle = Form.Elements.Toggle.bool ~default:false () graph in + let fill_on = let%arr fill_toggle = fill_toggle in Form.value_or_default fill_toggle ~default:false in - let%sub fill_input = - let%sub form = color_input ~display:fill_on () in - Form.Dynamic.with_default (Value.return default_fill_color) form + let fill_input = + let form = color_input ~display:fill_on () graph in + Form.Dynamic.with_default (Bonsai.return default_fill_color) form graph in let%arr fill_toggle = fill_toggle and fill_on = fill_on @@ -156,11 +157,11 @@ module Fill = struct ;; end -let component = - let%sub size_slider = size_slider in - let%sub stroke_width_slider = stroke_width_slider in - let%sub stroke_input = color_input () in - let%sub fill = Fill.component in +let component graph = + let size_slider = size_slider graph in + let stroke_width_slider = stroke_width_slider graph in + let stroke_input = color_input () graph in + let fill = Fill.component graph in let%arr size_slider = size_slider and stroke_width_slider = stroke_width_slider and stroke_input = stroke_input diff --git a/examples/feather_icons/controls.mli b/examples/feather_icons/controls.mli index e2dc48b2..04bffa92 100644 --- a/examples/feather_icons/controls.mli +++ b/examples/feather_icons/controls.mli @@ -9,4 +9,4 @@ type t = } (** The part on the right where you can control the size/color/etc. of all the icons. *) -val component : (t * Vdom.Node.t) Computation.t +val component : Bonsai.graph -> (t * Vdom.Node.t) Bonsai.t diff --git a/examples/feather_icons/import.ml b/examples/feather_icons/import.ml index 92e36c5f..3233bd3a 100644 --- a/examples/feather_icons/import.ml +++ b/examples/feather_icons/import.ml @@ -1,5 +1,5 @@ open! Core -include Bonsai_web +include Bonsai_web.Cont include Bonsai.Let_syntax module Form = Bonsai_web_ui_form.With_automatic_view diff --git a/examples/feather_icons/main.ml b/examples/feather_icons/main.ml index 56985f25..f3923425 100644 --- a/examples/feather_icons/main.ml +++ b/examples/feather_icons/main.ml @@ -34,8 +34,8 @@ stylesheet } |}] -let left_section ~controls = - let%sub icons, search_bar = Search_bar.component in +let left_section ~controls graph = + let%sub icons, search_bar = Search_bar.component graph in let%arr icons = icons and search_bar = search_bar and controls = controls in @@ -50,9 +50,9 @@ module Main = [%css stylesheet {| } |}] -let main = - let%sub controls, controls_view = Controls.component in - let%sub left_section = left_section ~controls in +let main graph = + let%sub controls, controls_view = Controls.component graph in + let left_section = left_section ~controls graph in let%arr left_section = left_section and controls_view = controls_view in Vdom.Node.main ~attrs:[ Main.class_ ] [ left_section; controls_view ] @@ -70,8 +70,8 @@ module App = [%css stylesheet {| } |}] -let app = - let%sub main = main in +let app graph = + let main = main graph in let%arr main = main in Vdom.Node.div ~attrs:[ App.class_ ] [ header; main ] ;; diff --git a/examples/feather_icons/search_bar.ml b/examples/feather_icons/search_bar.ml index fd91142d..6ea24dfe 100644 --- a/examples/feather_icons/search_bar.ml +++ b/examples/feather_icons/search_bar.ml @@ -29,14 +29,15 @@ stylesheet } |}] -let component = - let%sub search_bar = +let component graph = + let search_bar = Form.Elements.Textbox.string ~extra_attrs: - (Value.return [ Vdom.Attr.many [ Card_like.class_; Search_bar.class_ ] ]) - ~placeholder:(Value.return "Search icons") + (Bonsai.return [ Vdom.Attr.many [ Card_like.class_; Search_bar.class_ ] ]) + ~placeholder:(Bonsai.return "Search icons") ~allow_updates_when_focused:`Never () + graph in let%arr search_bar = search_bar in let icons = diff --git a/examples/feather_icons/search_bar.mli b/examples/feather_icons/search_bar.mli index e116c28f..4aa91392 100644 --- a/examples/feather_icons/search_bar.mli +++ b/examples/feather_icons/search_bar.mli @@ -3,4 +3,4 @@ open! Import (** [component] returns the search bar itself (as a Vdom.Node.t) as well as all matching icons. *) -val component : (Feather_icon.t list * Vdom.Node.t) Computation.t +val component : Bonsai.graph -> (Feather_icon.t list * Vdom.Node.t) Bonsai.t diff --git a/examples/file_download_button/main.ml b/examples/file_download_button/main.ml index 2cdd7fc2..f5f3fea6 100644 --- a/examples/file_download_button/main.ml +++ b/examples/file_download_button/main.ml @@ -1,8 +1,8 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -let component = - Bonsai.const +let component _graph = + Bonsai.return Vdom_file_download.( Button.create ~button_text:"click me!" diff --git a/examples/floating_positioning/main.ml b/examples/floating_positioning/main.ml index f005c078..570deb84 100644 --- a/examples/floating_positioning/main.ml +++ b/examples/floating_positioning/main.ml @@ -13,8 +13,9 @@ module Config = struct } [@@deriving typed_fields] - let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t - = function + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with | X -> Form.Elements.Number.int ~default:50 @@ -22,6 +23,7 @@ module Config = struct ~min:0 ~allow_updates_when_focused:`Always () + graph | Y -> Form.Elements.Number.int ~default:100 @@ -29,6 +31,7 @@ module Config = struct ~min:0 ~allow_updates_when_focused:`Always () + graph | Width -> Form.Elements.Number.int ~default:10 @@ -36,6 +39,7 @@ module Config = struct ~min:0 ~allow_updates_when_focused:`Always () + graph | Height -> Form.Elements.Number.int ~default:10 @@ -43,6 +47,7 @@ module Config = struct ~min:0 ~allow_updates_when_focused:`Always () + graph ;; let label_for_field = `Inferred @@ -56,11 +61,17 @@ module Config = struct | Virtual of Virtual.t [@@deriving typed_variants] - let form_for_variant : type a. a Typed_variant.t -> Bonsai.graph -> a Form.t Bonsai.t - = function + let form_for_variant : type a. a Typed_variant.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with | Dom -> - Form.Elements.Number.int ~default:0 ~step:1 ~allow_updates_when_focused:`Always () - | Virtual -> virtual_form + Form.Elements.Number.int + ~default:0 + ~step:1 + ~allow_updates_when_focused:`Always + () + graph + | Virtual -> virtual_form graph ;; let variant_to_string : type a. a Typed_variant.t -> string = function @@ -81,20 +92,23 @@ module Config = struct } [@@deriving typed_fields] - let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t - = function + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with | Main_axis -> Form.Elements.Number.float ~default:0. ~step:1. ~allow_updates_when_focused:`Always () + graph | Cross_axis -> Form.Elements.Number.float ~default:0. ~step:1. ~allow_updates_when_focused:`Always () + graph ;; let label_for_field = `Inferred @@ -118,17 +132,20 @@ module Config = struct } [@@deriving typed_fields] - let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t - = function + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with | Position -> Form.Elements.Dropdown.enumerable ~init:`First_item (module Floating_positioning_new.Position) + graph | Alignment -> Form.Elements.Dropdown.enumerable ~init:`First_item (module Floating_positioning_new.Alignment) - | Offset -> offset_form + graph + | Offset -> offset_form graph ;; let label_for_field = `Inferred @@ -143,10 +160,11 @@ module Config = struct } [@@deriving typed_fields] - let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t - = function - | Popovers -> popovers_form - | Anchor_type -> anchor_form + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with + | Popovers -> popovers_form graph + | Anchor_type -> anchor_form graph ;; let label_for_field = `Inferred diff --git a/examples/focus/main.ml b/examples/focus/main.ml index 29aabb78..ae79246a 100644 --- a/examples/focus/main.ml +++ b/examples/focus/main.ml @@ -1,11 +1,11 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax -let component = - let%sub focus_on_activate_attr = Effect.Focus.on_activate () in - let%sub focus_on_effect = Effect.Focus.on_effect () in - let%sub theme = View.Theme.current in +let component graph = + let focus_on_activate_attr = Effect.Focus.on_activate () graph in + let focus_on_effect = Effect.Focus.on_effect () graph in + let theme = View.Theme.current graph in let%arr focus_on_activate_attr = focus_on_activate_attr and { attr = focus_on_effect_attr; focus = effect_to_focus; blur = _ } = focus_on_effect and theme = theme in diff --git a/examples/font_hosting/main.ml b/examples/font_hosting/main.ml index 176cf530..69442576 100644 --- a/examples/font_hosting/main.ml +++ b/examples/font_hosting/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont module Css = [%css @@ -15,8 +15,8 @@ stylesheet } |}] -let component = - Bonsai.const +let component _graph = + Bonsai.return (Vdom.Node.div ~attrs:[ Css.firacode ] [ Vdom.Node.text "text with some ligatures -> ==> >>=" ]) diff --git a/examples/form_handle_enter/main.ml b/examples/form_handle_enter/main.ml index 1e563e4f..14a0f6dd 100644 --- a/examples/form_handle_enter/main.ml +++ b/examples/form_handle_enter/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Form = Bonsai_web_ui_form.With_automatic_view @@ -20,39 +20,42 @@ module Css = [%css stylesheet {| color: black; } |}] -let component = - let%sub notifications = - Bonsai_web_ui_notifications.component (module Unit) ~equal:[%equal: Unit.t] +let component graph = + let notifications = + Bonsai_web_ui_notifications.component (module Unit) ~equal:[%equal: Unit.t] graph in - let%sub rendered_notifications = - Bonsai_web_ui_notifications.render notifications ~f:(fun ~close:_ _ -> - Bonsai.const - (Vdom.Node.div ~attrs:[ Css.notification ] [ Vdom.Node.text "Submitted form" ])) + let rendered_notifications = + Bonsai_web_ui_notifications.render + notifications + ~f:(fun ~close:_ _ _graph -> + Bonsai.return + (Vdom.Node.div ~attrs:[ Css.notification ] [ Vdom.Node.text "Submitted form" ])) + graph in - let%sub notify = + let notify = let%arr notifications = notifications in Bonsai_web_ui_notifications.send_notification ~close_after:(Time_ns.Span.of_sec 5.0) notifications () in - let%sub form1 = - Bonsai_web_ui_auto_generated.form (module One) ~textbox_for_string:() () + let form1 = + Bonsai_web_ui_auto_generated.form (module One) ~textbox_for_string:() () graph in - let%sub form2 = - Bonsai_web_ui_auto_generated.form (module One) ~textbox_for_string:() () + let form2 = + Bonsai_web_ui_auto_generated.form (module One) ~textbox_for_string:() () graph in - let%sub form3 = - Bonsai_web_ui_auto_generated.form (module One) ~textbox_for_string:() () + let form3 = + Bonsai_web_ui_auto_generated.form (module One) ~textbox_for_string:() () graph in - let%sub form4 = - Bonsai_web_ui_auto_generated.form (module Two) ~textbox_for_string:() () + let form4 = + Bonsai_web_ui_auto_generated.form (module Two) ~textbox_for_string:() () graph in - let%sub form5 = - Bonsai_web_ui_auto_generated.form (module Two) ~textbox_for_string:() () + let form5 = + Bonsai_web_ui_auto_generated.form (module Two) ~textbox_for_string:() () graph in - let%sub form6 = - Bonsai_web_ui_auto_generated.form (module Two) ~textbox_for_string:() () + let form6 = + Bonsai_web_ui_auto_generated.form (module Two) ~textbox_for_string:() () graph in let%arr rendered_notifications = rendered_notifications and notify = notify diff --git a/examples/form_table/main.ml b/examples/form_table/main.ml index c23c255f..865e0659 100644 --- a/examples/form_table/main.ml +++ b/examples/form_table/main.ml @@ -1,6 +1,5 @@ open! Core -open! Bonsai_web -open Bonsai.Let_syntax +open! Bonsai_web.Cont module Username = Username_kernel.Username module Form = Bonsai_web_ui_form.With_automatic_view module Table_form = Bonsai_experimental_table_form @@ -20,7 +19,9 @@ let form_of_t = let label_for_field = `Inferred - let form_for_field (type a) (field : a Typed_field.t) : a Form.t Computation.t = + let form_for_field (type a) (field : a Typed_field.t) + : Bonsai.graph -> a Form.t Bonsai.t + = match field with | Name -> Form.Elements.Textbox.string ~allow_updates_when_focused:`Never () | Age -> Form.Elements.Textbox.int ~allow_updates_when_focused:`Never () @@ -43,7 +44,7 @@ let table_form = ~key_column_initial_width:(`Px 100) form_of_t ~columns: - (Value.return + (Bonsai.return [ Table_form.Column.create "name" ; Table_form.Column.create "age" ; Table_form.Column.create "likes cats" @@ -51,16 +52,16 @@ let table_form = ;; let () = - let app = - let%sub table_form = table_form in - let%sub table_form = + let app graph = + let table_form = table_form graph in + let table_form = (* We call with_default, which sets the form to contain the starting data that we care about. *) - Form.Dynamic.with_default (Value.return starting_data) table_form + Form.Dynamic.with_default (Bonsai.return starting_data) table_form graph in (* The application doesn't really do anything with this form other than view it, so we just project out the view and return that for the application component. *) - Bonsai.pure Form.view_as_vdom table_form + Bonsai.map ~f:Form.view_as_vdom table_form in Bonsai_web.Start.start app ;; diff --git a/examples/forms/big_form.ml b/examples/forms/big_form.ml index 0a2a1f1d..6bd0fab5 100644 --- a/examples/forms/big_form.ml +++ b/examples/forms/big_form.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Form = Bonsai_web_ui_form.With_automatic_view module Codemirror_form = Bonsai_web_ui_codemirror_form @@ -98,14 +98,16 @@ module My_variant = struct let label_for_variant = `Inferred let initial_choice = `First_constructor - let form_for_variant : type a. a Typed_variant.t -> a Form.t Computation.t = function - | A -> Bonsai.const (Form.return ()) - | B -> E.Textbox.string ~allow_updates_when_focused:`Never () + let form_for_variant : type a. a Typed_variant.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with + | A -> Bonsai.return (Form.return ()) + | B -> E.Textbox.string ~allow_updates_when_focused:`Never () graph | C -> - Computation.map2 + Bonsai.map2 ~f:Form.both - (E.Number.int ~step:1 ~allow_updates_when_focused:`Never ()) - (E.Number.float ~step:1. ~allow_updates_when_focused:`Never ()) + (E.Number.int ~step:1 ~allow_updates_when_focused:`Never () graph) + (E.Number.float ~step:1. ~allow_updates_when_focused:`Never () graph) ;; end @@ -124,9 +126,11 @@ module Nested_record = struct let label_for_field = `Inferred - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = function - | B_1 -> checkbox - | B_2 -> checkbox + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with + | B_1 -> checkbox graph + | B_2 -> checkbox graph ;; end @@ -141,9 +145,11 @@ module Nested_record = struct let label_for_field = `Inferred - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = function - | A_1 -> checkbox - | A_2 -> inner_form + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with + | A_1 -> checkbox graph + | A_2 -> inner_form graph ;; end @@ -161,10 +167,12 @@ module Record_for_list = struct let label_for_field = `Inferred - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = function - | My_int -> E.Textbox.int ~allow_updates_when_focused:`Never () - | My_string -> E.Textbox.string ~allow_updates_when_focused:`Never () - | My_bool -> E.Checkbox.bool ~default:false () + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with + | My_int -> E.Textbox.int ~allow_updates_when_focused:`Never () graph + | My_string -> E.Textbox.string ~allow_updates_when_focused:`Never () graph + | My_bool -> E.Checkbox.bool ~default:false () graph ;; end @@ -183,7 +191,7 @@ module Int_blang = struct let form = Codemirror_form.Sexp_grammar_autocomplete.sexpable (module T) - (Value.return T.t_sexp_grammar) + (Bonsai.return T.t_sexp_grammar) ;; end @@ -227,7 +235,7 @@ type t = } [@@deriving typed_fields, sexp_of] -let ( >>|| ) a f = Bonsai.Computation.map a ~f +let ( >>|| ) a f = Bonsai.map a ~f let label_for_field : type a. a Typed_field.t -> string = fun field -> @@ -237,9 +245,12 @@ let label_for_field : type a. a Typed_field.t -> string = |> String.substr_replace_all ~pattern:"_" ~with_:" " ;; -let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = function - | Variant -> my_variant_form >>|| Form.tooltip "Tooltips can also be on header groups" - | Optional_variant -> my_variant_optional_form +let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with + | Variant -> + my_variant_form graph >>|| Form.tooltip "Tooltips can also be on header groups" + | Optional_variant -> my_variant_optional_form graph | Int_from_range -> E.Range.int ~allow_updates_when_focused:`Never @@ -250,132 +261,153 @@ let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = functio ~right_label:(Vdom.Node.text "Banana 🍌") ~step:1 () - | String_from_text -> E.Textbox.string ~allow_updates_when_focused:`Never () + graph + | String_from_text -> E.Textbox.string ~allow_updates_when_focused:`Never () graph | String_from_vert_radio -> E.Radio_buttons.list (module String) ~equal:[%equal: String.t] ~layout:`Vertical - (Value.return [ "first"; "second"; "third" ]) + (Bonsai.return [ "first"; "second"; "third" ]) + graph | String_from_horiz_radio -> E.Radio_buttons.list (module String) ~equal:[%equal: String.t] ~layout:`Horizontal - (Value.return [ "first"; "second"; "third" ]) + (Bonsai.return [ "first"; "second"; "third" ]) + graph | Radiobutton_buttons -> E.Radio_buttons.list (module String) ~equal:[%equal: String.t] - ~style:(Value.return E.Selectable_style.Button_like) - ~extra_button_attrs:(Value.return barebones_button_like) + ~style:(Bonsai.return E.Selectable_style.Button_like) + ~extra_button_attrs:(Bonsai.return barebones_button_like) ~layout:`Horizontal - (Value.return [ "first"; "second"; "third" ]) - | Time_span -> E.Date_time.time_span ~allow_updates_when_focused:`Never () - | Date -> E.Date_time.date ~allow_updates_when_focused:`Never () - | Date_range -> E.Date_time.Range.date ~allow_updates_when_focused:`Never () - | Time_ns_of_day -> E.Date_time.time ~allow_updates_when_focused:`Never () - | Time_ns_of_day_range -> E.Date_time.Range.time ~allow_updates_when_focused:`Never () - | Date_time -> E.Date_time.datetime_local ~allow_updates_when_focused:`Never () + (Bonsai.return [ "first"; "second"; "third" ]) + graph + | Time_span -> E.Date_time.time_span ~allow_updates_when_focused:`Never () graph + | Date -> E.Date_time.date ~allow_updates_when_focused:`Never () graph + | Date_range -> E.Date_time.Range.date ~allow_updates_when_focused:`Never () graph + | Time_ns_of_day -> E.Date_time.time ~allow_updates_when_focused:`Never () graph + | Time_ns_of_day_range -> + E.Date_time.Range.time ~allow_updates_when_focused:`Never () graph + | Date_time -> E.Date_time.datetime_local ~allow_updates_when_focused:`Never () graph | Date_time_range -> - E.Date_time.Range.datetime_local ~allow_updates_when_focused:`Never () + E.Date_time.Range.datetime_local ~allow_updates_when_focused:`Never () graph | Date_from_string -> - E.Textbox.string ~allow_updates_when_focused:`Never () + E.Textbox.string ~allow_updates_when_focused:`Never () graph >>|| Form.project ~parse_exn:Date.of_string ~unparse:Date.to_string | Sexp_from_string -> - E.Textbox.sexpable ~allow_updates_when_focused:`Never (module Sexp) - | Bool_from_toggle -> E.Toggle.bool ~default:false () - | Bool_from_checkbox -> E.Checkbox.bool ~default:false () + E.Textbox.sexpable ~allow_updates_when_focused:`Never (module Sexp) graph + | Bool_from_toggle -> E.Toggle.bool ~default:false () graph + | Bool_from_checkbox -> E.Checkbox.bool ~default:false () graph | Checklist_buttons -> E.Checkbox.set (module String) - ~style:(Value.return E.Selectable_style.Button_like) - ~extra_checkbox_attrs:(Value.return barebones_button_like) - (Value.return [ "abc"; "def" ]) - | Bool_from_dropdown -> E.Dropdown.enumerable (module Bool) ~to_string:Bool.to_string + ~style:(Bonsai.return E.Selectable_style.Button_like) + ~extra_checkbox_attrs:(Bonsai.return barebones_button_like) + (Bonsai.return [ "abc"; "def" ]) + graph + | Bool_from_dropdown -> + E.Dropdown.enumerable (module Bool) ~to_string:Bool.to_string graph | Typeahead -> E.Typeahead.single (module Rodents) ~equal:[%equal: Rodents.t] ~placeholder:"Typeahead here!" - ~to_option_description:(Value.return Rodents.to_description) - ~handle_unknown_option:(Value.return (fun s -> Some (Rodents.Other s))) - ~all_options:(Value.return Rodents.all) + ~to_option_description:(Bonsai.return Rodents.to_description) + ~handle_unknown_option:(Bonsai.return (fun s -> Some (Rodents.Other s))) + ~all_options:(Bonsai.return Rodents.all) + graph | Query_box_as_typeahead -> E.Query_box.single (module Rodents) - ~to_option_description:(Value.return Rodents.to_description) - ~handle_unknown_option:(Value.return (fun s -> Some (Rodents.Other s))) - ~all_options:(Value.return Rodents.all) + ~to_option_description:(Bonsai.return Rodents.to_description) + ~handle_unknown_option:(Bonsai.return (fun s -> Some (Rodents.Other s))) + ~all_options:(Bonsai.return Rodents.all) + graph | String_option -> E.Dropdown.list_opt (module String) ~equal:[%equal: String.t] - (Value.return [ "hello"; "world" ]) - | A_b_or_c -> E.Dropdown.enumerable (module A_B_or_C) + (Bonsai.return [ "hello"; "world" ]) + graph + | A_b_or_c -> E.Dropdown.enumerable (module A_B_or_C) graph | Many -> - let%sub multi_select = + let multi_select = E.Multiselect.list ~allow_updates_when_focused:`Never (module A_B_or_C) - (Value.return A_B_or_C.all) + (Bonsai.return A_B_or_C.all) + graph in - Form.Dynamic.collapsible_group (Value.return "collapsible group") multi_select + Form.Dynamic.collapsible_group (Bonsai.return "collapsible group") multi_select graph | Many2 -> - let%sub multi_select = + let multi_select = E.Multiselect.list ~allow_updates_when_focused:`Never (module A_B_or_C) - (Value.return A_B_or_C.all) + (Bonsai.return A_B_or_C.all) + graph in - let%sub multi_select2 = + let multi_select2 = E.Multiselect.list ~allow_updates_when_focused:`Never (module A_B_or_C) (multi_select >>| Form.value_or_default ~default:[]) + graph in let%arr multi_select = multi_select and multi_select2 = multi_select2 in Form.both multi_select multi_select2 |> Form.project ~parse_exn:snd ~unparse:(fun selected -> selected, selected) | String_set -> - E.Checkbox.set (module String) (Value.return [ "first"; "second"; "third"; "fourth" ]) + E.Checkbox.set + (module String) + (Bonsai.return [ "first"; "second"; "third"; "fourth" ]) + graph | Files -> - E.File_select.multiple ~accept:[ `Mimetype "application/pdf"; `Extension ".csv" ] () + E.File_select.multiple + ~accept:[ `Mimetype "application/pdf"; `Extension ".csv" ] + () + graph | Rank -> - let%sub rank = + let rank = E.Rank.list (module String) - (fun ~source item -> + (fun ~source item _graph -> let%arr item = item and source = source in Vdom.Node.div ~attrs:[ source ] [ Vdom.Node.text item ]) + graph in - Form.Dynamic.with_default (Value.return [ "aaaaaa"; "bbbbbb"; "cccccc" ]) rank + Form.Dynamic.with_default (Bonsai.return [ "aaaaaa"; "bbbbbb"; "cccccc" ]) rank graph | Query_box -> - let%sub input = - Bonsai.const (String.Map.of_alist_exn [ "abc", "abc"; "def", "def"; "ghi", "ghi" ]) + let input = + Bonsai.return (String.Map.of_alist_exn [ "abc", "abc"; "def", "def"; "ghi", "ghi" ]) in E.Query_box.create (module String) - ~selected_item_attr:(Value.return Query_box_css.selected_item) - ~extra_list_container_attr:(Value.return Query_box_css.list) - ~selection_to_string:(Value.return Fn.id) - ~f:(fun query -> + ~selected_item_attr:(Bonsai.return Query_box_css.selected_item) + ~extra_list_container_attr:(Bonsai.return Query_box_css.list) + ~selection_to_string:(Bonsai.return Fn.id) + ~f:(fun query _graph -> let%arr query = query and input = input in Map.filter_map input ~f:(fun data -> if String.is_prefix ~prefix:query data then Some (Vdom.Node.text data) else None)) () - | Nested_record -> Nested_record.form - | Record_list_as_table -> Record_for_list.form - | Color_picker -> E.Color_picker.hex () - | Int_blang -> Int_blang.form - | Password -> E.Password.string ~allow_updates_when_focused:`Never () + graph + | Nested_record -> Nested_record.form graph + | Record_list_as_table -> Record_for_list.form graph + | Color_picker -> E.Color_picker.hex () graph + | Int_blang -> Int_blang.form graph + | Password -> E.Password.string ~allow_updates_when_focused:`Never () graph ;; -let form = - let%sub form = +let form graph = + let form = Form.Typed.Record.make (module struct module Typed_field = Typed_field @@ -383,14 +415,15 @@ let form = let label_for_field = `Computed label_for_field let form_for_field = form_for_field end) - |> Computation.map ~f:(Form.label "The big form") + graph + |> Bonsai.map ~f:(Form.label "The big form") in - Form.Dynamic.error_hint form + Form.Dynamic.error_hint form graph ;; -let component = - let%sub form = form in - let%sub editable, toggle_editable = Bonsai.toggle ~default_model:true in +let component graph = + let form = form graph in + let editable, toggle_editable = Bonsai.toggle ~default_model:true graph in let%arr editable = editable and toggle_editable = toggle_editable and form = form in diff --git a/examples/forms/big_form.mli b/examples/forms/big_form.mli index c79d00dd..fa35baae 100644 --- a/examples/forms/big_form.mli +++ b/examples/forms/big_form.mli @@ -1,4 +1,4 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -val component : Vdom.Node.t Computation.t +val component : Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/forms/file_form.ml b/examples/forms/file_form.ml index 1cffdeb5..1bb4547d 100644 --- a/examples/forms/file_form.ml +++ b/examples/forms/file_form.ml @@ -1,21 +1,21 @@ open! Core -open Bonsai_web +open Bonsai_web.Cont open Bonsai.Let_syntax -let form = - let%sub file_picker = - Bonsai_web_ui_form.With_automatic_view.Elements.File_select.single () +let form graph = + let file_picker = + Bonsai_web_ui_form.With_automatic_view.Elements.File_select.single () graph in - let%sub file_from_form = + let file_from_form = let%arr file_picker = file_picker in Bonsai_web_ui_form.With_automatic_view.value file_picker |> Or_error.ok in - let%sub result = Bonsai_web_ui_file.Read_on_change.create_single_opt file_from_form in - let%sub result = + let result = Bonsai_web_ui_file.Read_on_change.create_single_opt file_from_form graph in + let result = match%sub result with - | None -> Bonsai.const Vdom.Node.none + | None -> Bonsai.return Vdom.Node.none | Some (_, (Bonsai_web_ui_file.Read_on_change.Status.Starting | In_progress _)) -> - Bonsai.const (View.text "file still loading") + Bonsai.return (View.text "file still loading") | Some (filename, Complete (Error error)) -> let%arr error = error and filename = filename in diff --git a/examples/forms/file_form.mli b/examples/forms/file_form.mli index a48c4329..68a9aa65 100644 --- a/examples/forms/file_form.mli +++ b/examples/forms/file_form.mli @@ -1,4 +1,4 @@ open! Core -open Bonsai_web +open Bonsai_web.Cont -val form : Vdom.Node.t Computation.t +val form : Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/forms/form_with_submit.ml b/examples/forms/form_with_submit.ml index 09d64dd7..decd5707 100644 --- a/examples/forms/form_with_submit.ml +++ b/examples/forms/form_with_submit.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont module Form = Bonsai_web_ui_form.With_automatic_view module E = Form.Elements @@ -12,9 +12,11 @@ module T = struct let label_for_field = `Inferred - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = function - | A -> E.Multiple.list (E.Textbox.int ~allow_updates_when_focused:`Never ()) - | B -> E.Textbox.string ~allow_updates_when_focused:`Never () + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with + | A -> E.Multiple.list (E.Textbox.int ~allow_updates_when_focused:`Never ()) graph + | B -> E.Textbox.string ~allow_updates_when_focused:`Never () graph ;; end @@ -27,8 +29,9 @@ let alert_effect = Effect.of_sync_fun alert ;; -let component = - let%map.Computation form = form in +let component graph = + let open Bonsai.Let_syntax in + let%map form = form graph in let on_submit = Form.Submit.create ~f:(fun t -> alert_effect ([%sexp_of: T.t] t)) () in Vdom.Node.div [ Vdom.Node.h1 [ Vdom.Node.text "Form With Submit" ] diff --git a/examples/forms/form_with_submit.mli b/examples/forms/form_with_submit.mli index c79d00dd..fa35baae 100644 --- a/examples/forms/form_with_submit.mli +++ b/examples/forms/form_with_submit.mli @@ -1,4 +1,4 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -val component : Vdom.Node.t Computation.t +val component : Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/forms/list_form.ml b/examples/forms/list_form.ml index 8788d4e3..773b2b0f 100644 --- a/examples/forms/list_form.ml +++ b/examples/forms/list_form.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Form = Bonsai_web_ui_form.With_automatic_view module E = Form.Elements @@ -68,8 +68,8 @@ module Simple_list = struct E.Multiple.stringable_list (module T) ~equal:[%equal: T.t] - ~extra_pill_container_attr:(Value.return S.container) - ~extra_pill_attr:(Value.return S.pill) + ~extra_pill_container_attr:(Bonsai.return S.container) + ~extra_pill_attr:(Bonsai.return S.pill) ;; end @@ -97,25 +97,29 @@ module Advanced_list = struct let label_for_field = `Inferred - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = function - | Duration_hrs -> E.Textbox.int ~allow_updates_when_focused:`Never () - | Price -> E.Textbox.float ~allow_updates_when_focused:`Never () + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with + | Duration_hrs -> E.Textbox.int ~allow_updates_when_focused:`Never () graph + | Price -> E.Textbox.float ~allow_updates_when_focused:`Never () graph ;; end) ;; - let form_per_symbol = - let%sub symbol_form = E.Textbox.string ~allow_updates_when_focused:`Never () in + let form_per_symbol graph = + let symbol_form = E.Textbox.string ~allow_updates_when_focused:`Never () graph in Form.Typed.Record.make (module struct module Typed_field = Per_symbol.Typed_field let label_for_field = `Inferred - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = function - | Symbol -> return symbol_form + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with + | Symbol -> symbol_form | Configs -> - let%sub add_element_text = + let add_element_text = let%arr symbol_form = symbol_form in let symbol = Form.value_or_default ~default:"" symbol_form in sprintf "add config for %s" symbol @@ -124,15 +128,17 @@ module Advanced_list = struct form_for_config ~button_placement:`Indented ~add_element_text + graph ;; end) + graph ;; let many_symbols = Form.Elements.Multiple.list form_per_symbol ~button_placement:`Indented - ~add_element_text:(Value.return "add new symbol") + ~add_element_text:(Bonsai.return "add new symbol") ;; let starting_value = @@ -145,18 +151,18 @@ module Advanced_list = struct ] ;; - let component = - let%sub many_symbols = many_symbols in - let%sub many_symbols = - Form.Dynamic.with_default (Bonsai.Value.return starting_value) many_symbols + let component graph = + let many_symbols = many_symbols graph in + let many_symbols = + Form.Dynamic.with_default (Bonsai.return starting_value) many_symbols graph in - return many_symbols + many_symbols ;; end -let component = - let%sub simple_list = Simple_list.component in - let%sub advanced_list = Advanced_list.component in +let component graph = + let simple_list = Simple_list.component graph in + let advanced_list = Advanced_list.component graph in let%arr simple_list = simple_list and advanced_list = advanced_list in let simple_output = diff --git a/examples/forms/list_form.mli b/examples/forms/list_form.mli index c79d00dd..fa35baae 100644 --- a/examples/forms/list_form.mli +++ b/examples/forms/list_form.mli @@ -1,4 +1,4 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -val component : Vdom.Node.t Computation.t +val component : Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/forms/main.ml b/examples/forms/main.ml index 9f485596..78cf76dc 100644 --- a/examples/forms/main.ml +++ b/examples/forms/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont module Style = [%css @@ -16,12 +16,12 @@ stylesheet } |}] -let component = - let%map.Computation big_form = Big_form.component - and list_form = List_form.component - and form_with_submit = Form_with_submit.component - and typed_record = Typed.component - and file_form = File_form.form in +let component graph = + let%map.Bonsai big_form = Big_form.component graph + and list_form = List_form.component graph + and form_with_submit = Form_with_submit.component graph + and typed_record = Typed.component graph + and file_form = File_form.form graph in Vdom.Node.div ~attrs:[ Style.container ] [ big_form; list_form; form_with_submit; typed_record; file_form ] diff --git a/examples/forms/typed.ml b/examples/forms/typed.ml index 544eb88e..c5fff868 100644 --- a/examples/forms/typed.ml +++ b/examples/forms/typed.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont module Form = Bonsai_web_ui_form.With_automatic_view module E = Form.Elements @@ -17,9 +17,11 @@ module Person = struct let label_for_field = `Computed field_to_string - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = function - | Name -> E.Textbox.string ~allow_updates_when_focused:`Never () - | Age -> E.Textbox.int ~allow_updates_when_focused:`Never () + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with + | Name -> E.Textbox.string ~allow_updates_when_focused:`Never () graph + | Age -> E.Textbox.int ~allow_updates_when_focused:`Never () graph ;; end @@ -45,12 +47,14 @@ module Dyn = struct let label_for_variant = `Computed to_string let initial_choice = `First_constructor - let form_for_variant : type a. a Typed_variant.t -> a Form.t Computation.t = function - | Unit -> Bonsai.const (Form.return ()) - | Integer -> E.Textbox.int ~allow_updates_when_focused:`Never () - | Floating -> E.Textbox.float ~allow_updates_when_focused:`Never () - | Text -> E.Textbox.string ~allow_updates_when_focused:`Never () - | People -> Form.Typed.Record.make_table (module Person) + let form_for_variant : type a. a Typed_variant.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with + | Unit -> Bonsai.return (Form.return ()) + | Integer -> E.Textbox.int ~allow_updates_when_focused:`Never () graph + | Floating -> E.Textbox.float ~allow_updates_when_focused:`Never () graph + | Text -> E.Textbox.string ~allow_updates_when_focused:`Never () graph + | People -> Form.Typed.Record.make_table (module Person) graph ;; end @@ -98,19 +102,20 @@ module Food = struct : type a cmp. a Typed_variant.t -> (a, cmp) Bonsai.comparator - -> (a, cmp) Set.t Form.t Computation.t + -> Bonsai.graph + -> (a, cmp) Set.t Form.t Bonsai.t = - fun variant (module Cmp) -> + fun variant (module Cmp) graph -> match variant with | Snack -> - let%sub.Bonsai checkbox = E.Checkbox.bool ~default:false () in + let checkbox = E.Checkbox.bool ~default:false () graph in let%arr.Bonsai checkbox = checkbox in Form.project checkbox ~parse_exn:(fun is_set -> if is_set then Set.singleton (module Cmp) () else Set.empty (module Cmp)) ~unparse:(fun set -> Set.is_empty set |> not) - | Meal -> E.Typeahead.set (module Cmp) ~all_options:(Value.return Meal.all) + | Meal -> E.Typeahead.set (module Cmp) ~all_options:(Bonsai.return Meal.all) graph ;; include Comparable.Make_plain (T) @@ -118,12 +123,13 @@ end let food_form = Form.Typed.Variant.make_set (module Food) -let component = - let%map.Computation person = person_form - and dyn = dyn_form - and radio_form = radio_form - and horizontal_radio_form = horizontal_radio_form - and food_form = food_form in +let component graph = + let open Bonsai.Let_syntax in + let%arr person = person_form graph + and dyn = dyn_form graph + and radio_form = radio_form graph + and horizontal_radio_form = horizontal_radio_form graph + and food_form = food_form graph in Vdom.Node.div [ Vdom.Node.h1 [ Vdom.Node.text "Typed Fields" ] ; Form.View.to_vdom (Form.view person) diff --git a/examples/forms/typed.mli b/examples/forms/typed.mli index c79d00dd..fa35baae 100644 --- a/examples/forms/typed.mli +++ b/examples/forms/typed.mli @@ -1,4 +1,4 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -val component : Vdom.Node.t Computation.t +val component : Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/freeform_multiselect/main.ml b/examples/freeform_multiselect/main.ml index 0a416d49..6506f2f6 100644 --- a/examples/freeform_multiselect/main.ml +++ b/examples/freeform_multiselect/main.ml @@ -1,11 +1,11 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -let components = +let components graph = let open! Bonsai.Let_syntax in let open! Bonsai_web_ui_freeform_multiselect in - let%sub control = - Freeform_multiselect.create ~placeholder:"Enter something here.." () + let control = + Freeform_multiselect.create ~placeholder:"Enter something here.." () graph in let%arr selected, control, (_ : String.Set.t -> unit Ui_effect.t) = control in let have_you_selected_something = diff --git a/examples/gauge/main.ml b/examples/gauge/main.ml index 72c155e1..4ea4c4a5 100644 --- a/examples/gauge/main.ml +++ b/examples/gauge/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Gauge = Bonsai_web_ui_gauge open Vdom @@ -66,10 +66,10 @@ let colors = let radius = 30. -let ticker = - let%sub percentage, increase = +let ticker graph = + let percentage, increase = Bonsai.state_machine0 - () + graph ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] ~sexp_of_action:[%sexp_of: Unit.t] @@ -77,9 +77,9 @@ let ticker = ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model () -> (model + 1) % 101) in - let%sub color_index, increment = + let color_index, increment = Bonsai.state_machine0 - () + graph ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] ~sexp_of_action:[%sexp_of: Unit.t] @@ -88,7 +88,7 @@ let ticker = prev + (1 % Array.length colors)) in let%sub () = - let%sub effect = + let effect = let%arr increase = increase and increment = increment in Effect.Many [ increase (); increment () ] @@ -98,19 +98,21 @@ let ticker = ~trigger_on_activate:false (Time_ns.Span.of_sec 0.1) effect + graph; + Bonsai.return () in let%arr percentage = percentage and color_index = color_index in Percent.of_percentage (Int.to_float percentage), color_index ;; -let component = - let%sub percentage, color_index = ticker in - let%sub gauge1 = +let component graph = + let%sub percentage, color_index = ticker graph in + let gauge1 = let%arr percentage = percentage in Gauge.create ~radius percentage in - let%sub gauge2 = + let gauge2 = let percent_to_color p = let open Float in let p = Percent.to_percentage p in @@ -125,8 +127,8 @@ let component = let%arr percentage = percentage in Gauge.create ~percent_to_color ~radius percentage in - let%sub gauge3 = - let%sub percent_to_color = + let gauge3 = + let percent_to_color = let%arr color_index = color_index in let color = Array.get colors (color_index % Array.length colors) in Fn.const (`Hex color) diff --git a/examples/handle_io/main.ml b/examples/handle_io/main.ml index 517b07d4..b0046961 100644 --- a/examples/handle_io/main.ml +++ b/examples/handle_io/main.ml @@ -1,12 +1,12 @@ open! Core open! Async_kernel -open! Bonsai_web +open! Bonsai_web.Cont let print_effect = Effect.of_sync_fun print_endline -let component = +let component _graph = let on_click = print_effect "hello world" in - Bonsai.const + Bonsai.return (Vdom.Node.button ~attrs:[ Vdom.Attr.on_click (fun _ -> on_click) ] [ Vdom.Node.text "click me to print a thing to the console" ]) diff --git a/examples/hello_view/main.ml b/examples/hello_view/main.ml index 1fc6bdf5..cc2f1a1b 100644 --- a/examples/hello_view/main.ml +++ b/examples/hello_view/main.ml @@ -1,9 +1,9 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax -let my_component = - let%sub theme = View.Theme.current in +let my_component graph = + let theme = View.Theme.current graph in let%arr theme = theme in View.vbox ~cross_axis_alignment:Center @@ -14,7 +14,7 @@ let my_component = ;; let app = - let theme = Value.return (Kado.theme ~version:Bleeding ()) in + let theme = Bonsai.return (Kado.theme ~version:Bleeding ()) in View.Theme.set_for_app theme my_component ;; diff --git a/examples/hello_world/main.ml b/examples/hello_world/main.ml index 874eb6d0..9cfb28f1 100644 --- a/examples/hello_world/main.ml +++ b/examples/hello_world/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -let component = Bonsai.const (Vdom.Node.text "hello world") +let component _graph = Bonsai.return (Vdom.Node.text "hello world") let () = Bonsai_web.Start.start component diff --git a/examples/inline_css/main.ml b/examples/inline_css/main.ml index a4331a94..0b553bb3 100644 --- a/examples/inline_css/main.ml +++ b/examples/inline_css/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont module Boxes = struct module Style = @@ -104,13 +104,13 @@ stylesheet |}] let () = - Bonsai_web.Start.start - (Bonsai.const - (Vdom.Node.div - [ Vdom.Node.h1 [ Vdom.Node.text "Boxes" ] - ; Boxes.component - ; Vdom.Node.h1 [ Vdom.Node.text "Themeable Component" ] - ; Themeable.component () - ; Themeable.component ~style:(module My_theme) () - ])) + Bonsai_web.Start.start (fun _graph -> + Bonsai.return + (Vdom.Node.div + [ Vdom.Node.h1 [ Vdom.Node.text "Boxes" ] + ; Boxes.component + ; Vdom.Node.h1 [ Vdom.Node.text "Themeable Component" ] + ; Themeable.component () + ; Themeable.component ~style:(module My_theme) () + ])) ;; diff --git a/examples/inline_css_dynamic/main.ml b/examples/inline_css_dynamic/main.ml index 9c0ac198..29b2fe88 100644 --- a/examples/inline_css_dynamic/main.ml +++ b/examples/inline_css_dynamic/main.ml @@ -1,18 +1,20 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont module Form = Bonsai_web_ui_form.With_automatic_view let tomato = `Hex "#FF6347" -let component = +let component graph = let open Bonsai.Let_syntax in - let%sub applied, toggle = Bonsai.toggle ~default_model:true in - let%sub color_form = - Form.Elements.Color_picker.hex () - |> Bonsai.sub ~f:(Form.Dynamic.with_default (Value.return tomato)) + let applied, toggle = Bonsai.toggle ~default_model:true graph in + let color_form = + Form.Dynamic.with_default + (Bonsai.return tomato) + (Form.Elements.Color_picker.hex () graph) + graph in - let%sub toggle_button = - let%sub theme = View.Theme.current in + let toggle_button = + let theme = View.Theme.current graph in let%arr toggle = toggle and applied = applied and theme = theme in @@ -23,11 +25,11 @@ let component = | false -> "Apply" | true -> "Disable") in - let%sub attr = + let attr = match%sub applied with - | false -> Bonsai.const Vdom.Attr.empty + | false -> Bonsai.return Vdom.Attr.empty | true -> - let%sub color = + let color = let%arr color_form = color_form in Form.value_or_default color_form ~default:tomato in diff --git a/examples/inline_css_private_appending/main.ml b/examples/inline_css_private_appending/main.ml index 3bebd533..c3f17104 100644 --- a/examples/inline_css_private_appending/main.ml +++ b/examples/inline_css_private_appending/main.ml @@ -1,11 +1,11 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax -let component = - let%sub height, increase_height = +let component graph = + let height, increase_height = Bonsai.state_machine0 - () + graph ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] ~sexp_of_action:[%sexp_of: Unit.t] @@ -13,7 +13,7 @@ let component = ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) old_model () -> old_model + 10) in - let%sub append_effect = + let append_effect = let%arr height = height in Effect.of_sync_fun Inline_css.Private.append @@ -23,7 +23,7 @@ let component = } |}] in - let%sub effect = + let effect = let%arr increase_height = increase_height and append_effect = append_effect in let%bind.Effect () = append_effect in diff --git a/examples/inline_css_with_var/main.ml b/examples/inline_css_with_var/main.ml index 3e335384..6972dfc9 100644 --- a/examples/inline_css_with_var/main.ml +++ b/examples/inline_css_with_var/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont module Style = [%css @@ -34,4 +34,6 @@ let component = [ red_box; blue_box; other_box ] ;; -let () = Bonsai_web.Start.start (Bonsai.const (Vdom.Node.div [ component ])) +let () = + Bonsai_web.Start.start (fun _graph -> Bonsai.return (Vdom.Node.div [ component ])) +;; diff --git a/examples/inside_incr_dom/my_bonsai_component.ml b/examples/inside_incr_dom/my_bonsai_component.ml index f9fee95c..f236fa9e 100644 --- a/examples/inside_incr_dom/my_bonsai_component.ml +++ b/examples/inside_incr_dom/my_bonsai_component.ml @@ -1,23 +1,24 @@ open! Core -open Bonsai_web +open Bonsai_web.Cont open Bonsai.Let_syntax include - (val Bonsai_web.To_incr_dom.convert (fun (_ : unit Bonsai.Value.t) -> - let%sub counters = Bonsai_web_counters_example.application in - let%sub () = + (val Bonsai_web.To_incr_dom.convert (fun (_ : unit Bonsai.t) graph -> + let counters = Bonsai_web_counters_example.application graph in + let () = Bonsai.Edge.lifecycle - () - ~on_activate:(Value.return (Bonsai.Effect.print_s [%message "hi!"])) + ~on_activate:(Bonsai.return (Bonsai.Effect.print_s [%message "hi!"])) + graph in - let%sub () = + let () = Bonsai.Clock.every ~when_to_start_next_effect:`Every_multiple_of_period_blocking (Time_ns.Span.of_sec 1.0) - (Value.return (Bonsai.Effect.print_s [%message "tick"])) + (Bonsai.return (Bonsai.Effect.print_s [%message "tick"])) + graph in - let%sub wait_after_display = Bonsai.Edge.wait_after_display in - let%sub print_button = + let wait_after_display = Bonsai.Edge.wait_after_display graph in + let print_button = let%arr wait_after_display = wait_after_display in Vdom.Node.button ~attrs: diff --git a/examples/kado_specific/button.ml b/examples/kado_specific/button.ml index a8925669..c45f627a 100644 --- a/examples/kado_specific/button.ml +++ b/examples/kado_specific/button.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Autogen = Bonsai_web_ui_auto_generated module Form = Bonsai_web_ui_form.With_automatic_view @@ -76,19 +76,20 @@ let form_store = ~default:Parameters.default ;; -let component = - let%sub theme = View.Theme.current in - let%sub form = - Bonsai.sub - (Autogen.form (module Parameters) ()) - ~f:(Form.Dynamic.with_default (Persistent_var.value form_store)) +let component graph = + let theme = View.Theme.current graph in + let form = + Form.Dynamic.with_default + (Persistent_var.value form_store) + (Autogen.form (module Parameters) () graph) + graph in let%sub () = Bonsai_extra.mirror () ~sexp_of_model:[%sexp_of: Parameters.t] ~equal:[%equal: Parameters.t] - ~store_set:(Value.return (Persistent_var.effect form_store)) + ~store_set:(Bonsai.return (Persistent_var.effect form_store)) ~store_value:(Persistent_var.value form_store) ~interactive_value: (let%map form = form in @@ -96,6 +97,7 @@ let component = ~interactive_set: (let%map form = form in Form.set form) + graph in let%arr theme = theme and form = form in diff --git a/examples/kado_specific/button.mli b/examples/kado_specific/button.mli index 42acf7fa..a8a7a4bc 100644 --- a/examples/kado_specific/button.mli +++ b/examples/kado_specific/button.mli @@ -1,4 +1,4 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -val component : Vdom.Node.t list Computation.t +val component : Bonsai.graph -> Vdom.Node.t list Bonsai.t diff --git a/examples/kado_specific/main.ml b/examples/kado_specific/main.ml index b8b51c4a..88e1f7f3 100644 --- a/examples/kado_specific/main.ml +++ b/examples/kado_specific/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax let card ~theme ~name ~content = @@ -12,27 +12,27 @@ let card ~theme ~name ~content = content ;; -let buttons = - let%map.Computation buttons = Button.component - and theme = View.Theme.current in +let buttons graph = + let%map.Bonsai buttons = Button.component graph + and theme = View.Theme.current graph in card ~theme ~name:"buttons" ~content:buttons ;; -let textbox = - let%map.Computation textbox = Textbox.component - and theme = View.Theme.current in +let textbox graph = + let%map.Bonsai textbox = Textbox.component graph + and theme = View.Theme.current graph in card ~theme ~name:"input" ~content:textbox ;; -let app = - let%map.Computation buttons = buttons - and textbox = textbox in +let app graph = + let%map.Bonsai buttons = buttons graph + and textbox = textbox graph in View.hbox ~gap:(`Em 1) [ buttons; textbox ] ;; -let app = - let theme = Value.return (Kado.theme ~version:Bleeding ()) in - View.Theme.set_for_app theme app +let app graph = + let theme = Bonsai.return (Kado.theme ~version:Bleeding ()) in + View.Theme.set_for_app theme app graph ;; let () = Bonsai_web.Start.start app diff --git a/examples/kado_specific/textbox.ml b/examples/kado_specific/textbox.ml index d0b2b7e9..0bb1d158 100644 --- a/examples/kado_specific/textbox.ml +++ b/examples/kado_specific/textbox.ml @@ -1,11 +1,11 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax let on_change _ = Effect.Ignore -let component = - let%map.Computation theme = View.Theme.current in +let component graph = + let%map theme = View.Theme.current graph in [ Kado.Unstable.Input.textbox ~constants:(View.constants theme) ~input_attr:Vdom.Attr.empty diff --git a/examples/kado_specific/textbox.mli b/examples/kado_specific/textbox.mli index 42acf7fa..a8a7a4bc 100644 --- a/examples/kado_specific/textbox.mli +++ b/examples/kado_specific/textbox.mli @@ -1,4 +1,4 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -val component : Vdom.Node.t list Computation.t +val component : Bonsai.graph -> Vdom.Node.t list Bonsai.t diff --git a/examples/keyboard/main.ml b/examples/keyboard/main.ml index d04d3f67..e5a19eb6 100644 --- a/examples/keyboard/main.ml +++ b/examples/keyboard/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax open Vdom_keyboard module Js = Js_of_ocaml.Js @@ -100,16 +100,17 @@ let handle_event inject = module Style = [%css stylesheet {| .red { color: red } |}] -let component = - let%sub model_and_inject = - Bonsai.state_machine0 - () - ~sexp_of_model:[%sexp_of: Model.t] - ~equal:[%equal: Model.t] - ~sexp_of_action:[%sexp_of: Action.t] - ~default_model:[] - ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model action -> - List.append model [ action ]) +let component graph = + let model_and_inject = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state_machine0 + graph + ~sexp_of_model:[%sexp_of: Model.t] + ~equal:[%equal: Model.t] + ~sexp_of_action:[%sexp_of: Action.t] + ~default_model:[] + ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model action -> + List.append model [ action ]) in let%arr model, inject = model_and_inject in let last_event = List.last model in diff --git a/examples/modal/main.ml b/examples/modal/main.ml index 43dbfec1..d424d482 100644 --- a/examples/modal/main.ml +++ b/examples/modal/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax module Modal = Bonsai_web_ui_modal module Native_modal = Draft_modal @@ -8,12 +8,12 @@ module Native_modal = Draft_modal let center = Vdom.Attr.style (Css_gen.text_align `Center) -let modal_1_contents = - Bonsai.const (Vdom.Node.div ~attrs:[ center ] [ Vdom.Node.text "Surprise!" ]) +let modal_1_contents _graph = + Bonsai.return (Vdom.Node.div ~attrs:[ center ] [ Vdom.Node.text "Surprise!" ]) ;; -let modal_2_contents n = - let%sub got_ya = +let modal_2_contents n _graph = + let got_ya = match%arr n with | 1 -> "Got ya!" | n -> sprintf "Got ya %d times!" n @@ -24,21 +24,23 @@ let modal_2_contents n = [ Vdom.Node.text "Surprise!"; Vdom.Node.br (); Vdom.Node.text got_ya ] ;; -let original_app = - let%sub modal_1 = +let original_app graph = + let modal_1 = Modal.create (module Unit) (fun _ ~hide_self:_ -> modal_1_contents) ~equal:[%equal: Unit.t] + graph in - let%sub modal_2 = + let modal_2 = Modal.create (module Int) (fun n ~hide_self:_ -> modal_2_contents n) ~equal:[%equal: Int.t] + graph in - let%sub state, set_state = - Bonsai.state 1 ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] + let state, set_state = + Bonsai.state 1 ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] graph in let%arr state = state and set_state = set_state @@ -92,8 +94,8 @@ let dialog_contents ?title ?close_button:close_button_on_cancel contents = (* A button that adds lots of content to the page. Used to see/test that we disabled body scrolling. *) -let add_lots_of_content_markup = - let%sub show, toggle = Bonsai.toggle ~default_model:false in +let add_lots_of_content_markup graph = + let show, toggle = Bonsai.toggle ~default_model:false graph in let%arr show = show and toggle = toggle in let content = @@ -115,14 +117,16 @@ let add_lots_of_content_markup = [ button; content ] ;; -let stacking_example = +let stacking_example (* Note: there's probably a way to write this such that the outer modal takes a bonsai - component for it's contents. For now it just demonstrates that the vdom's can stack - *) + component for it's contents. For now it just demonstrates that the vdom's can stack + *) (* Creates the button to show/hide the modal and has state management. creator should - be fun on_cancel -> modal *) - let%sub show_outer, toggle_outer = Bonsai.toggle ~default_model:false in - let%sub show_inner, toggle_inner = Bonsai.toggle ~default_model:false in + be fun on_cancel -> modal *) + graph + = + let show_outer, toggle_outer = Bonsai.toggle ~default_model:false graph in + let show_inner, toggle_inner = Bonsai.toggle ~default_model:false graph in let toggle_button text toggler = Vdom.Node.button ~attrs:[ Vdom.Attr.on_click (fun _ -> toggler) ] @@ -151,12 +155,14 @@ let stacking_example = ] ;; -let native_app = +let native_app (* I have a simple code example here of creating a modal, but then for later examples - I have a slightly non-idiomatic helper function to cut down on the boilerplate - of creating several different similar variations. *) + I have a slightly non-idiomatic helper function to cut down on the boilerplate + of creating several different similar variations. *) (* Creates the button to show/hide the modal and has state management. creator should - be fun on_cancel -> modal *) + be fun on_cancel -> modal *) + graph + = let create_modal_example' ?desc creator button_text ~show ~toggle () = let toggle_button text toggler = Vdom.Node.button @@ -180,13 +186,13 @@ let native_app = ; Vdom.Node.div [ toggle_button button_text toggle; extra_desc_markup ] ] in - let create_modal_example ?desc creator button_text () = - let%sub show, toggle = Bonsai.toggle ~default_model:false in + let create_modal_example ?desc creator button_text () graph = + let show, toggle = Bonsai.toggle ~default_model:false graph in let%arr show = show and toggle = toggle in create_modal_example' ?desc creator button_text ~show ~toggle () in - let%sub simple_modal = + let simple_modal = (* I have many examples so I factored out the logic that adds a button and manages show/hide with bonsai state, but you can just write that directly. *) @@ -200,9 +206,9 @@ let native_app = in Native_modal.view ~on_close:toggle contents in - create_modal_example creator "Simple modal" () + create_modal_example creator "Simple modal" () graph in - let%sub side_sheet_modal = + let side_sheet_modal = let creator ~toggle = let contents = dialog_contents @@ -212,11 +218,11 @@ let native_app = in Native_modal.view ~layout:`Right_side_sheet ~on_close:toggle contents in - create_modal_example creator "Side sheet" () + create_modal_example creator "Side sheet" () graph in - let%sub confirm_modal = + let confirm_modal = let%sub contents, confirm_prompt = - let%sub value, set_value = Bonsai.state None in + let value, set_value = Bonsai.state None graph in let%arr value = value and set_value = set_value in let module N = Vdom.Node in @@ -230,7 +236,7 @@ let native_app = ] , Option.map value ~f:(fun _ -> "Are you sure you want to close?") ) in - let%sub show, toggle = Bonsai.toggle ~default_model:false in + let show, toggle = Bonsai.toggle ~default_model:false graph in let%arr contents = contents and confirm_prompt = confirm_prompt and show = show @@ -251,7 +257,7 @@ let native_app = ~toggle () in - let%sub transparent_modal = + let transparent_modal = let creator ~toggle = let contents = dialog_contents @@ -265,7 +271,7 @@ let native_app = ~on_close:toggle contents in - create_modal_example creator "Transparent backdrop, no animation" () + create_modal_example creator "Transparent backdrop, no animation" () graph in let intro_text = Vdom.Node.div @@ -281,8 +287,8 @@ let native_app = \ Can customize overlay and frame styling if desired." ] in - let%sub add_lots_of_content_markup = add_lots_of_content_markup in - let%sub stacking_example = stacking_example in + let add_lots_of_content_markup = add_lots_of_content_markup graph in + let stacking_example = stacking_example graph in let%arr simple_modal = simple_modal and side_sheet_modal = side_sheet_modal and confirm_modal = confirm_modal @@ -304,9 +310,9 @@ let native_app = ])) ;; -let combined_app = - let%sub original_app = original_app in - let%sub native_app = native_app in +let combined_app graph = + let original_app = original_app graph in + let native_app = native_app graph in let%arr original_app = original_app and native_app = native_app in Vdom.Node.div diff --git a/examples/mouse_position/client/bin/main.ml b/examples/mouse_position/client/bin/main.ml index 6e85f921..31569b30 100644 --- a/examples/mouse_position/client/bin/main.ml +++ b/examples/mouse_position/client/bin/main.ml @@ -1,6 +1,6 @@ open! Core open! Async_kernel -open Bonsai_web +open Bonsai_web.Cont open Bonsai_examples_mouse_position_lib let run () = diff --git a/examples/mouse_position/client/src/app.ml b/examples/mouse_position/client/src/app.ml index 9ffb3163..9fe9ac95 100644 --- a/examples/mouse_position/client/src/app.ml +++ b/examples/mouse_position/client/src/app.ml @@ -1,5 +1,5 @@ open! Core -open Bonsai_web +open Bonsai_web.Cont open Bonsai_examples_mouse_position_common open Bonsai.Let_syntax open Username_kernel @@ -17,7 +17,7 @@ let session_to_color session = ] ;; -let app = +let app graph = let%sub { last_ok_response; _ } = Rpc_effect.Polling_state_rpc.poll ~sexp_of_query:[%sexp_of: Unit.t] @@ -27,19 +27,20 @@ let app = Protocol.Active_users.rpc ~where_to_connect:Self ~every:(Time_ns.Span.of_sec 1.0) - (Value.return ()) + (Bonsai.return ()) + graph in - let%sub active_users = + let active_users = match%arr last_ok_response with | Some (_, { active_users }) -> active_users | None -> Session.Map.empty in - let%sub rpc_results = + let rpc_results = Bonsai.assoc (module Session) active_users - ~f:(fun session username -> - let%sub result = + ~f:(fun session username graph -> + let result = Rpc_effect.Rpc.poll ~sexp_of_query:[%sexp_of: Session.t] ~sexp_of_response:[%sexp_of: Mouse_position.t option] @@ -49,24 +50,27 @@ let app = ~where_to_connect:Self ~every:(Time_ns.Span.of_sec 0.1) session + graph in let%arr result = result and username = username in result, username) + graph in - let%sub mouse_positions = + let mouse_positions = Bonsai.Map.filter_mapi rpc_results ~f:(fun ~key:_ ~data:({ last_ok_response; _ }, username) -> - match last_ok_response with - | Some (_, Some mouse_position) -> Some (mouse_position, username) - | None | Some (_, None) -> None) + match last_ok_response with + | Some (_, Some mouse_position) -> Some (mouse_position, username) + | None | Some (_, None) -> None) + graph in - let%sub cursor_blocks = + let cursor_blocks = Bonsai.assoc (module Session) mouse_positions - ~f:(fun session info -> + ~f:(fun session info _graph -> let%arr session = session and mouse_position, username = info in Node.div @@ -82,9 +86,10 @@ let app = ; Style.item ] [ Node.text (Username.to_string username) ]) + graph in - let%sub status_sidebar = - let%sub theme = View.Theme.current in + let status_sidebar = + let theme = View.Theme.current graph in let%arr rpc_results = rpc_results and theme = theme in let rows = Map.to_alist rpc_results in @@ -111,8 +116,8 @@ let app = in Vdom.Node.div ~attrs:[ Style.sidebar ] [ View.Table.render theme columns rows ] in - let%sub set_mouse_position = - Rpc_effect.Rpc.dispatcher Protocol.Set_mouse_position.rpc ~where_to_connect:Self + let set_mouse_position = + Rpc_effect.Rpc.dispatcher Protocol.Set_mouse_position.rpc ~where_to_connect:Self graph in let%arr cursor_blocks = cursor_blocks and status_sidebar = status_sidebar diff --git a/examples/mouse_position/client/src/app.mli b/examples/mouse_position/client/src/app.mli index 3ac5143c..2b7088b1 100644 --- a/examples/mouse_position/client/src/app.mli +++ b/examples/mouse_position/client/src/app.mli @@ -1,4 +1,4 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -val app : Vdom.Node.t Computation.t +val app : Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/mouse_position/client/test/app_test.ml b/examples/mouse_position/client/test/app_test.ml index 592fc5fe..b114306a 100644 --- a/examples/mouse_position/client/test/app_test.ml +++ b/examples/mouse_position/client/test/app_test.ml @@ -1,6 +1,6 @@ open! Core open! Bonsai_web_test -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai_examples_mouse_position_lib let%expect_test "basic page appearance" = diff --git a/examples/mouse_position/common/bonsai_examples_mouse_position_common.ml b/examples/mouse_position/common/bonsai_examples_mouse_position_common.ml index 05dac20a..c4e03aeb 100644 --- a/examples/mouse_position/common/bonsai_examples_mouse_position_common.ml +++ b/examples/mouse_position/common/bonsai_examples_mouse_position_common.ml @@ -13,7 +13,7 @@ end module Session = Unique_id.Int () module Active_users = struct - type t = { active_users : (Username.t[@atomic]) Session.Map.t [@diff.map] } + type t = { active_users : (Username.t[@atomic]) Session.Map.t } [@@deriving diff, bin_io, equal, sexp] end diff --git a/examples/mouse_position/doc/graph_generator.ml b/examples/mouse_position/doc/graph_generator.ml index d6af8f68..3558471b 100644 --- a/examples/mouse_position/doc/graph_generator.ml +++ b/examples/mouse_position/doc/graph_generator.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai_examples_mouse_position_lib let () = print_endline (Bonsai.Debug.to_dot App.app) diff --git a/examples/multi_select/main.ml b/examples/multi_select/main.ml index 421d1637..74625ef5 100644 --- a/examples/multi_select/main.ml +++ b/examples/multi_select/main.ml @@ -1,5 +1,5 @@ open Core -open Bonsai_web +open Bonsai_web.Cont open Bonsai.Let_syntax module Attribute = struct @@ -33,18 +33,19 @@ let subwidgets = in attr, { Widget.default_selection_status = Selected; all_items }) |> Attribute.Map.of_alist_exn - |> Value.return + |> Bonsai.return ;; -let id_prefix = Value.return "multi-select-widget-example" +let id_prefix = Bonsai.return "multi-select-widget-example" -let bonsai = - let%sub widget_result = +let bonsai graph = + let widget_result = Widget.bonsai ~allow_updates_when_focused:`Never ~all_keys:(Attribute.Set.of_list Attribute.all) ~id_prefix subwidgets + graph in let%arr widget_result = widget_result in let open Virtual_dom.Vdom in diff --git a/examples/node_with_map_children/attr.ml b/examples/node_with_map_children/attr.ml index dc36a663..22531690 100644 --- a/examples/node_with_map_children/attr.ml +++ b/examples/node_with_map_children/attr.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax type t = @@ -7,10 +7,10 @@ type t = ; reset : unit Effect.t } -let component = - let%sub attr, inject = +let component graph = + let attr, inject = Bonsai.state_machine0 - () + graph ~sexp_of_model:[%sexp_of: opaque] ~equal:phys_equal ~sexp_of_action:[%sexp_of: Unit.t] diff --git a/examples/node_with_map_children/attr.mli b/examples/node_with_map_children/attr.mli index 0b076464..e069cb2a 100644 --- a/examples/node_with_map_children/attr.mli +++ b/examples/node_with_map_children/attr.mli @@ -1,9 +1,9 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont type t = { attr : Vdom.Attr.t ; reset : unit Effect.t } -val component : t Computation.t +val component : Bonsai.graph -> t Bonsai.t diff --git a/examples/node_with_map_children/automator.ml b/examples/node_with_map_children/automator.ml index 0d64795a..08289f84 100644 --- a/examples/node_with_map_children/automator.ml +++ b/examples/node_with_map_children/automator.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax open Js_of_ocaml @@ -11,8 +11,8 @@ let validate : unit -> bool = let validate = Effect.of_sync_fun validate () -let driver ~reset_all ~step ~is_done ~set_has_error = - let%sub get_is_done = Bonsai.yoink is_done in +let driver ~reset_all ~step ~is_done ~set_has_error graph = + let get_is_done = Bonsai.peek is_done graph in Bonsai.Edge.after_display (let%map get_is_done = get_is_done and reset_all = reset_all @@ -29,16 +29,20 @@ let driver ~reset_all ~step ~is_done ~set_has_error = | Inactive -> Effect.never in if is_done then reset_all else Effect.Ignore)) + graph; + Bonsai.return () ;; -let component ~is_running ~reset_all ~step ~is_done = - let%sub has_error, set_has_error = - Bonsai.state false ~sexp_of_model:[%sexp_of: Bool.t] ~equal:[%equal: Bool.t] +let component ~is_running ~reset_all ~step ~is_done graph = + let has_error, set_has_error = + Bonsai.state false ~sexp_of_model:[%sexp_of: Bool.t] ~equal:[%equal: Bool.t] graph in - let%sub active = + let active = let%arr is_running = is_running and has_error = has_error in is_running && not has_error in - if%sub active then driver ~set_has_error ~reset_all ~step ~is_done else Bonsai.const () + if%sub active + then driver ~set_has_error ~reset_all ~step ~is_done graph + else Bonsai.return () ;; diff --git a/examples/node_with_map_children/automator.mli b/examples/node_with_map_children/automator.mli index e567dfc4..ebb75a85 100644 --- a/examples/node_with_map_children/automator.mli +++ b/examples/node_with_map_children/automator.mli @@ -1,13 +1,14 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont (** Given values and effects for manipulating the other components in the app, [Automator.component] will run through testing scenarios, pausing after every frame to see if the results can be validated. *) val component - : is_running:bool Value.t - -> reset_all:unit Effect.t Value.t - -> step:unit Effect.t Value.t - -> is_done:bool Value.t - -> unit Computation.t + : is_running:bool Bonsai.t + -> reset_all:unit Effect.t Bonsai.t + -> step:unit Effect.t Bonsai.t + -> is_done:bool Bonsai.t + -> Bonsai.graph + -> unit Bonsai.t diff --git a/examples/node_with_map_children/color_list.ml b/examples/node_with_map_children/color_list.ml index 7bd13fc3..9a603991 100644 --- a/examples/node_with_map_children/color_list.ml +++ b/examples/node_with_map_children/color_list.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax type result = @@ -42,10 +42,10 @@ module Action = struct [@@deriving sexp_of] end -let component name = - let%sub state, inject = +let component name graph = + let state, inject = Bonsai.state_machine0 - () + graph ~sexp_of_model:[%sexp_of: Model.t] ~equal:[%equal: Model.t] ~sexp_of_action:[%sexp_of: Action.t] @@ -55,12 +55,12 @@ let component name = | Regenerate -> generate_random () | Remove i -> Map.remove model i) in - let%sub () = + let () = Bonsai.Edge.lifecycle ~on_activate: (let%map inject = inject in inject Regenerate) - () + graph in let%arr state = state and inject = inject in diff --git a/examples/node_with_map_children/color_list.mli b/examples/node_with_map_children/color_list.mli index 2afd7a4f..910b9b0d 100644 --- a/examples/node_with_map_children/color_list.mli +++ b/examples/node_with_map_children/color_list.mli @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont type t = float Int.Map.t [@@deriving sexp, equal] @@ -12,4 +12,4 @@ type result = (** The color-list component is used for both the "before" and "after" columns in the UI. It generates random int => float maps, and exposes a UI for manipulating them, as well as returning the currently-selected map. *) -val component : string -> result Computation.t +val component : string -> Bonsai.graph -> result Bonsai.t diff --git a/examples/node_with_map_children/comparison.ml b/examples/node_with_map_children/comparison.ml index 71a6919d..bcc7cbf5 100644 --- a/examples/node_with_map_children/comparison.ml +++ b/examples/node_with_map_children/comparison.ml @@ -1,25 +1,26 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax -let to_vdom_node_map ~with_key map = +let to_vdom_node_map ~with_key map graph = Bonsai.assoc (module Int) map - ~f:(fun key data -> + ~f:(fun key data _graph -> let%arr key = key and data = data in let key = Int.to_string key in let text = Vdom.Node.textf "%s" key in let key = if with_key then Some key else None in Style.chip ?key data text) + graph ;; let make_comparison_list node = Vdom.Node.div ~attrs:[ Style.comparison_list ] [ node ] -let view ~tag ~attr nodes = - let%sub nodes_with_key = to_vdom_node_map ~with_key:true nodes in - let%sub nodes_without_key = to_vdom_node_map ~with_key:false nodes in +let view ~tag ~attr nodes graph = + let nodes_with_key = to_vdom_node_map ~with_key:true nodes graph in + let nodes_without_key = to_vdom_node_map ~with_key:false nodes graph in let%arr nodes_with_key = nodes_with_key and nodes_without_key = nodes_without_key and tag = tag diff --git a/examples/node_with_map_children/comparison.mli b/examples/node_with_map_children/comparison.mli index b0411e32..3eca0163 100644 --- a/examples/node_with_map_children/comparison.mli +++ b/examples/node_with_map_children/comparison.mli @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont (** Given a color-list map, this component will display the given rows using four approaches: @@ -9,7 +9,8 @@ open! Bonsai_web 3. Traditional (but with elements that have ~key properties on them) 4. Node_with_map_children (but with elements that have ~key properties on them) *) val view - : tag:string Value.t - -> attr:Vdom.Attr.t Value.t - -> Color_list.t Value.t - -> Vdom.Node.t Computation.t + : tag:string Bonsai.t + -> attr:Vdom.Attr.t Bonsai.t + -> Color_list.t Bonsai.t + -> Bonsai.graph + -> Vdom.Node.t Bonsai.t diff --git a/examples/node_with_map_children/main.ml b/examples/node_with_map_children/main.ml index 60345f51..709063d1 100644 --- a/examples/node_with_map_children/main.ml +++ b/examples/node_with_map_children/main.ml @@ -1,28 +1,28 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax -let component = - let%sub { tag; reset = reset_tag } = Tag.component in - let%sub { attr; reset = reset_attr } = Attr.component in +let component graph = + let%sub { tag; reset = reset_tag } = Tag.component graph in + let%sub { attr; reset = reset_attr } = Attr.component graph in let%sub { out = before_state; view = before_view; reset = reset_before } = - Color_list.component "before" + Color_list.component "before" graph in let%sub { out = after_state; view = after_view; reset = reset_after } = - Color_list.component "after" + Color_list.component "after" graph in let%sub { state; view = tweener; is_automating = is_running; is_done; step } = - Stepper.component ~before_state ~after_state + Stepper.component ~before_state ~after_state graph in - let%sub reset_all = + let reset_all = let%arr reset_before = reset_before and reset_after = reset_after and reset_tag = reset_tag and reset_attr = reset_attr in Effect.Many [ reset_before; reset_after; reset_tag; reset_attr ] in - let%sub () = Automator.component ~is_running ~step ~is_done ~reset_all in - let%sub comparison = Comparison.view ~tag ~attr state in + let%sub () = Automator.component ~is_running ~step ~is_done ~reset_all graph in + let comparison = Comparison.view ~tag ~attr state graph in let%arr before_view = before_view and after_view = after_view and tweener = tweener diff --git a/examples/node_with_map_children/stepper.ml b/examples/node_with_map_children/stepper.ml index 12044ec0..6570dbc5 100644 --- a/examples/node_with_map_children/stepper.ml +++ b/examples/node_with_map_children/stepper.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax type t = @@ -61,14 +61,17 @@ let generate_diffs ~before ~after = |> List.group ~break:(fun _ _ -> Random.bool ()) ;; -let component ~(before_state : Color_list.t Value.t) ~(after_state : Color_list.t Value.t) +let component + ~(before_state : Color_list.t Bonsai.t) + ~(after_state : Color_list.t Bonsai.t) + graph = - let%sub input = + let input = let%arr before = before_state and after = after_state in { Input.before; after } in - let%sub state, inject = + let state, inject = Bonsai.state_machine1 ~sexp_of_model:[%sexp_of: Model.t] ~equal:[%equal: Model.t] @@ -76,32 +79,33 @@ let component ~(before_state : Color_list.t Value.t) ~(after_state : Color_list. input ~default_model:{ cur = Int.Map.empty; diffs = []; pointer = 0 } ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) input model action -> - match input with - | Active { Input.before; after } -> - (match action with - | Set_state model -> model - | Restart -> - let diffs = generate_diffs ~before ~after in - { cur = before; diffs; pointer = 0 } - | Step -> - let packet = - match List.nth model.diffs model.pointer with - | Some packet -> packet - | None -> [] - in - let cur = List.fold packet ~init:model.cur ~f:Modification.apply in - { model with cur; pointer = model.pointer + 1 }) - | Inactive -> - eprint_s - [%message - [%here] - "An action sent to a [state_machine1] has been dropped because its input \ - was not present. This happens when the [state_machine1] is inactive when \ - it receives a message." - (action : Action.t)]; - model) + match input with + | Active { Input.before; after } -> + (match action with + | Set_state model -> model + | Restart -> + let diffs = generate_diffs ~before ~after in + { cur = before; diffs; pointer = 0 } + | Step -> + let packet = + match List.nth model.diffs model.pointer with + | Some packet -> packet + | None -> [] + in + let cur = List.fold packet ~init:model.cur ~f:Modification.apply in + { model with cur; pointer = model.pointer + 1 }) + | Inactive -> + eprint_s + [%message + [%here] + "An action sent to a [state_machine1] has been dropped because its input \ + was not present. This happens when the [state_machine1] is inactive \ + when it receives a message." + (action : Action.t)]; + model) + graph in - let%sub () = + let () = Bonsai.Edge.on_change ~sexp_of_model:[%sexp_of: Input.t] ~equal:[%equal: Input.t] @@ -109,10 +113,13 @@ let component ~(before_state : Color_list.t Value.t) ~(after_state : Color_list. ~callback: (let%map inject = inject in fun _ -> inject Restart) + graph in - let%sub help_toggler = Bonsai.toggle ~default_model:false in - let%sub is_automating, toggle_automating = Bonsai.toggle ~default_model:false in - let%sub ff_button = + let help_toggler = + Tuple2.uncurry Bonsai.both @@ Bonsai.toggle ~default_model:false graph + in + let is_automating, toggle_automating = Bonsai.toggle ~default_model:false graph in + let ff_button = let%arr toggle_automating = toggle_automating in Style.fast_forward `Dark ~on_click:toggle_automating in diff --git a/examples/node_with_map_children/stepper.mli b/examples/node_with_map_children/stepper.mli index 79596472..72fb2d15 100644 --- a/examples/node_with_map_children/stepper.mli +++ b/examples/node_with_map_children/stepper.mli @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont type t = { state : Color_list.t @@ -13,6 +13,7 @@ type t = "after" maps that it should step through. It returns the view, the current state of the map, and a "step" effect along with its status for the automator component. *) val component - : before_state:Color_list.t Value.t - -> after_state:Color_list.t Value.t - -> t Computation.t + : before_state:Color_list.t Bonsai.t + -> after_state:Color_list.t Bonsai.t + -> Bonsai.graph + -> t Bonsai.t diff --git a/examples/node_with_map_children/style.ml b/examples/node_with_map_children/style.ml index 682307fd..ced0b2a6 100644 --- a/examples/node_with_map_children/style.ml +++ b/examples/node_with_map_children/style.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont include [%css diff --git a/examples/node_with_map_children/tag.ml b/examples/node_with_map_children/tag.ml index 374a1037..66c70d6f 100644 --- a/examples/node_with_map_children/tag.ml +++ b/examples/node_with_map_children/tag.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax type t = @@ -7,10 +7,10 @@ type t = ; reset : unit Effect.t } -let component = - let%sub tag, inject = +let component graph = + let tag, inject = Bonsai.state_machine0 - () + graph ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] ~sexp_of_action:[%sexp_of: Unit.t] diff --git a/examples/node_with_map_children/tag.mli b/examples/node_with_map_children/tag.mli index 0d3c0634..0188611d 100644 --- a/examples/node_with_map_children/tag.mli +++ b/examples/node_with_map_children/tag.mli @@ -1,9 +1,9 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont type t = { tag : string ; reset : unit Effect.t } -val component : t Computation.t +val component : Bonsai.graph -> t Bonsai.t diff --git a/examples/not_connected_warning_box/main.ml b/examples/not_connected_warning_box/main.ml index 040cdc62..8cd6cb7a 100644 --- a/examples/not_connected_warning_box/main.ml +++ b/examples/not_connected_warning_box/main.ml @@ -1,14 +1,14 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax -let component = - let%sub is_connected, set_is_connected = - Bonsai.state false ~sexp_of_model:[%sexp_of: Bool.t] ~equal:[%equal: Bool.t] +let component graph = + let is_connected, set_is_connected = + Bonsai.state false ~sexp_of_model:[%sexp_of: Bool.t] ~equal:[%equal: Bool.t] graph in - let%sub not_connected_warning_box = + let not_connected_warning_box = Bonsai_web_ui_not_connected_warning_box.( - component ~create_message:message_for_async_durable is_connected) + component ~create_message:message_for_async_durable is_connected graph) in let%arr not_connected_warning_box = not_connected_warning_box and is_connected = is_connected diff --git a/examples/notifications/main.ml b/examples/notifications/main.ml index f5659df4..8f727330 100644 --- a/examples/notifications/main.ml +++ b/examples/notifications/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Gallery = Bonsai_web_ui_gallery module Notifications = Bonsai_web_ui_notifications @@ -20,8 +20,8 @@ module User_defined_notification = struct | Error of string [@@deriving sexp, equal] - let render ~close t = - let%sub theme = View.Theme.current in + let render ~close t graph = + let theme = View.Theme.current graph in let%arr close = close and t = t and theme = theme in @@ -33,19 +33,22 @@ module User_defined_notification = struct ;; end - let component = - let%sub notifications = - Notifications.component (module Notification) ~equal:[%equal: Notification.t] + let component graph = + let notifications = + Notifications.component + (module Notification) + ~equal:[%equal: Notification.t] + graph in - let%sub vdom = Notifications.render notifications ~f:Notification.render in + let vdom = Notifications.render notifications ~f:Notification.render graph in let%arr vdom = vdom and notifications = notifications in vdom, notifications ;;] - let view = - let%sub theme = View.Theme.current in - let%sub component, notifications = component in + let view graph = + let theme = View.Theme.current graph in + let%sub component, notifications = component graph in let%arr component = component and notifications = notifications and theme = theme in @@ -94,8 +97,8 @@ module User_defined_notification = struct let filter_attrs = None end -let component = - let%sub theme, theme_picker = Gallery.Theme_picker.component () in +let component graph = + let%sub theme, theme_picker = Gallery.Theme_picker.component () graph in View.Theme.set_for_app theme (Gallery.make_sections @@ -108,6 +111,7 @@ let component = notifications appear in front of your content. |} , [ Gallery.make_demo (module User_defined_notification) ] ) ]) + graph ;; let () = Bonsai_web.Start.start component diff --git a/examples/notifications_test/main.ml b/examples/notifications_test/main.ml index d52752fb..166d52d8 100644 --- a/examples/notifications_test/main.ml +++ b/examples/notifications_test/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Gallery = Bonsai_web_ui_gallery @@ -15,17 +15,17 @@ there are no changes to the previous API.|} [%demo module Notifications = Bonsai_web_ui_notifications - let component = - let%sub notifications = Notifications.Basic.create () in - let%sub vdom = Notifications.Basic.render notifications in + let component graph = + let notifications = Notifications.Basic.create () graph in + let vdom = Notifications.Basic.render notifications graph in let%arr vdom = vdom and notifications = notifications in vdom, notifications ;;] - let view = - let%sub theme = View.Theme.current in - let%sub component, notifications = component in + let view graph = + let theme = View.Theme.current graph in + let%sub component, notifications = component graph in let%arr component = component and notifications = notifications and theme = theme in @@ -57,8 +57,8 @@ there are no changes to the previous API.|} let filter_attrs = None end -let component = - let%sub theme, theme_picker = Gallery.Theme_picker.component () in +let component graph = + let%sub theme, theme_picker = Gallery.Theme_picker.component () graph in View.Theme.set_for_app theme (Gallery.make_sections @@ -68,6 +68,7 @@ let component = are backwards compatible with the old API without showing the old API in the homepage.|} , [ Gallery.make_demo (module Basic_notification) ] ) ]) + graph ;; let () = Start.start ~bind_to_element_with_id:"app" component diff --git a/examples/oklab/knobs.ml b/examples/oklab/knobs.ml index f07a083e..922ef8e6 100644 --- a/examples/oklab/knobs.ml +++ b/examples/oklab/knobs.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Form = Bonsai_web_ui_form.With_automatic_view @@ -31,9 +31,11 @@ module Shared = struct let label_for_field = `Inferred - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = function - | Left -> Form.Elements.Color_picker.hex () - | Right -> Form.Elements.Color_picker.hex () + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with + | Left -> Form.Elements.Color_picker.hex () graph + | Right -> Form.Elements.Color_picker.hex () graph ;; end) ;; @@ -49,7 +51,9 @@ module For_gradient = struct let label_for_field = `Inferred - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = function + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with | Steps -> Form.Elements.Range.int ~min:1 @@ -58,6 +62,7 @@ module For_gradient = struct ~step:1 ~allow_updates_when_focused:`Never () + graph ;; end) ;; @@ -82,7 +87,9 @@ module For_overlay = struct let label_for_field = `Computed label_for_field - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = function + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with | Left_alpha -> Form.Elements.Range.float ~min:0.0 @@ -91,6 +98,7 @@ module For_overlay = struct ~step:0.01 ~allow_updates_when_focused:`Never () + graph | Right_alpha -> Form.Elements.Range.float ~min:0.0 @@ -99,6 +107,7 @@ module For_overlay = struct ~step:0.01 ~allow_updates_when_focused:`Never () + graph ;; end) ;; @@ -118,26 +127,29 @@ let initial_params = } ;; -let form = - let%sub shared = Shared.form in - let%sub for_gradient = For_gradient.form in - let%sub for_overlay = For_overlay.form in - let%sub all = +let form graph = + let shared = Shared.form graph in + let for_gradient = For_gradient.form graph in + let for_overlay = For_overlay.form graph in + let all = Form.Typed.Record.make (module struct module Typed_field = Typed_field let label_for_field = `Inferred - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = function - | Shared -> return shared - | For_gradient -> return for_gradient - | For_overlay -> return for_overlay + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field _graph -> + match typed_field with + | Shared -> shared + | For_gradient -> for_gradient + | For_overlay -> for_overlay ;; end) + graph in - let%sub all = Form.Dynamic.with_default (Value.return initial_params) all in - let%sub value = + let all = Form.Dynamic.with_default (Bonsai.return initial_params) all graph in + let value = let%arr all = all in (match Form.value all with | Error e -> print_s [%message (e : Error.t)] @@ -152,8 +164,8 @@ let form = ~title:[ Vdom.Node.text title ] [ Form.view_as_vdom form ] in - let%sub view = - let%sub theme = View.Theme.current in + let view = + let theme = View.Theme.current graph in let%arr shared = shared and for_gradient = for_gradient and for_overlay = for_overlay @@ -166,5 +178,5 @@ let form = ; card_helper theme "for overlay" for_overlay ] in - return (Value.both value view) + Bonsai.both value view ;; diff --git a/examples/oklab/knobs.mli b/examples/oklab/knobs.mli index 501e3dcb..896c56f4 100644 --- a/examples/oklab/knobs.mli +++ b/examples/oklab/knobs.mli @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont module Shared : sig type t = @@ -25,4 +25,4 @@ type t = ; for_overlay : For_overlay.t } -val form : (t * Vdom.Node.t) Computation.t +val form : Bonsai.graph -> (t * Vdom.Node.t) Bonsai.t diff --git a/examples/oklab/main.ml b/examples/oklab/main.ml index d4d6d80f..62c165bf 100644 --- a/examples/oklab/main.ml +++ b/examples/oklab/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Knobs = Knobs @@ -61,8 +61,8 @@ let overlay ~left ~right = ] ;; -let component = - let%sub form = Knobs.form in +let component graph = + let form = Knobs.form graph in let%arr ( { shared = { left = `Hex left; right = `Hex right } ; for_gradient = { steps } ; for_overlay = { left_alpha; right_alpha } @@ -89,5 +89,5 @@ let component = let () = Bonsai_web.Start.start - (View.Theme.set_for_app (Value.return (Kado.theme ~version:Bleeding ())) component) + (View.Theme.set_for_app (Bonsai.return (Kado.theme ~version:Bleeding ())) component) ;; diff --git a/examples/on_display/main.ml b/examples/on_display/main.ml index 837786e3..185f72b7 100644 --- a/examples/on_display/main.ml +++ b/examples/on_display/main.ml @@ -1,17 +1,17 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax -let component = - let%sub state, set_state = - Bonsai.state 0 ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] +let component graph = + let state, set_state = + Bonsai.state 0 ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] graph in - let%sub increment = + let increment = let%arr state = state and set_state = set_state in set_state (state + 1) in - let%sub () = Bonsai.Edge.after_display increment in + let () = Bonsai.Edge.after_display increment graph in let%arr state = state in Vdom.Node.textf "Number of frames rendered: %d" state ;; diff --git a/examples/open_source/rpc_chat/client/app.ml b/examples/open_source/rpc_chat/client/app.ml index 2405b9da..d16fda41 100644 --- a/examples/open_source/rpc_chat/client/app.ml +++ b/examples/open_source/rpc_chat/client/app.ml @@ -1,21 +1,30 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai_chat_open_source_common -let component ~room_list ~current_room ~messages ~refresh_rooms ~change_room ~send_message +let component + ~room_list + ~current_room + ~messages + ~refresh_rooms + ~change_room + ~send_message + graph = let open Bonsai.Let_syntax in - let%sub send_message = + let send_message = match%arr current_room with | Some room -> fun contents -> send_message ~room ~contents | None -> Fn.const Effect.never in - let%sub current_room = - return (current_room >>| Option.value ~default:(Room.of_string "no room selected")) + let current_room = + current_room >>| Option.value ~default:(Room.of_string "no room selected") in - let%sub rooms_list = Room_list_panel.component ~room_list ~refresh_rooms ~change_room in - let%sub compose_panel = Compose_message.component ~send_message in - let%sub messages_panel = Messages_panel.component ~messages ~current_room in + let rooms_list = + Room_list_panel.component ~room_list ~refresh_rooms ~change_room graph + in + let compose_panel = Compose_message.component ~send_message graph in + let messages_panel = Messages_panel.component ~messages ~current_room graph in let%arr rooms_list = rooms_list and compose_panel = compose_panel and messages_panel = messages_panel in diff --git a/examples/open_source/rpc_chat/client/app.mli b/examples/open_source/rpc_chat/client/app.mli index e954bca4..a45f4801 100644 --- a/examples/open_source/rpc_chat/client/app.mli +++ b/examples/open_source/rpc_chat/client/app.mli @@ -1,12 +1,13 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai_chat_open_source_common val component - : room_list:Room.t list Value.t - -> current_room:Room.t option Value.t - -> messages:Message.t list Value.t + : room_list:Room.t list Bonsai.t + -> current_room:Room.t option Bonsai.t + -> messages:Message.t list Bonsai.t -> refresh_rooms:unit Effect.t -> change_room:(Room.t -> unit Effect.t) -> send_message:(room:Room.t -> contents:string -> unit Effect.t) - -> Vdom.Node.t Computation.t + -> Bonsai.graph + -> Vdom.Node.t Bonsai.t diff --git a/examples/open_source/rpc_chat/client/compose_message.ml b/examples/open_source/rpc_chat/client/compose_message.ml index 5fc219eb..ab20db98 100644 --- a/examples/open_source/rpc_chat/client/compose_message.ml +++ b/examples/open_source/rpc_chat/client/compose_message.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont let build_result send_message (textbox_content, set_textbox_content) = let submit_and_then_clear = @@ -33,16 +33,20 @@ let build_result send_message (textbox_content, set_textbox_content) = Vdom.Node.div ~attrs:[ Vdom.Attr.id "compose" ] [ text_input; submit_button ] ;; -let component ~send_message = +let component ~send_message graph = let open Bonsai.Let_syntax in - let%sub textbox_state = - Bonsai.state_machine0 - () - ~sexp_of_model:[%sexp_of: String.t] - ~equal:[%equal: String.t] - ~sexp_of_action:[%sexp_of: String.t] - ~default_model:"" - ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) _ new_state -> new_state) + let textbox_state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state_machine0 + graph + ~sexp_of_model:[%sexp_of: String.t] + ~equal:[%equal: String.t] + ~sexp_of_action:[%sexp_of: String.t] + ~default_model:"" + ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) _ new_state -> + new_state) in - return (build_result <$> send_message <*> textbox_state) + let%arr send_message = send_message + and textbox_state = textbox_state in + build_result send_message textbox_state ;; diff --git a/examples/open_source/rpc_chat/client/compose_message.mli b/examples/open_source/rpc_chat/client/compose_message.mli index 21243467..61986577 100644 --- a/examples/open_source/rpc_chat/client/compose_message.mli +++ b/examples/open_source/rpc_chat/client/compose_message.mli @@ -1,6 +1,7 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont val component - : send_message:(string -> unit Effect.t) Value.t - -> Vdom.Node.t Computation.t + : send_message:(string -> unit Effect.t) Bonsai.t + -> Bonsai.graph + -> Vdom.Node.t Bonsai.t diff --git a/examples/open_source/rpc_chat/client/main.ml b/examples/open_source/rpc_chat/client/main.ml index 9a3edd23..7149a2a5 100644 --- a/examples/open_source/rpc_chat/client/main.ml +++ b/examples/open_source/rpc_chat/client/main.ml @@ -1,13 +1,13 @@ open! Core open! Async_kernel -open! Bonsai_web +open! Bonsai_web.Cont open Async_js open Bonsai_chat_open_source_common open Composition_infix let run_refresh_rooms ~conn ~rooms_list_var = let%map rooms = Rpc.Rpc.dispatch_exn Protocol.List_rooms.t conn () in - Bonsai.Var.set rooms_list_var rooms + Bonsai.Expert.Var.set rooms_list_var rooms ;; let refresh_rooms ~conn ~rooms_list_var = @@ -28,7 +28,7 @@ end let process_message_stream ~conn ~room_state_var = let%bind pipe, _ = Rpc.Pipe_rpc.dispatch_exn Protocol.Message_stream.t conn () in Pipe.iter pipe ~f:(fun message -> - Bonsai.Var.update + Bonsai.Expert.Var.update room_state_var ~f:(fun ({ Room_state.messages; current_room } as prev) -> if [%equal: Room.t option] current_room (Some message.room) @@ -62,7 +62,7 @@ let send_message ~conn = let change_room ~conn ~room_state_var = let on_room_switch room = let%map messages = Rpc.Rpc.dispatch_exn Protocol.Messages_request.t conn room in - Bonsai.Var.set room_state_var { Room_state.messages; current_room = Some room } + Bonsai.Expert.Var.set room_state_var { Room_state.messages; current_room = Some room } in let dispatch = on_room_switch |> Effect.of_deferred_fun in fun room -> dispatch room @@ -71,20 +71,23 @@ let change_room ~conn ~room_state_var = let run () = Async_js.init (); let%bind conn = Rpc.Connection.client_exn () in - let rooms_list_var = Bonsai.Var.create [] in + let rooms_list_var = Bonsai.Expert.Var.create [] in let room_state_var = - Bonsai.Var.create { Room_state.messages = []; current_room = None } + Bonsai.Expert.Var.create { Room_state.messages = []; current_room = None } in let change_room = change_room ~conn ~room_state_var in let refresh_rooms = refresh_rooms ~conn ~rooms_list_var in let send_message = send_message ~conn in let () = Bonsai_web.Start.start - (let open Bonsai.Let_syntax in - App.component - ~room_list:(Bonsai.Var.value rooms_list_var) - ~current_room:(Room_state.current_room <$> Bonsai.Var.value room_state_var) - ~messages:(Room_state.messages <$> Bonsai.Var.value room_state_var) + (App.component + ~room_list:(Bonsai.Expert.Var.value rooms_list_var) + ~current_room: + (Bonsai.map + ~f:Room_state.current_room + (Bonsai.Expert.Var.value room_state_var)) + ~messages: + (Bonsai.map ~f:Room_state.messages (Bonsai.Expert.Var.value room_state_var)) ~refresh_rooms ~change_room ~send_message) diff --git a/examples/open_source/rpc_chat/client/messages_panel.ml b/examples/open_source/rpc_chat/client/messages_panel.ml index 54ccfc3b..a1ddf0ff 100644 --- a/examples/open_source/rpc_chat/client/messages_panel.ml +++ b/examples/open_source/rpc_chat/client/messages_panel.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai_chat_open_source_common open Bonsai.Let_syntax @@ -15,7 +15,7 @@ let view messages current_room = ] ;; -let component ~messages ~current_room = +let component ~messages ~current_room _graph = let%arr messages = messages and current_room = current_room in view messages current_room diff --git a/examples/open_source/rpc_chat/client/messages_panel.mli b/examples/open_source/rpc_chat/client/messages_panel.mli index 1f660ae1..4a5b348d 100644 --- a/examples/open_source/rpc_chat/client/messages_panel.mli +++ b/examples/open_source/rpc_chat/client/messages_panel.mli @@ -1,8 +1,9 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai_chat_open_source_common val component - : messages:Message.t list Value.t - -> current_room:Room.t Value.t - -> Vdom.Node.t Computation.t + : messages:Message.t list Bonsai.t + -> current_room:Room.t Bonsai.t + -> Bonsai.graph + -> Vdom.Node.t Bonsai.t diff --git a/examples/open_source/rpc_chat/client/room_list_panel.ml b/examples/open_source/rpc_chat/client/room_list_panel.ml index 4f7464c2..df60ff98 100644 --- a/examples/open_source/rpc_chat/client/room_list_panel.ml +++ b/examples/open_source/rpc_chat/client/room_list_panel.ml @@ -1,9 +1,9 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai_chat_open_source_common open Bonsai.Let_syntax -let component ~room_list ~refresh_rooms ~change_room = +let component ~room_list ~refresh_rooms ~change_room _graph = let%arr room_list = room_list in let room_header = Vdom.Node.h2 diff --git a/examples/open_source/rpc_chat/client/room_list_panel.mli b/examples/open_source/rpc_chat/client/room_list_panel.mli index 91a5e115..250f6724 100644 --- a/examples/open_source/rpc_chat/client/room_list_panel.mli +++ b/examples/open_source/rpc_chat/client/room_list_panel.mli @@ -1,9 +1,10 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai_chat_open_source_common val component - : room_list:Room.t list Value.t + : room_list:Room.t list Bonsai.t -> refresh_rooms:unit Effect.t -> change_room:(Room.t -> unit Effect.t) - -> Vdom.Node.t Computation.t + -> Bonsai.graph + -> Vdom.Node.t Bonsai.t diff --git a/examples/panels/main.ml b/examples/panels/main.ml index efc09d6a..b1e0c9b8 100644 --- a/examples/panels/main.ml +++ b/examples/panels/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Vdom open Bonsai.Let_syntax module Id = Int @@ -30,15 +30,16 @@ module Ids = struct { next = state.next + 1; ids = Map.add_exn ~key:state.next ~data:() state.ids } ;; - let component = - let%sub state = - Bonsai.state_machine0 - () - ~sexp_of_model:[%sexp_of: State.t] - ~equal:[%equal: State.t] - ~sexp_of_action:[%sexp_of: [ `Remove of Id.t | `Add_with_next_id ]] - ~default_model:State.default - ~apply_action + let component graph = + let state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state_machine0 + graph + ~sexp_of_model:[%sexp_of: State.t] + ~equal:[%equal: State.t] + ~sexp_of_action:[%sexp_of: [ `Remove of Id.t | `Add_with_next_id ]] + ~default_model:State.default + ~apply_action in let%arr state, inject = state in Result. @@ -49,19 +50,20 @@ module Ids = struct ;; end -let panel_component id (_ : unit Value.t) = +let panel_component id (_ : unit Bonsai.t) _graph = let%arr id = id in Node.div [ Node.textf !"Hello, world %{Id}!" id ] ;; -let component = - let%sub { ids; inject_add_with_next_id; inject_remove } = Ids.component in - let%sub panels = Bonsai.assoc (module Id) ids ~f:panel_component in +let component graph = + let%sub { ids; inject_add_with_next_id; inject_remove } = Ids.component graph in + let panels = Bonsai.assoc (module Id) ids ~f:panel_component graph in Bonsai_web_ui_panels_experimental.component ~key:(module Id) ~inject_add:inject_add_with_next_id ~inject_remove panels + graph ;; let () = Bonsai_web.Start.start component diff --git a/examples/partial_render_table/bin/main.ml b/examples/partial_render_table/bin/main.ml index 3a9f1eb0..0878c311 100644 --- a/examples/partial_render_table/bin/main.ml +++ b/examples/partial_render_table/bin/main.ml @@ -1,9 +1,9 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module PRT_example = Bonsai_partial_render_table_example -let component ~theme_picker = +let component ~theme_picker graph = let%sub ( form_view , { themed ; show_position @@ -13,9 +13,9 @@ let component ~theme_picker = ; multisort_columns_when } ) = - PRT_example.Layout_form.component + PRT_example.Layout_form.component graph in - let%sub data = + let data = let%arr num_rows = num_rows in PRT_example.Row.many_random num_rows in @@ -38,8 +38,8 @@ let component ~theme_picker = data in (match%sub themed with - | false -> base ~theming:`Legacy_don't_use_theme - | true -> base ~theming:`Themed) + | false -> base ~theming:`Legacy_don't_use_theme graph + | true -> base ~theming:`Themed graph) | true -> let base = PRT_example.component @@ -50,25 +50,25 @@ let component ~theme_picker = data in (match%sub themed with - | false -> base ~theming:`Legacy_don't_use_theme - | true -> base ~theming:`Themed) + | false -> base ~theming:`Legacy_don't_use_theme graph + | true -> base ~theming:`Themed graph) in - let%sub toggle_focus_lock_button = - let%sub on_click = + let toggle_focus_lock_button = + let on_click = let%arr focus_is_locked = focus_is_locked and lock_focus = lock_focus and unlock_focus = unlock_focus in if focus_is_locked then unlock_focus else lock_focus in - let%sub theme = View.Theme.current in + let theme = View.Theme.current graph in let%arr on_click = on_click and focus_is_locked = focus_is_locked and theme = theme in let text = if focus_is_locked then "Unlock focus" else "Lock focus" in View.button theme ~on_click text in - let%sub form_view = - let%sub width_form = PRT_example.Column_width_form.component ~set_column_width in + let form_view = + let width_form = PRT_example.Column_width_form.component ~set_column_width graph in let%arr form_view = form_view and width_form = width_form in View.vbox [ form_view; width_form ] @@ -83,9 +83,9 @@ let component ~theme_picker = [ theme_picker; form_view; toggle_focus_lock_button; table ] ;; -let component_with_theme = - let%sub theme, theme_picker = Bonsai_web_ui_gallery.Theme_picker.component () in - View.Theme.set_for_app theme (component ~theme_picker) +let component_with_theme graph = + let%sub theme, theme_picker = Bonsai_web_ui_gallery.Theme_picker.component () graph in + View.Theme.set_for_app theme (component ~theme_picker) graph ;; let () = Bonsai_web.Start.start component_with_theme diff --git a/examples/partial_render_table/src/bonsai_partial_render_table_example.ml b/examples/partial_render_table/src/bonsai_partial_render_table_example.ml index db7d3f8e..ffc4d533 100644 --- a/examples/partial_render_table/src/bonsai_partial_render_table_example.ml +++ b/examples/partial_render_table/src/bonsai_partial_render_table_example.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Table = Bonsai_web_ui_partial_render_table.Basic module Indexed_column_id = Bonsai_web_ui_partial_render_table.Indexed_column_id @@ -47,17 +47,17 @@ let column_helper then None else Some - (Value.return (fun (_, a) (_, b) -> + (Bonsai.return (fun (_, a) (_, b) -> M.compare (Field.get field a) (Field.get field b))) in let render_header text = - Value.return (Column.Sortable.Header.with_icon (Vdom.Node.text text)) + Bonsai.return (Column.Sortable.Header.with_icon (Vdom.Node.text text)) in Column.column ?visible ~header:(render_header (Fieldslib.Field.name field)) ?sort - ~cell:(fun ~key:_ ~data -> + ~cell:(fun ~key:_ ~data _graph -> let%arr data = data in Vdom.Node.text (M.to_string (Field.get field data))) () @@ -76,49 +76,49 @@ let special_compare_option how compare_inner a b = let columns ~should_show_position = let render_header text = - Value.return (Column.Sortable.Header.with_icon (Vdom.Node.text text)) + Bonsai.return (Column.Sortable.Header.with_icon (Vdom.Node.text text)) in - [ column_helper (module String) Row.Fields.symbol - ; column_helper (module Float) Row.Fields.edge - ; column_helper (module Float) Row.Fields.max_edge - ; column_helper (module Int) Row.Fields.bsize - ; column_helper (module Float) Row.Fields.bid - ; column_helper (module Float) Row.Fields.ask - ; column_helper (module Int) Row.Fields.asize - ; Column.group - ~label:(Value.return (Vdom.Node.text "some group")) - [ Column.group - ~label:(Value.return (Vdom.Node.text "small")) - [ column_helper - (module Int) - Row.Fields.position - ~disable_sort:true - ~visible:should_show_position - ] - ; Column.column - ~header:(render_header "last fill") - ~sort: - (Value.return (fun (_key1, a) (_key2, b) -> - special_compare_option - `Ascending - [%compare: Time_ns.t] - a.Row.last_fill - b.Row.last_fill)) - ~sort_reversed: - (Value.return (fun (_key1, a) (_key2, b) -> - special_compare_option - `Descending - [%compare: Time_ns.t] - a.Row.last_fill - b.Row.last_fill)) - ~cell:(fun ~key:_ ~data -> - let%arr data = data in - Vdom.Node.text (Time_ns_option.to_string data.Row.last_fill)) - () - ] - ; column_helper (module String) Row.Fields.trader - ] - |> Column.lift + Column.lift + [ column_helper (module String) Row.Fields.symbol + ; column_helper (module Float) Row.Fields.edge + ; column_helper (module Float) Row.Fields.max_edge + ; column_helper (module Int) Row.Fields.bsize + ; column_helper (module Float) Row.Fields.bid + ; column_helper (module Float) Row.Fields.ask + ; column_helper (module Int) Row.Fields.asize + ; Column.group + ~label:(Bonsai.return (Vdom.Node.text "some group")) + [ Column.group + ~label:(Bonsai.return (Vdom.Node.text "small")) + [ column_helper + (module Int) + Row.Fields.position + ~disable_sort:true + ~visible:should_show_position + ] + ; Column.column + ~header:(render_header "last fill") + ~sort: + (Bonsai.return (fun (_key1, a) (_key2, b) -> + special_compare_option + `Ascending + [%compare: Time_ns.t] + a.Row.last_fill + b.Row.last_fill)) + ~sort_reversed: + (Bonsai.return (fun (_key1, a) (_key2, b) -> + special_compare_option + `Descending + [%compare: Time_ns.t] + a.Row.last_fill + b.Row.last_fill)) + ~cell:(fun ~key:_ ~data _graph -> + let%arr data = data in + Vdom.Node.text (Time_ns_option.to_string data.Row.last_fill)) + () + ] + ; column_helper (module String) Row.Fields.trader + ] ;; type t = @@ -143,8 +143,9 @@ let generic_table_and_focus_attr ~get_unlock_focus ~attr_of_focus data + graph = - let%sub table = + let table = Table.component (module String) ?filter @@ -154,6 +155,7 @@ let generic_table_and_focus_attr ~row_height ~columns:(columns ~should_show_position) data + graph in let%arr { Table.Result.view = table ; for_testing = _ @@ -193,7 +195,7 @@ let component ~theming ~multisort_columns_when ~should_show_position - ~focus:(By_row { on_change = Value.return (Fn.const Effect.Ignore) }) + ~focus:(By_row { on_change = Bonsai.return (Fn.const Effect.Ignore) }) ~get_lock_focus:Focus_control.lock_focus ~get_unlock_focus:Focus_control.unlock_focus ~get_focus_is_locked:Focus_control.focus_is_locked @@ -222,7 +224,7 @@ let component ~theming ~multisort_columns_when ~should_show_position - ~focus:(By_cell { on_change = Value.return (Fn.const Effect.Ignore) }) + ~focus:(By_cell { on_change = Bonsai.return (Fn.const Effect.Ignore) }) ~get_lock_focus:Focus_control.lock_focus ~get_unlock_focus:Focus_control.unlock_focus ~get_focus_is_locked:Focus_control.focus_is_locked @@ -278,12 +280,14 @@ module Layout_form = struct } [@@deriving typed_fields] - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = function - | Themed -> Form.Elements.Toggle.bool ~default:true () - | Show_position -> Form.Elements.Toggle.bool ~default:true () - | Cell_based_highlighting -> Form.Elements.Toggle.bool ~default:false () + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with + | Themed -> Form.Elements.Toggle.bool ~default:true () graph + | Show_position -> Form.Elements.Toggle.bool ~default:true () graph + | Cell_based_highlighting -> Form.Elements.Toggle.bool ~default:false () graph | Row_height -> - let%sub form = + let form = Form.Elements.Range.int ~min:0 ~max:100 @@ -291,6 +295,7 @@ module Layout_form = struct ~allow_updates_when_focused:`Never () ~default:30 + graph in let%arr form = form in Form.project form ~parse_exn:(fun x -> `Px x) ~unparse:(fun (`Px x) -> x) @@ -300,15 +305,16 @@ module Layout_form = struct ~default:10_000 ~step:1 () + graph | Multisort_columns_when -> - Form.Elements.Dropdown.enumerable (module Multisort_columns_when) + Form.Elements.Dropdown.enumerable (module Multisort_columns_when) graph ;; let label_for_field = `Inferred end - let component = - let%sub form = Form.Typed.Record.make (module Params) in + let component graph = + let form = Form.Typed.Record.make (module Params) graph in let%arr form = form in let values = Form.value_or_default @@ -328,16 +334,17 @@ module Layout_form = struct end module Column_width_form = struct - let component ~set_column_width = + let component ~set_column_width graph = let open Bonsai.Let_syntax in - let%sub form = + let form = Form.Elements.Textbox.int - ~placeholder:(Value.return "Symbol column width") + ~placeholder:(Bonsai.return "Symbol column width") ~allow_updates_when_focused:`Always () + graph in - let%sub button = - let%sub theme = View.Theme.current in + let button = + let theme = View.Theme.current graph in let%arr form = form and theme = theme and set_column_width = set_column_width in diff --git a/examples/partial_render_table/src/bonsai_partial_render_table_example.mli b/examples/partial_render_table/src/bonsai_partial_render_table_example.mli index ef491bc9..3bc52c7a 100644 --- a/examples/partial_render_table/src/bonsai_partial_render_table_example.mli +++ b/examples/partial_render_table/src/bonsai_partial_render_table_example.mli @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont module Row = Row type t = @@ -15,14 +15,15 @@ type t = } val component - : ?filter:(key:string -> data:Row.t -> bool) Value.t + : ?filter:(key:string -> data:Row.t -> bool) Bonsai.t -> focus_kind:[ `Row | `Cell ] - -> row_height:[ `Px of int ] Value.t + -> row_height:[ `Px of int ] Bonsai.t -> theming:[ `Legacy_don't_use_theme | `Themed ] - -> multisort_columns_when:[ `Shift_click | `Ctrl_click | `Shift_or_ctrl_click ] Value.t - -> should_show_position:bool Value.t - -> (string, Row.t, Base.String.comparator_witness) Base.Map.t Value.t - -> t Computation.t + -> multisort_columns_when:[ `Shift_click | `Ctrl_click | `Shift_or_ctrl_click ] Bonsai.t + -> should_show_position:bool Bonsai.t + -> (string, Row.t, Base.String.comparator_witness) Base.Map.t Bonsai.t + -> Bonsai.graph + -> t Bonsai.t module Layout_form : sig module Params : sig @@ -36,7 +37,7 @@ module Layout_form : sig } end - val component : (Vdom.Node.t * Params.t) Computation.t + val component : Bonsai.graph -> (Vdom.Node.t * Params.t) Bonsai.t end module Column_width_form : sig @@ -45,6 +46,7 @@ module Column_width_form : sig (column_id:Bonsai_web_ui_partial_render_table.Indexed_column_id.t -> [ `Px_float of float ] -> unit Ui_effect.t) - Value.t - -> Vdom.Node.t Computation.t + Bonsai.t + -> Bonsai.graph + -> Vdom.Node.t Bonsai.t end diff --git a/examples/partial_render_table_fully_dynamic/main.ml b/examples/partial_render_table_fully_dynamic/main.ml index 27ee968c..138c9c02 100644 --- a/examples/partial_render_table_fully_dynamic/main.ml +++ b/examples/partial_render_table_fully_dynamic/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Table = Bonsai_web_ui_partial_render_table.Basic module Form = Bonsai_web_ui_form.With_automatic_view @@ -21,24 +21,22 @@ module Col_id = struct include Comparator.Make (Row.Typed_field.Packed) end -let component ?filter (data : Row.t String.Map.t Value.t) = - let all = Value.return Row.Typed_field.Packed.all in - let%sub form = Form.Elements.Typeahead.list (module Col_id) ~all_options:all in - let%sub form = Form.Dynamic.with_default all form in - let%sub columns = - return (form >>| Form.value_or_default ~default:Row.Typed_field.Packed.all) - in - let%sub table = +let component ?filter (data : Row.t String.Map.t Bonsai.t) graph = + let all = Bonsai.return Row.Typed_field.Packed.all in + let form = Form.Elements.Typeahead.list (module Col_id) ~all_options:all graph in + let form = Form.Dynamic.with_default all form graph in + let columns = form >>| Form.value_or_default ~default:Row.Typed_field.Packed.all in + let table = Table.component (module String) ?filter - ~focus:(By_row { on_change = Value.return (Fn.const Effect.Ignore) }) - ~row_height:(Value.return (`Px 30)) + ~focus:(By_row { on_change = Bonsai.return (Fn.const Effect.Ignore) }) + ~row_height:(Bonsai.return (`Px 30)) ~columns: (Column.build (module Col_id) ~columns - ~render_cell:(fun col _key data -> + ~render_cell:(fun col _key data _graph -> let%arr { f = T field } = col and data = data in let string, float, int = @@ -56,11 +54,12 @@ let component ?filter (data : Row.t String.Map.t Value.t) = | Position -> int value | Last_fill -> Vdom.Node.text (Time_ns_option.to_string value) | Trader -> string value) - ~render_header:(fun col -> + ~render_header:(fun col _graph -> let%arr { f = T field } = col in Table.Columns.Dynamic_columns.Sortable.Header.with_icon (Vdom.Node.text (Row.Typed_field.name field)))) data + graph in let%arr { Table.Result.view = table ; for_testing = _ @@ -95,8 +94,8 @@ let component ?filter (data : Row.t String.Map.t Value.t) = ;; let () = - let input = Value.return (Row.many_random 100_000) in + let input = Bonsai.return (Row.many_random 100_000) in component input - |> View.Theme.set_for_app (Value.return (Kado.theme ~version:Bleeding ())) + |> View.Theme.set_for_app (Bonsai.return (Kado.theme ~version:Bleeding ())) |> Bonsai_web.Start.start ;; diff --git a/examples/persistent_var/main.ml b/examples/persistent_var/main.ml index 75e5c2cb..1ff7a152 100644 --- a/examples/persistent_var/main.ml +++ b/examples/persistent_var/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax let local_storage_var = @@ -18,7 +18,7 @@ let session_storage_var = ~default:"This will be saved in session storage!" ;; -let display_text_var ~doc storage_var = +let display_text_var ~doc storage_var _graph = let set_effect = Bonsai_web.Persistent_var.effect storage_var in let%arr value = Bonsai_web.Persistent_var.value storage_var in Vdom.Node.div @@ -34,20 +34,22 @@ let display_text_var ~doc storage_var = ] ;; -let component = - let%sub local_storage_node = +let component graph = + let local_storage_node = display_text_var ~doc: "Write some text in this box and refresh the page or close the tab and reopen \ it; the text should still be there!" local_storage_var + graph in - let%sub session_storage_node = + let session_storage_node = display_text_var ~doc: "Write some text in this box and refresh the page; the text should still be \ there!" session_storage_var + graph in let%arr local_storage_node = local_storage_node and session_storage_node = session_storage_node in diff --git a/examples/polling_state_rpc_stress_test/main.ml b/examples/polling_state_rpc_stress_test/main.ml index 5a46cc63..c46ba12e 100644 --- a/examples/polling_state_rpc_stress_test/main.ml +++ b/examples/polling_state_rpc_stress_test/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Async_kernel open! Async_rpc_kernel open Bonsai.Let_syntax @@ -14,7 +14,7 @@ open Bonsai.Let_syntax type Rpc_effect.Where_to_connect.Custom.t += Connection module T = struct - type t = { data : int Int.Map.t [@diff.map] } [@@deriving sexp, diff, bin_io, equal] + type t = { data : int Int.Map.t } [@@deriving sexp, diff, bin_io, equal] end let rpc = @@ -26,29 +26,34 @@ let rpc = (module Diffable_polling_state_rpc_response.Polling_state_rpc_response.Make (T)) ;; -let component = +let component graph = let%sub (_, items), inject = - Bonsai.state_machine0 - () - ~sexp_of_model:[%sexp_of: int * unit Int.Map.t] - ~equal:[%equal: int * unit Int.Map.t] - ~sexp_of_action:[%sexp_of: [ `Add | `Remove of int ]] - ~default_model:(0, Int.Map.empty) - ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) (last_index, map) action -> - match action with - | `Add -> - let map = - if Map.length map > 100 then Map.remove map (fst (Map.min_elt_exn map)) else map - in - last_index + 1, Map.set map ~key:last_index ~data:() - | `Remove i -> last_index, Map.remove map i) + Tuple2.uncurry Bonsai.both + @@ Bonsai.state_machine0 + graph + ~sexp_of_model:[%sexp_of: int * unit Int.Map.t] + ~equal:[%equal: int * unit Int.Map.t] + ~sexp_of_action:[%sexp_of: [ `Add | `Remove of int ]] + ~default_model:(0, Int.Map.empty) + ~apply_action: + (fun + (_ : _ Bonsai.Apply_action_context.t) (last_index, map) action -> + match action with + | `Add -> + let map = + if Map.length map > 100 + then Map.remove map (fst (Map.min_elt_exn map)) + else map + in + last_index + 1, Map.set map ~key:last_index ~data:() + | `Remove i -> last_index, Map.remove map i) in - let%sub items = + let items = Bonsai.assoc (module Int) items - ~f:(fun key _data -> - let%sub response = + ~f:(fun key _data graph -> + let response = Rpc_effect.Polling_state_rpc.poll ~sexp_of_query:[%sexp_of: Int.t] ~sexp_of_response:[%sexp_of: T.t] @@ -58,6 +63,7 @@ let component = ~where_to_connect:(Custom Connection) ~every:(Time_ns.Span.of_sec 1.0) key + graph in let%arr key = key and inject = inject @@ -71,6 +77,7 @@ let component = [%sexp (response : (int, T.t) Rpc_effect.Poll_result.t)] ] ]) + graph in let%arr items = items and inject = inject in diff --git a/examples/popover/main.ml b/examples/popover/main.ml index 112a77fe..ef61003c 100644 --- a/examples/popover/main.ml +++ b/examples/popover/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax module Gallery = Bonsai_web_ui_gallery @@ -10,28 +10,29 @@ module Popover = struct {| This popover gives a warning that prompts for extra confirmation. |} ;; - let view = + let view graph = let vdom, demo = [%demo - let%sub theme = View.Theme.current in - let popover_content ~close = + let theme = View.Theme.current graph in + let popover_content ~close _graph = let%arr close = close and theme = theme in View.button theme ~on_click:close "Close popover" in - let%sub popover = + let popover = Bonsai_web_ui_popover.component - ~close_when_clicked_outside:(Value.return true) - ~direction:(Value.return Bonsai_web_ui_popover.Direction.Right) - ~alignment:(Value.return Bonsai_web_ui_popover.Alignment.Center) + ~close_when_clicked_outside:(Bonsai.return true) + ~direction:(Bonsai.return Bonsai_web_ui_popover.Direction.Right) + ~alignment:(Bonsai.return Bonsai_web_ui_popover.Alignment.Center) ~popover:popover_content () + graph in let%arr { wrap; open_; close = _; toggle = _; is_open = _ } = popover and theme = theme in wrap (View.button theme ~intent:Info ~on_click:open_ "Open Popover")] in - Computation.map vdom ~f:(fun vdom -> vdom, demo) + Bonsai.map vdom ~f:(fun vdom -> vdom, demo) ;; let selector = None @@ -45,16 +46,16 @@ module Context_menu_popover = struct {| This popover shows available actions, similar to a context menu. It also showcases nested popovers.|} ;; - let view = + let view graph = let computation, demo = [%demo - let%sub theme = View.Theme.current in - let%sub popover = + let theme = View.Theme.current graph in + let popover = Bonsai_web_ui_popover.component - ~close_when_clicked_outside:(Value.return true) - ~direction:(Value.return Bonsai_web_ui_popover.Direction.Right) - ~alignment:(Value.return Bonsai_web_ui_popover.Alignment.Center) - ~popover:(fun ~close:_ -> + ~close_when_clicked_outside:(Bonsai.return true) + ~direction:(Bonsai.return Bonsai_web_ui_popover.Direction.Right) + ~alignment:(Bonsai.return Bonsai_web_ui_popover.Alignment.Center) + ~popover:(fun ~close:_ _graph -> let%arr theme = theme in View.vbox [ View.text "Context Menu" @@ -62,6 +63,7 @@ module Context_menu_popover = struct ; View.button theme ~intent:Warning ~on_click:Effect.Ignore "Action 2" ]) () + graph in let%arr { wrap; open_; _ } = popover in let attr = @@ -70,15 +72,15 @@ module Context_menu_popover = struct in wrap (View.text ~attrs:[ attr ] "Right click me!")] in - Computation.map computation ~f:(fun vdom -> vdom, demo) + Bonsai.map computation ~f:(fun vdom -> vdom, demo) ;; let selector = None let filter_attrs = Some (fun k _ -> not (String.is_prefix k ~prefix:"style")) end -let component = - let%sub theme, theme_picker = Gallery.Theme_picker.component () in +let component graph = + let%sub theme, theme_picker = Gallery.Theme_picker.component () graph in View.Theme.set_for_app theme (Gallery.make_sections @@ -90,6 +92,7 @@ let component = ; Gallery.make_demo (module Context_menu_popover) ] ) ]) + graph ;; let () = diff --git a/examples/popover_test/main.ml b/examples/popover_test/main.ml index 73c50f05..226fddae 100644 --- a/examples/popover_test/main.ml +++ b/examples/popover_test/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax module Gallery = Bonsai_web_ui_gallery @@ -10,24 +10,28 @@ module Toggle_popover = struct {| This tests that there is not a bad interaction between popover toggling behavior and the [close_when_clicked_outside] flag. |} ;; - let view = + let view graph = let vdom, demo = [%demo - let%sub theme = View.Theme.current in - let popover_content ~close:_ = Bonsai.const (View.text "Popover contents") in - let%sub popover = - Bonsai_web_ui_popover.component - ~close_when_clicked_outside:(Value.return true) - ~direction:(Value.return Bonsai_web_ui_popover.Direction.Right) - ~alignment:(Value.return Bonsai_web_ui_popover.Alignment.Center) - ~popover:popover_content - () - in - let%arr { wrap; open_ = _; close = _; toggle; is_open = _ } = popover - and theme = theme in - wrap (View.button theme ~intent:Info ~on_click:toggle "toggle popover")] + fun graph -> + let theme = View.Theme.current graph in + let popover_content ~close:_ _graph = + Bonsai.return (View.text "Popover contents") + in + let popover = + Bonsai_web_ui_popover.component + ~close_when_clicked_outside:(Bonsai.return true) + ~direction:(Bonsai.return Bonsai_web_ui_popover.Direction.Right) + ~alignment:(Bonsai.return Bonsai_web_ui_popover.Alignment.Center) + ~popover:popover_content + () + graph + in + let%arr { wrap; open_ = _; close = _; toggle; is_open = _ } = popover + and theme = theme in + wrap (View.button theme ~intent:Info ~on_click:toggle "toggle popover")] in - Computation.map vdom ~f:(fun vdom -> vdom, demo) + Bonsai.map (vdom graph) ~f:(fun vdom -> vdom, demo) ;; let selector = None @@ -41,23 +45,26 @@ module Two_left_click_popovers = struct {|This test tests against a regression where clicking on the outside to click a context menu, also opened chrome's context menu.|} ;; - let view = + let view graph = let vdom, demo = [%demo - let%sub theme = View.Theme.current in - let popover_content ~close:_ = Bonsai.const (View.text "Popover contents") in - let popover = - let%sub popover = + let theme = View.Theme.current graph in + let popover_content ~close:_ _graph = + Bonsai.return (View.text "Popover contents") + in + let popover graph = + let popover = Bonsai_web_ui_popover.component - ~close_when_clicked_outside:(Value.return true) + ~close_when_clicked_outside:(Bonsai.return true) ~allow_event_propagation_when_clicked_outside: - (Value.return (function + (Bonsai.return (function | `Left_click | `Escape -> false | `Right_click -> true)) - ~direction:(Value.return Bonsai_web_ui_popover.Direction.Right) - ~alignment:(Value.return Bonsai_web_ui_popover.Alignment.Center) + ~direction:(Bonsai.return Bonsai_web_ui_popover.Direction.Right) + ~alignment:(Bonsai.return Bonsai_web_ui_popover.Alignment.Center) ~popover:popover_content () + graph in let%arr { wrap; open_; close = _; toggle = _; is_open = _ } = popover and theme = theme in @@ -72,19 +79,19 @@ module Two_left_click_popovers = struct ~on_click:open_ "toggle popover") in - let%map.Computation p1 = popover - and p2 = popover in + let%map p1 = popover graph + and p2 = popover graph in Vdom.Node.div [ p1; p2 ]] in - Computation.map vdom ~f:(fun vdom -> vdom, demo) + Bonsai.map vdom ~f:(fun vdom -> vdom, demo) ;; let selector = None let filter_attrs = Some (fun k _ -> not (String.is_prefix k ~prefix:"style")) end -let component = - let%sub theme, theme_picker = Gallery.Theme_picker.component () in +let component graph = + let%sub theme, theme_picker = Gallery.Theme_picker.component () graph in View.Theme.set_for_app theme (Gallery.make_sections @@ -95,6 +102,7 @@ let component = ; Gallery.make_demo (module Toggle_popover) ] ) ]) + graph ;; let () = diff --git a/examples/ppx_html/dune b/examples/ppx_html/dune index 0f08ce6e..e61592c3 100644 --- a/examples/ppx_html/dune +++ b/examples/ppx_html/dune @@ -3,4 +3,4 @@ (names main) (libraries bonsai_web bonsai_web_ui_gallery virtual_dom.svg) (preprocess - (pps ppx_jane ppx_bonsai ppx_html ppx_demo))) + (pps ppx_jane ppx_bonsai ppx_html ppx_demo ppx_tailwind))) diff --git a/examples/ppx_html/main.ml b/examples/ppx_html/main.ml index b8db6d9d..29364d3c 100644 --- a/examples/ppx_html/main.ml +++ b/examples/ppx_html/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax module Gallery = Bonsai_web_ui_gallery @@ -28,7 +28,7 @@ module Basic = struct
|}]] in - Bonsai.const (vdom, demo) + fun _graph -> Bonsai.return (vdom, demo) ;; let selector = None @@ -43,7 +43,7 @@ module With_tailwind = struct let vdom, demo = [%demo [%html {|
|}]] in - Bonsai.const (vdom, demo) + fun _graph -> Bonsai.return (vdom, demo) ;; let selector = None @@ -71,15 +71,15 @@ module Svg_example = struct |}]] in - Bonsai.const (vdom, demo) + fun _graph -> Bonsai.return (vdom, demo) ;; let selector = None let filter_attrs = Some (fun k _ -> not (String.is_prefix k ~prefix:"style")) end -let component = - let%sub theme, theme_picker = Gallery.Theme_picker.component ~default:Kado () in +let component graph = + let%sub theme, theme_picker = Gallery.Theme_picker.component ~default:Kado () graph in View.Theme.set_for_app theme (Gallery.make_sections @@ -91,6 +91,7 @@ let component = ; Gallery.make_demo (module With_tailwind) ] ) ]) + graph ;; let () = Bonsai_web.Start.start component diff --git a/examples/ppx_html_internal/main.ml b/examples/ppx_html_internal/main.ml index 796d9fdc..0924ab4c 100644 --- a/examples/ppx_html_internal/main.ml +++ b/examples/ppx_html_internal/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax module Gallery = Bonsai_web_ui_gallery @@ -42,15 +42,15 @@ module Svg_example = struct |}]] in - Bonsai.const (vdom, demo) + fun _graph -> Bonsai.return (vdom, demo) ;; let selector = None let filter_attrs = Some (fun k _ -> not (String.is_prefix k ~prefix:"style")) end -let component = - let%sub theme, theme_picker = Gallery.Theme_picker.component ~default:Kado () in +let component graph = + let%sub theme, theme_picker = Gallery.Theme_picker.component ~default:Kado () graph in View.Theme.set_for_app theme (Gallery.make_sections @@ -59,6 +59,7 @@ let component = , {|Internal testing example to test out ppx_html syntax we may not want to show to newcomers.|} , [ Gallery.make_demo (module Svg_example) ] ) ]) + graph ;; let () = Bonsai_web.Start.start component diff --git a/examples/query_box/main.ml b/examples/query_box/main.ml index cfacc08b..75e06a73 100644 --- a/examples/query_box/main.ml +++ b/examples/query_box/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Vdom open Bonsai.Let_syntax module Form = Bonsai_web_ui_form.With_automatic_view @@ -68,10 +68,10 @@ let items = ] ;; -let component = - let%sub selected_items, add_item = +let component graph = + let selected_items, add_item = Bonsai.state_machine0 - () + graph ~sexp_of_model:[%sexp_of: string list] ~equal:[%equal: string list] ~sexp_of_action:[%sexp_of: String.t] @@ -79,29 +79,35 @@ let component = ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) items item -> item :: items) in - let%sub form = + let form = Form.Typed.Record.make (module struct module Typed_field = Example_params.Typed_field let label_for_field = `Inferred - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = function + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with | Suggestion_list_kind -> - Form.Elements.Dropdown.enumerable (module Query_box.Suggestion_list_kind) + Form.Elements.Dropdown.enumerable + (module Query_box.Suggestion_list_kind) + graph | Expand_direction -> - Form.Elements.Dropdown.enumerable (module Query_box.Expand_direction) + Form.Elements.Dropdown.enumerable (module Query_box.Expand_direction) graph | Max_visible_items -> Form.Elements.Number.int ~default:10 ~step:1 ~allow_updates_when_focused:`Never () - | Input_source -> Form.Elements.Dropdown.enumerable (module Input_source) + graph + | Input_source -> Form.Elements.Dropdown.enumerable (module Input_source) graph | Filter_strategy -> - Form.Elements.Dropdown.enumerable (module Query_box.Filter_strategy) + Form.Elements.Dropdown.enumerable (module Query_box.Filter_strategy) graph ;; end) + graph in let%sub { suggestion_list_kind ; expand_direction @@ -110,12 +116,12 @@ let component = ; filter_strategy } = - return (form >>| Form.value_or_default ~default:Example_params.default) + form >>| Form.value_or_default ~default:Example_params.default in - let%sub data = + let data = match%sub input_source with | Small_and_static_list_of_fruits -> - Bonsai.const (String.Map.of_alist_exn (List.map ~f:(fun x -> x, x) items)) + Bonsai.return (String.Map.of_alist_exn (List.map ~f:(fun x -> x, x) items)) | Large_and_rapidly_changing_filepaths -> let module Action = struct let quickcheck_generator_string = @@ -130,16 +136,16 @@ let component = [@@deriving sexp_of, quickcheck] end in - let%sub map, inject = + let map, inject = Bonsai.state_machine0 ~default_model:String.Map.empty ~apply_action:(fun _context model action -> match action with | Action.Add key -> Map.set model ~key ~data:key | Remove key -> Map.remove model key) - () + graph in - let%sub add_random_item = + let add_random_item = let%arr inject = inject in let%bind.Effect item = Effect.of_sync_fun @@ -152,15 +158,16 @@ let component = in inject item in - let%sub () = + let () = Bonsai.Clock.every ~when_to_start_next_effect:`Wait_period_after_previous_effect_starts_blocking (Time_ns.Span.of_sec 0.2) add_random_item + graph in - return map + map in - let%sub query_box = + let query_box = (* [filter_strategy] is not a dynamic parameter to [Query_box.stringable], so we have to add introduce dynamism ourselves with [match%sub]. *) match%sub filter_strategy with @@ -170,24 +177,26 @@ let component = ~suggestion_list_kind ~expand_direction ~max_visible_items - ~selected_item_attr:(Value.return Css.selected_item) - ~extra_list_container_attr:(Value.return Css.list_container) - ~extra_input_attr:(Value.return (Attr.placeholder "Filter Fruits")) + ~selected_item_attr:(Bonsai.return Css.selected_item) + ~extra_list_container_attr:(Bonsai.return Css.list_container) + ~extra_input_attr:(Bonsai.return (Attr.placeholder "Filter Fruits")) ~filter_strategy:Fuzzy_search_and_score ~on_select:add_item data + graph | Fuzzy_search_and_score -> Query_box.stringable (module String) ~suggestion_list_kind ~expand_direction ~max_visible_items - ~selected_item_attr:(Value.return Css.selected_item) - ~extra_list_container_attr:(Value.return Css.list_container) - ~extra_input_attr:(Value.return (Attr.placeholder "Filter Fruits")) + ~selected_item_attr:(Bonsai.return Css.selected_item) + ~extra_list_container_attr:(Bonsai.return Css.list_container) + ~extra_input_attr:(Bonsai.return (Attr.placeholder "Filter Fruits")) ~filter_strategy:Fuzzy_match ~on_select:add_item data + graph in let%arr selected_items = selected_items and query_box = query_box diff --git a/examples/rpc_chat/client/bin/main.ml b/examples/rpc_chat/client/bin/main.ml index 1aa67632..bda0ab50 100644 --- a/examples/rpc_chat/client/bin/main.ml +++ b/examples/rpc_chat/client/bin/main.ml @@ -1,6 +1,6 @@ open! Core open! Async_kernel -open! Bonsai_web +open! Bonsai_web.Cont open Async_js open Bonsai_chat_common module Rpc_connection = Persistent_connection.Rpc @@ -17,7 +17,7 @@ let process_message_stream ~conn ~room_state_var = let%bind conn = Rpc_connection.connected conn in let%bind pipe, _ = Rpc.Pipe_rpc.dispatch_exn Protocol.Message_stream.t conn () in Pipe.iter_without_pushback pipe ~f:(fun message -> - Bonsai.Var.update room_state_var ~f:(function + Bonsai.Expert.Var.update room_state_var ~f:(function | { Room_state.messages; current_room } when [%equal: Room.t option] current_room (Some message.room) -> { current_room; messages = List.append messages [ message ] } @@ -38,7 +38,7 @@ let change_room ~conn ~room_state_var = let on_room_switch room = let%bind conn = Rpc_connection.connected conn in let%map messages = Rpc.Rpc.dispatch_exn Protocol.Messages_request.t conn room in - Bonsai.Var.set room_state_var { Room_state.messages; current_room = Some room } + Bonsai.Expert.Var.set room_state_var { Room_state.messages; current_room = Some room } in let dispatch = Effect.of_deferred_fun on_room_switch in fun room -> dispatch room @@ -53,14 +53,14 @@ let run () = Deferred.Or_error.return in let room_state_var = - Bonsai.Var.create { Room_state.messages = []; current_room = None } + Bonsai.Expert.Var.create { Room_state.messages = []; current_room = None } in let change_room = change_room ~conn ~room_state_var in let component = let open Bonsai.Let_syntax in Bonsai_examples_rpc_chat_client.App.component - ~current_room:(Bonsai.Var.value room_state_var >>| Room_state.current_room) - ~messages:(Bonsai.Var.value room_state_var >>| Room_state.messages) + ~current_room:(Bonsai.Expert.Var.value room_state_var >>| Room_state.current_room) + ~messages:(Bonsai.Expert.Var.value room_state_var >>| Room_state.messages) ~change_room ~obfuscate_message in diff --git a/examples/rpc_chat/client/src/app.ml b/examples/rpc_chat/client/src/app.ml index 6f27f637..fc9a4515 100644 --- a/examples/rpc_chat/client/src/app.ml +++ b/examples/rpc_chat/client/src/app.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax open Bonsai_chat_common @@ -34,27 +34,28 @@ stylesheet } |}] -let component ~current_room ~messages ~change_room ~obfuscate_message = - let%sub dispatch_room_list = - Rpc_effect.Rpc.dispatcher Protocol.List_rooms.t ~where_to_connect:Self +let component ~current_room ~messages ~change_room ~obfuscate_message graph = + let dispatch_room_list = + Rpc_effect.Rpc.dispatcher Protocol.List_rooms.t ~where_to_connect:Self graph in - let%sub fetch_room_list = + let fetch_room_list = let%arr dispatch_room_list = dispatch_room_list in match%map.Effect dispatch_room_list () with | Ok rooms -> rooms | Error _ -> [] in - let%sub room_list, refresh_rooms = + let room_list, refresh_rooms = Bonsai.Edge.Poll.manual_refresh ~sexp_of_model:[%sexp_of: Room.t list] ~equal:[%equal: Room.t list] (Bonsai.Edge.Poll.Starting.initial []) ~effect:fetch_room_list + graph in - let%sub dispatch_send_message = - Rpc_effect.Rpc.dispatcher Protocol.Send_message.t ~where_to_connect:Self + let dispatch_send_message = + Rpc_effect.Rpc.dispatcher Protocol.Send_message.t ~where_to_connect:Self graph in - let%sub send_message = + let send_message = let%arr dispatch_send_message = dispatch_send_message and current_room = current_room in match current_room with @@ -70,10 +71,12 @@ let component ~current_room ~messages ~change_room ~obfuscate_message = let current_room = current_room >>| Option.value ~default:(Room.of_string "no room selected") in - let%sub rooms_list = Room_list_panel.component ~room_list ~refresh_rooms ~change_room in - let%sub compose_panel = Compose_message.component ~send_message in - let%sub messages_panel = Messages_panel.component ~messages ~current_room in - let%sub connection_status = Rpc_effect.Status.state ~where_to_connect:Self in + let rooms_list = + Room_list_panel.component ~room_list ~refresh_rooms ~change_room graph + in + let compose_panel = Compose_message.component ~send_message graph in + let messages_panel = Messages_panel.component ~messages ~current_room graph in + let connection_status = Rpc_effect.Status.state ~where_to_connect:Self graph in let%arr rooms_list = rooms_list and compose_panel = compose_panel and messages_panel = messages_panel diff --git a/examples/rpc_chat/client/src/app.mli b/examples/rpc_chat/client/src/app.mli index cbe5352c..93972d5a 100644 --- a/examples/rpc_chat/client/src/app.mli +++ b/examples/rpc_chat/client/src/app.mli @@ -1,10 +1,11 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai_chat_common val component - : current_room:Room.t option Value.t - -> messages:Message.t list Value.t + : current_room:Room.t option Bonsai.t + -> messages:Message.t list Bonsai.t -> change_room:(Room.t -> unit Effect.t) -> obfuscate_message:(string -> string) - -> Vdom.Node.t Computation.t + -> Bonsai.graph + -> Vdom.Node.t Bonsai.t diff --git a/examples/rpc_chat/client/src/compose_message.ml b/examples/rpc_chat/client/src/compose_message.ml index b0140eb1..77ee2f63 100644 --- a/examples/rpc_chat/client/src/compose_message.ml +++ b/examples/rpc_chat/client/src/compose_message.ml @@ -1,6 +1,6 @@ open! Core open! Async_kernel -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Style = @@ -21,9 +21,10 @@ stylesheet } |}] -let component ~send_message = - let%sub textbox_state = - Bonsai.state "" ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] +let component ~send_message graph = + let textbox_state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state "" ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] graph in let%arr textbox_content, set_textbox_content = textbox_state and send_message = send_message in diff --git a/examples/rpc_chat/client/src/compose_message.mli b/examples/rpc_chat/client/src/compose_message.mli index 8047875b..bbdd17dd 100644 --- a/examples/rpc_chat/client/src/compose_message.mli +++ b/examples/rpc_chat/client/src/compose_message.mli @@ -1,7 +1,8 @@ open! Core open! Async_kernel -open! Bonsai_web +open! Bonsai_web.Cont val component - : send_message:(string -> unit Effect.t) Value.t - -> Vdom.Node.t Computation.t + : send_message:(string -> unit Effect.t) Bonsai.t + -> Bonsai.graph + -> Vdom.Node.t Bonsai.t diff --git a/examples/rpc_chat/client/src/messages_panel.ml b/examples/rpc_chat/client/src/messages_panel.ml index 64fbe396..ba7fefef 100644 --- a/examples/rpc_chat/client/src/messages_panel.ml +++ b/examples/rpc_chat/client/src/messages_panel.ml @@ -1,6 +1,6 @@ open! Core open! Async_kernel -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax open Bonsai_chat_common @@ -14,7 +14,7 @@ let view_message { Message.room = _; author; contents } = Vdom.Node.div [ Vdom.Node.textf "%s: %s" author contents ] ;; -let component ~messages ~current_room = +let component ~messages ~current_room _graph = let%arr messages = messages and current_room = current_room in Vdom.Node.div diff --git a/examples/rpc_chat/client/src/messages_panel.mli b/examples/rpc_chat/client/src/messages_panel.mli index 11aaaa1e..a018286a 100644 --- a/examples/rpc_chat/client/src/messages_panel.mli +++ b/examples/rpc_chat/client/src/messages_panel.mli @@ -1,9 +1,10 @@ open! Core open! Async_kernel -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai_chat_common val component - : messages:Message.t list Value.t - -> current_room:Room.t Value.t - -> Vdom.Node.t Computation.t + : messages:Message.t list Bonsai.t + -> current_room:Room.t Bonsai.t + -> Bonsai.graph + -> Vdom.Node.t Bonsai.t diff --git a/examples/rpc_chat/client/src/room_list_panel.ml b/examples/rpc_chat/client/src/room_list_panel.ml index a22ddf5e..9f72e2e4 100644 --- a/examples/rpc_chat/client/src/room_list_panel.ml +++ b/examples/rpc_chat/client/src/room_list_panel.ml @@ -1,6 +1,6 @@ open! Core open! Async_kernel -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax open Bonsai_chat_common @@ -20,7 +20,7 @@ stylesheet } |}] -let component ~room_list ~refresh_rooms ~change_room = +let component ~room_list ~refresh_rooms ~change_room _graph = let%arr room_list = room_list and refresh_rooms = refresh_rooms in let room_header = diff --git a/examples/rpc_chat/client/src/room_list_panel.mli b/examples/rpc_chat/client/src/room_list_panel.mli index cdc1aec0..7298269f 100644 --- a/examples/rpc_chat/client/src/room_list_panel.mli +++ b/examples/rpc_chat/client/src/room_list_panel.mli @@ -1,9 +1,10 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai_chat_common val component - : room_list:Room.t list Value.t - -> refresh_rooms:unit Effect.t Value.t + : room_list:Room.t list Bonsai.t + -> refresh_rooms:unit Effect.t Bonsai.t -> change_room:(Room.t -> unit Effect.t) - -> Vdom.Node.t Computation.t + -> Bonsai.graph + -> Vdom.Node.t Bonsai.t diff --git a/examples/rpgdice/bin/dice_spec_clicker_input.ml b/examples/rpgdice/bin/dice_spec_clicker_input.ml index 94c222ed..20568516 100644 --- a/examples/rpgdice/bin/dice_spec_clicker_input.ml +++ b/examples/rpgdice/bin/dice_spec_clicker_input.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Rpgdice = Bonsai_web_rpgdice_example @@ -33,25 +33,26 @@ module Action = struct [@@deriving sexp_of] end -let component = - let%sub dice_state = - Bonsai.state_machine0 - () - ~sexp_of_model:[%sexp_of: Model.t] - ~equal:[%equal: Model.t] - ~sexp_of_action:[%sexp_of: Action.t] - ~default_model:Model.init - ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model -> function - | Decrement_const -> { model with const = model.const - 1 } - | Increment_const -> { model with const = model.const + 1 } - | Increment { num_faces } -> - { model with - dice = - Map.update model.dice num_faces ~f:(function - | None -> failwith "map keys shouldn't have changed" - | Some v -> v + 1) - } - | Clear -> { const = 0; dice = Map.map model.dice ~f:(Fn.const 0) }) +let component graph = + let dice_state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state_machine0 + graph + ~sexp_of_model:[%sexp_of: Model.t] + ~equal:[%equal: Model.t] + ~sexp_of_action:[%sexp_of: Action.t] + ~default_model:Model.init + ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model -> function + | Decrement_const -> { model with const = model.const - 1 } + | Increment_const -> { model with const = model.const + 1 } + | Increment { num_faces } -> + { model with + dice = + Map.update model.dice num_faces ~f:(function + | None -> failwith "map keys shouldn't have changed" + | Some v -> v + 1) + } + | Clear -> { const = 0; dice = Map.map model.dice ~f:(Fn.const 0) }) in let%arr model, inject = dice_state in let button = Vdom_input_widgets.Button.simple in diff --git a/examples/rpgdice/bin/dice_spec_clicker_input.mli b/examples/rpgdice/bin/dice_spec_clicker_input.mli index f72e6912..1728f09e 100644 --- a/examples/rpgdice/bin/dice_spec_clicker_input.mli +++ b/examples/rpgdice/bin/dice_spec_clicker_input.mli @@ -1,4 +1,6 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -val component : (Bonsai_web_rpgdice_example.Roll_spec.t * Vdom.Node.t) Computation.t +val component + : Bonsai.graph + -> (Bonsai_web_rpgdice_example.Roll_spec.t * Vdom.Node.t) Bonsai.t diff --git a/examples/rpgdice/bin/dropdown_menu.ml b/examples/rpgdice/bin/dropdown_menu.ml index 82001a3e..23319dbb 100644 --- a/examples/rpgdice/bin/dropdown_menu.ml +++ b/examples/rpgdice/bin/dropdown_menu.ml @@ -1,5 +1,6 @@ open! Core -open! Bonsai_web +module Bonsai_proc = Bonsai_web +open! Bonsai_web.Cont include Dropdown_menu_intf module Make (Enum : Enum) = struct @@ -32,7 +33,7 @@ module Make (Enum : Enum) = struct end let component = - Bonsai.of_module0 + Bonsai_proc.Bonsai.of_module0 (module T) ~sexp_of_model:[%sexp_of: T.Model.t] ~equal:[%equal: T.Model.t] diff --git a/examples/rpgdice/bin/dropdown_menu_intf.ml b/examples/rpgdice/bin/dropdown_menu_intf.ml index 4cbd618a..55ad5c68 100644 --- a/examples/rpgdice/bin/dropdown_menu_intf.ml +++ b/examples/rpgdice/bin/dropdown_menu_intf.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont module type Enum = sig type t [@@deriving enumerate, equal, sexp] @@ -11,7 +11,7 @@ end module type S = sig type enum - val component : default_model:enum -> (enum * Vdom.Node.t) Computation.t + val component : default_model:enum -> Bonsai.graph -> (enum * Vdom.Node.t) Bonsai.t end module type Dropdown_menu = sig diff --git a/examples/rpgdice/bin/main.ml b/examples/rpgdice/bin/main.ml index 58b8c7e7..626cea90 100644 --- a/examples/rpgdice/bin/main.ml +++ b/examples/rpgdice/bin/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Rpgdice = Bonsai_web_rpgdice_example @@ -15,16 +15,16 @@ end module Input_method_selector = Dropdown_menu.Make (Input_method) -let input_kind ~input_method = +let input_kind ~input_method graph = match%sub input_method with | Input_method.Text -> - String_input.component (module Rpgdice.Roll_spec) ~default_model:"" + String_input.component (module Rpgdice.Roll_spec) ~default_model:"" graph | Clicker -> - let%sub result_and_vdom = Dice_spec_clicker_input.component in - return (result_and_vdom >>| Tuple2.map_fst ~f:Result.return) + let result_and_vdom = Dice_spec_clicker_input.component graph in + result_and_vdom >>| Tuple2.map_fst ~f:Result.return ;; -let app = +let app graph = let build_result ~input ~roller ~input_method_selector = Vdom.Node.div [ Vdom.Node.div ~attrs:[ Vdom.Attr.id "input" ] [ input_method_selector; input ] @@ -32,10 +32,10 @@ let app = ] in let%sub input_method, input_method_selector = - Input_method_selector.component ~default_model:Text + Input_method_selector.component ~default_model:Text graph in - let%sub roll_spec, input = input_kind ~input_method in - let%sub roller = Roller.component roll_spec in + let%sub roll_spec, input = input_kind ~input_method graph in + let roller = Roller.component roll_spec graph in let%arr input = input and roller = roller and input_method_selector = input_method_selector in diff --git a/examples/rpgdice/bin/roller.ml b/examples/rpgdice/bin/roller.ml index c8565fa0..08d38ec3 100644 --- a/examples/rpgdice/bin/roller.ml +++ b/examples/rpgdice/bin/roller.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Rpgdice = Bonsai_web_rpgdice_example @@ -7,30 +7,33 @@ module Model = struct type t = (Rpgdice.Roll_spec.t * Rpgdice.Roll_result.t) option [@@deriving equal, sexp] end -let roller_state = - Bonsai.state_machine1 - ~sexp_of_model:[%sexp_of: Model.t] - ~equal:[%equal: Model.t] - ~sexp_of_action:[%sexp_of: Unit.t] - ~default_model:None - ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) roll_spec model () -> - match roll_spec with - | Active roll_spec -> - (match roll_spec with - | Ok spec -> Some (spec, Rpgdice.Roll_spec.roll spec) - | Error _ -> None) - | Inactive -> - eprint_s - [%message - [%here] - "An action sent to a [state_machine1] has been dropped because its input was \ - not present. This happens when the [state_machine1] is inactive when it \ - receives a message."]; - model) +let roller_state input graph = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state_machine1 + ~sexp_of_model:[%sexp_of: Model.t] + ~equal:[%equal: Model.t] + ~sexp_of_action:[%sexp_of: Unit.t] + ~default_model:None + ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) roll_spec model () -> + match roll_spec with + | Active roll_spec -> + (match roll_spec with + | Ok spec -> Some (spec, Rpgdice.Roll_spec.roll spec) + | Error _ -> None) + | Inactive -> + eprint_s + [%message + [%here] + "An action sent to a [state_machine1] has been dropped because its \ + input was not present. This happens when the [state_machine1] is \ + inactive when it receives a message."]; + model) + input + graph ;; -let component roll_spec = - let%sub roller_state = roller_state roll_spec in +let component roll_spec graph = + let roller_state = roller_state roll_spec graph in let%arr model, inject = roller_state and roll_spec = roll_spec in let roll_result = diff --git a/examples/rpgdice/bin/roller.mli b/examples/rpgdice/bin/roller.mli index 41888104..c01bbd42 100644 --- a/examples/rpgdice/bin/roller.mli +++ b/examples/rpgdice/bin/roller.mli @@ -1,6 +1,7 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont val component - : Bonsai_web_rpgdice_example.Roll_spec.t Or_error.t Value.t - -> Vdom.Node.t Computation.t + : Bonsai_web_rpgdice_example.Roll_spec.t Or_error.t Bonsai.t + -> Bonsai.graph + -> Vdom.Node.t Bonsai.t diff --git a/examples/rpgdice/bin/string_input.ml b/examples/rpgdice/bin/string_input.ml index a6cf863d..5e65a2ab 100644 --- a/examples/rpgdice/bin/string_input.ml +++ b/examples/rpgdice/bin/string_input.ml @@ -1,6 +1,6 @@ open! Core open! Async_kernel -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module type Conv = sig @@ -10,12 +10,14 @@ module type Conv = sig val to_string_hum : t -> string end -let component (type t) (module Conv : Conv with type t = t) ~default_model = - let%sub text_state = - Bonsai.state - default_model - ~sexp_of_model:[%sexp_of: String.t] - ~equal:[%equal: String.t] +let component (type t) (module Conv : Conv with type t = t) ~default_model graph = + let text_state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state + default_model + ~sexp_of_model:[%sexp_of: String.t] + ~equal:[%equal: String.t] + graph in let%arr text, set_text = text_state in let conv = Or_error.try_with (fun () -> Conv.of_string text) in diff --git a/examples/rpgdice/bin/string_input.mli b/examples/rpgdice/bin/string_input.mli index 78c7875f..63d1574f 100644 --- a/examples/rpgdice/bin/string_input.mli +++ b/examples/rpgdice/bin/string_input.mli @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont module type Conv = sig type t @@ -11,4 +11,5 @@ end val component : (module Conv with type t = 't) -> default_model:string - -> ('t Or_error.t * Vdom.Node.t) Computation.t + -> Bonsai.graph + -> ('t Or_error.t * Vdom.Node.t) Bonsai.t diff --git a/examples/search_bar/main.ml b/examples/search_bar/main.ml index 4d0679e6..1a21ff97 100644 --- a/examples/search_bar/main.ml +++ b/examples/search_bar/main.ml @@ -1,5 +1,5 @@ open! Core -open Bonsai_web +open Bonsai_web.Cont open Bonsai.Let_syntax module User_info = struct @@ -52,7 +52,7 @@ module Input = struct let default () = { all_users = User_info.sample_data } end -let selected_display selected_user = +let selected_display selected_user _graph = match%arr selected_user with | None -> Vdom.Node.div [ Vdom.Node.text "No user selected" ] | Some ({ name; int_id } : User_info.t) -> @@ -63,16 +63,21 @@ let selected_display selected_user = ] ;; -let set_model_component = +let set_model_component graph = let module User_opt = struct type t = User_info.t option [@@deriving equal, sexp] end in - Bonsai.state None ~sexp_of_model:[%sexp_of: User_opt.t] ~equal:[%equal: User_opt.t] + Tuple2.uncurry Bonsai.both + @@ Bonsai.state + None + ~sexp_of_model:[%sexp_of: User_opt.t] + ~equal:[%equal: User_opt.t] + graph ;; -let to_server_input input = - let%sub set_model = set_model_component in +let to_server_input input graph = + let set_model = set_model_component graph in let%arr current_user, inject_set_model = set_model and all_users = input >>| Input.all_users in let choices = all_users |> Map.data |> List.map ~f:Search_bar.Username.of_user_info in @@ -82,16 +87,16 @@ let to_server_input input = current_user, Search_bar.Input.create ~choices ~on_select ;; -let component input = - let%sub current_user, search_bar_input = to_server_input input in - let%sub selected = selected_display current_user in - let%sub search_bar = Search_bar.component search_bar_input in +let component input graph = + let%sub current_user, search_bar_input = to_server_input input graph in + let selected = selected_display current_user graph in + let search_bar = Search_bar.component search_bar_input graph in let%arr selected = selected and search_bar = search_bar in Vdom.Node.div [ search_bar; selected ] ;; let () = - let input = Bonsai.Var.create (Input.default ()) in - Bonsai_web.Start.start (component (Bonsai.Var.value input)) + let input = Bonsai.Expert.Var.create (Input.default ()) in + Bonsai_web.Start.start (component (Bonsai.Expert.Var.value input)) ;; diff --git a/examples/sexp_grammar/main.ml b/examples/sexp_grammar/main.ml index 838d389c..752af14b 100644 --- a/examples/sexp_grammar/main.ml +++ b/examples/sexp_grammar/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax module Auto_generated = Bonsai_web_ui_auto_generated module Form = Bonsai_web_ui_form.With_automatic_view @@ -14,14 +14,14 @@ let generated_values = |> Sequence.to_list ;; -let component = +let component graph = let type_definition = Vdom.Node.pre [ Vdom.Node.text Embedded_files.type_intf_dot_ml ] in - let%sub form = Type.form in - let%sub index, incr = + let form = Type.form graph in + let index, incr = Bonsai.state_machine0 - () + graph ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] ~sexp_of_action:[%sexp_of: Unit.t] diff --git a/examples/snips/main.ml b/examples/snips/main.ml index dbffcc66..9d60da62 100644 --- a/examples/snips/main.ml +++ b/examples/snips/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax module Gallery = Bonsai_web_ui_gallery module Snips = Bonsai_experimental_snips @@ -92,8 +92,8 @@ module Shared_code = struct ; container : Vdom.Node.t -> Vdom.Node.t } - let prepare = - let%sub theme = View.Theme.current in + let prepare graph = + let theme = View.Theme.current graph in let%arr theme = theme in let { View.Constants.intent = { info; error; warning; success } ; primary @@ -142,10 +142,10 @@ module Markup = struct type t = | H of string | P of string - | D of (Vdom.Node.t * string) Computation.t - | Ds of (string option * (Vdom.Node.t * string) Computation.t) list + | D of (Bonsai.graph -> (Vdom.Node.t * string) Bonsai.t) + | Ds of (string option * (Bonsai.graph -> (Vdom.Node.t * string) Bonsai.t)) list - let remove_layout_comments code = + let remove_layout_comments code _graph = let%arr code = code in String.split_lines code |> List.filter ~f:(Fn.non (String.is_substring ~substring:"remove-this-line")) @@ -162,43 +162,46 @@ module Markup = struct | _ -> '_') in let link = Vdom.Node.a ~attrs:[ Vdom.Attr.href ("#" ^ id) ] [ Vdom.Node.text s ] in - Bonsai.const (Vdom.Node.h2 ~attrs:[ Vdom.Attr.id id ] [ link ]) + fun _graph -> Bonsai.return (Vdom.Node.h2 ~attrs:[ Vdom.Attr.id id ] [ link ]) | P s -> - Bonsai.const - (Vdom.Node.inner_html - () - ~attrs:[ Vdom.Attr.empty ] - ~tag:"p" - ~this_html_is_sanitized_and_is_totally_safe_trust_me:s) + fun _graph -> + Bonsai.return + (Vdom.Node.inner_html + () + ~attrs:[ Vdom.Attr.empty ] + ~tag:"p" + ~this_html_is_sanitized_and_is_totally_safe_trust_me:s) | D c -> - let%sub demo, code = c in - let%sub code = remove_layout_comments code in - let%sub gallery = - Gallery.make_demo' ~hide_html:true ~ocaml_label:None ~demo ~code () - in - let%arr { Gallery.demo; code } = gallery in - Vdom.Node.div ~attrs:[ Shared_code.Style.x2_grid ] [ code; demo ] + fun graph -> + let%sub demo, code = c graph in + let code = remove_layout_comments code graph in + let gallery = + Gallery.make_demo' ~hide_html:true ~ocaml_label:None ~demo ~code () graph + in + let%arr { Gallery.demo; code } = gallery in + Vdom.Node.div ~attrs:[ Shared_code.Style.x2_grid ] [ code; demo ] | Ds a -> - a - |> List.map ~f:(fun (ocaml_label, c) -> - let%sub demo, code = c in - let%sub code = remove_layout_comments code in - let%sub r = - Gallery.make_demo' ~hide_html:true ~ocaml_label:None ~demo ~code () - in - let%arr r = r in - ocaml_label, r) - |> Computation.all - |> Computation.map ~f:(fun demos -> - demos - |> List.concat_map ~f:(fun (label, { code; demo }) -> - let pre = - match label with - | None -> [] - | Some label -> [ Vdom.Node.text label; Vdom.Node.div [] ] - in - pre @ [ code; demo ]) - |> Vdom.Node.div ~attrs:[ Shared_code.Style.x2_grid ]) + fun graph -> + a + |> List.map ~f:(fun (ocaml_label, c) -> + let%sub demo, code = c graph in + let code = remove_layout_comments code graph in + let r = + Gallery.make_demo' ~hide_html:true ~ocaml_label:None ~demo ~code () graph + in + let%arr r = r in + ocaml_label, r) + |> Bonsai.all + |> Bonsai.map ~f:(fun demos -> + demos + |> List.concat_map ~f:(fun (label, { code; demo }) -> + let pre = + match label with + | None -> [] + | Some label -> [ Vdom.Node.text label; Vdom.Node.div [] ] + in + pre @ [ code; demo ]) + |> Vdom.Node.div ~attrs:[ Shared_code.Style.x2_grid ]) ;; end @@ -232,8 +235,8 @@ let blue, green, orange = ... (* more of the same *) |} ;; - let view = - let%sub prepared = Shared_code.prepare in + let view graph = + let prepared = Shared_code.prepare graph in let%arr { container; normal; _ } = prepared in let vdom, demo = [%demo @@ -255,8 +258,8 @@ let blue, green, orange = ... (* more of the same *) end module Your_first_snip = struct - let view = - let%sub prepared = Shared_code.prepare in + let view graph = + let prepared = Shared_code.prepare graph in let%arr { container; normal; red; _ } = prepared in let vdom, demo = [%demo @@ -294,8 +297,8 @@ module Your_first_snip = struct end module Composed_snips = struct - let view = - let%sub prepared = Shared_code.prepare in + let view graph = + let prepared = Shared_code.prepare graph in let%arr { container; normal; red; blue; _ } = prepared in let vdom, demo = [%demo @@ -323,8 +326,8 @@ module Composed_snips = struct end module Sideways_snips = struct - let view1 = - let%sub prepared = Shared_code.prepare in + let view1 graph = + let prepared = Shared_code.prepare graph in let%arr { container; normal; red; blue; green; _ } = prepared in let vdom, demo = [%demo @@ -339,8 +342,8 @@ module Sideways_snips = struct container vdom, demo ;; - let alt_1 = - let%sub prepared = Shared_code.prepare in + let alt_1 graph = + let prepared = Shared_code.prepare graph in let%arr { container; normal; red; blue; green; _ } = prepared in let vdom, demo = [%demo @@ -355,8 +358,8 @@ module Sideways_snips = struct container vdom, demo ;; - let alt_2 = - let%sub prepared = Shared_code.prepare in + let alt_2 graph = + let prepared = Shared_code.prepare graph in let%arr { container; normal; red; blue; green; _ } = prepared in let vdom, demo = [%demo @@ -371,8 +374,8 @@ module Sideways_snips = struct container vdom, demo ;; - let alt_3 = - let%sub prepared = Shared_code.prepare in + let alt_3 graph = + let prepared = Shared_code.prepare graph in let%arr { container; normal; red; blue; green; _ } = prepared in let vdom, demo = [%demo @@ -403,8 +406,8 @@ module Sideways_snips = struct end module All_the_sides = struct - let view = - let%sub prepared = Shared_code.prepare in + let view graph = + let prepared = Shared_code.prepare graph in let%arr { container; normal; red; blue; green; orange } = prepared in let vdom, demo = [%demo @@ -430,8 +433,8 @@ module All_the_sides = struct end module Splits = struct - let view = - let%sub prepared = Shared_code.prepare in + let view graph = + let prepared = Shared_code.prepare graph in let%arr { container; red; blue; green; _ } = prepared in let vdom, demo = [%demo @@ -448,8 +451,8 @@ module Splits = struct container vdom, demo ;; - let view2 = - let%sub prepared = Shared_code.prepare in + let view2 graph = + let prepared = Shared_code.prepare graph in let%arr { container; red; green; orange; blue; normal; _ } = prepared in let vdom, demo = [%demo @@ -488,8 +491,8 @@ module Splits = struct end module Splits_on_splits = struct - let view = - let%sub prepared = Shared_code.prepare in + let view graph = + let prepared = Shared_code.prepare graph in let%arr { container; red; normal; orange; blue; _ } = prepared in let vdom, demo = [%demo @@ -509,8 +512,8 @@ module Splits_on_splits = struct container vdom, demo ;; - let view2 = - let%sub prepared = Shared_code.prepare in + let view2 graph = + let prepared = Shared_code.prepare graph in let%arr { container; red; normal; orange; blue; _ } = prepared in let vdom, demo = [%demo @@ -537,13 +540,13 @@ module Splits_on_splits = struct ;; end -let component = +let component graph = let%sub theme, theme_picker = - Gallery.Theme_picker.component ~default:Kado_light ~standalone:false () + Gallery.Theme_picker.component ~default:Kado_light ~standalone:false () graph in let%sub () = Bonsai_extra.exactly_once - (Value.return + (Bonsai.return (Effect.of_sync_fun (fun () -> let open Js_of_ocaml in @@ -553,48 +556,53 @@ let component = Dom_html.window##.location##.hash := Js.string ""; Dom_html.window##.location##.hash := Js.string other) ())) + graph in View.Theme.set_for_app theme - (let%sub nodes = - [ intro - ; Basic.content - ; Your_first_snip.content - ; Composed_snips.content - ; Sideways_snips.content - ; All_the_sides.content - ; Splits.content - ; Splits_on_splits.content - ] - |> List.concat - |> List.map ~f:Markup.to_component - |> Bonsai.Computation.all - in - let%sub attr = - let%arr theme = theme in - let border = - View.extreme_primary_border_color theme |> Css_gen.Color.to_string_css - in - Shared_code.Style.Variables.set - ~header_border:border - ~code_border:border - ~header_bg:((View.extreme_colors theme).background |> Css_gen.Color.to_string_css) - ~code_bg:((View.extreme_colors theme).background |> Css_gen.Color.to_string_css) - ~code_fg:((View.extreme_colors theme).foreground |> Css_gen.Color.to_string_css) - () - in - let%sub body = - Gallery.wrap_application ~theme_picker:(Value.return Vdom.Node.none) nodes - in - let%arr theme_picker = theme_picker - and attr = attr - and body = body in - let header = - Vdom.Node.div - ~attrs:[ Shared_code.Style.header ] - [ Vdom.Node.h1 [ Vdom.Node.text "snips" ]; theme_picker ] - in - Snips.top header |+| Snips.body body |> Snips.render ~container_attr:attr) + (fun graph -> + let nodes = + Bonsai.all + ([ intro + ; Basic.content + ; Your_first_snip.content + ; Composed_snips.content + ; Sideways_snips.content + ; All_the_sides.content + ; Splits.content + ; Splits_on_splits.content + ] + |> List.concat + |> List.map ~f:Markup.to_component + |> List.map ~f:(fun x -> x graph)) + in + let attr = + let%arr theme = theme in + let border = + View.extreme_primary_border_color theme |> Css_gen.Color.to_string_css + in + Shared_code.Style.Variables.set + ~header_border:border + ~code_border:border + ~header_bg: + ((View.extreme_colors theme).background |> Css_gen.Color.to_string_css) + ~code_bg:((View.extreme_colors theme).background |> Css_gen.Color.to_string_css) + ~code_fg:((View.extreme_colors theme).foreground |> Css_gen.Color.to_string_css) + () + in + let body = + Gallery.wrap_application ~theme_picker:(Bonsai.return Vdom.Node.none) nodes graph + in + let%arr theme_picker = theme_picker + and attr = attr + and body = body in + let header = + Vdom.Node.div + ~attrs:[ Shared_code.Style.header ] + [ Vdom.Node.h1 [ Vdom.Node.text "snips" ]; theme_picker ] + in + Snips.top header |+| Snips.body body |> Snips.render ~container_attr:attr) + graph ;; let () = diff --git a/examples/snips_demo/main.ml b/examples/snips_demo/main.ml index e2e5ec23..c9665dcd 100644 --- a/examples/snips_demo/main.ml +++ b/examples/snips_demo/main.ml @@ -1,11 +1,11 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Snips = Bonsai_experimental_snips -let main = +let main graph = let open Snips.Infix in - let%sub theme = View.Theme.current in + let theme = View.Theme.current graph in let%arr theme = theme in let colors = let f = Css_gen.Color.to_string_css in @@ -59,6 +59,6 @@ let main = let () = Async_js.init (); main - |> View.Theme.set_for_app (Value.return (Kado.theme ~version:Bleeding ())) + |> View.Theme.set_for_app (Bonsai.return (Kado.theme ~version:Bleeding ())) |> Bonsai_web.Start.start ;; diff --git a/examples/split_pane/bin/main.ml b/examples/split_pane/bin/main.ml index c06c7692..1af4b943 100644 --- a/examples/split_pane/bin/main.ml +++ b/examples/split_pane/bin/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Async_kernel let () = diff --git a/examples/split_pane/src/bonsai_web_ui_split_pane_example.ml b/examples/split_pane/src/bonsai_web_ui_split_pane_example.ml index 43462c10..177adef7 100644 --- a/examples/split_pane/src/bonsai_web_ui_split_pane_example.ml +++ b/examples/split_pane/src/bonsai_web_ui_split_pane_example.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax open! Vdom module Parameters = Bonsai_web_ui_split_pane.For_testing.Parameters @@ -59,18 +59,19 @@ module Parameters_or_error = struct type t = Parameters.t Or_error.t [@@deriving sexp, equal] end -let create_demo ~parameters = +let create_demo ~parameters graph = let first_panel = - Value.return (Vdom.Node.div ~attrs:[ Styles.first_panel ] [ Vdom.Node.text "1" ]) + Bonsai.return (Vdom.Node.div ~attrs:[ Styles.first_panel ] [ Vdom.Node.text "1" ]) in let second_panel = - Value.return (Vdom.Node.div ~attrs:[ Styles.second_panel ] [ Vdom.Node.text "2" ]) + Bonsai.return (Vdom.Node.div ~attrs:[ Styles.second_panel ] [ Vdom.Node.text "2" ]) in - let%sub pane = + let pane = Bonsai_web_ui_split_pane.For_testing.create_from_parameters parameters ~first_panel ~second_panel + graph in let%arr pane = pane in let view = @@ -79,21 +80,23 @@ let create_demo ~parameters = view, Bonsai_web_ui_split_pane.inject_set_size pane ;; -let create_parameters_form = - let%sub form = Bonsai_web_ui_auto_generated.form (module Parameters) () in - let%sub form = +let create_parameters_form graph = + let form = Bonsai_web_ui_auto_generated.form (module Parameters) () graph in + let form = Bonsai_web_ui_form.With_automatic_view.Dynamic.with_default - (Value.return Parameters.default) + (Bonsai.return Parameters.default) form + graph in - let%sub last_ok = + let last_ok = Bonsai.most_recent_value_satisfying ~sexp_of_model:[%sexp_of: Parameters_or_error.t] ~equal:[%equal: Parameters_or_error.t] (form >>| Form.value) ~condition:(function - | Ok _ -> true - | Error _ -> false) + | Ok _ -> true + | Error _ -> false) + graph in let%arr last_ok = last_ok and form = form in @@ -112,10 +115,10 @@ let create_parameters_form = parameters, view ;; -let app = - let%sub parameters, parameters_form = create_parameters_form in - let%sub demo, inject_set_size = create_demo ~parameters in - let%sub inject_reset = +let app graph = + let%sub parameters, parameters_form = create_parameters_form graph in + let%sub demo, inject_set_size = create_demo ~parameters graph in + let inject_reset = let%arr parameters = parameters and inject_set_size = inject_set_size in inject_set_size parameters.initial_size diff --git a/examples/split_pane/src/bonsai_web_ui_split_pane_example.mli b/examples/split_pane/src/bonsai_web_ui_split_pane_example.mli index 3ac5143c..2b7088b1 100644 --- a/examples/split_pane/src/bonsai_web_ui_split_pane_example.mli +++ b/examples/split_pane/src/bonsai_web_ui_split_pane_example.mli @@ -1,4 +1,4 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -val app : Vdom.Node.t Computation.t +val app : Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/string_duplicator/main.ml b/examples/string_duplicator/main.ml index acb50751..ececfabb 100644 --- a/examples/string_duplicator/main.ml +++ b/examples/string_duplicator/main.ml @@ -1,16 +1,17 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax -let string_duplicator input_string = - let%sub duplication_count_state = - Bonsai.state_machine0 - () - ~sexp_of_model:[%sexp_of: Int.t] - ~equal:[%equal: Int.t] - ~sexp_of_action:[%sexp_of: Unit.t] - ~default_model:1 - ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model () -> model + 1) +let string_duplicator input_string graph = + let duplication_count_state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state_machine0 + graph + ~sexp_of_model:[%sexp_of: Int.t] + ~equal:[%equal: Int.t] + ~sexp_of_action:[%sexp_of: Unit.t] + ~default_model:1 + ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) model () -> model + 1) in let%arr num_duplicated, inject_duplicate = duplication_count_state and input_string = input_string in @@ -24,9 +25,14 @@ let string_duplicator input_string = Vdom.Node.div [ button; Vdom.Node.text repeated_string ] ;; -let string_to_repeat = - let%sub state = - Bonsai.state "hello" ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] +let string_to_repeat graph = + let state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state + "hello" + ~sexp_of_model:[%sexp_of: String.t] + ~equal:[%equal: String.t] + graph in let%arr state, set_state = state in let view = @@ -40,11 +46,13 @@ let string_to_repeat = state, view ;; -let app = +let app (* let%sub can decompose the [(string * Vdom.Node.t) Value.t] into both a - [string Value.t] and a [Vdom.Node.t Value.t]. *) - let%sub string, textbox_view = string_to_repeat in - let%sub duplicated = string_duplicator string in + [string Value.t] and a [Vdom.Node.t Value.t]. *) + graph + = + let%sub string, textbox_view = string_to_repeat graph in + let duplicated = string_duplicator string graph in let%arr textbox_view = textbox_view and duplicated = duplicated in Vdom.Node.div [ textbox_view; duplicated ] diff --git a/examples/styled_components/main.ml b/examples/styled_components/main.ml index 2de6212a..589e95e9 100644 --- a/examples/styled_components/main.ml +++ b/examples/styled_components/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax module Gallery = Bonsai_web_ui_gallery module Form = Bonsai_web_ui_form.With_automatic_view @@ -23,7 +23,7 @@ module Basic = struct ] []] in - Bonsai.const (vdom, demo) + fun _graph -> Bonsai.return (vdom, demo) ;; let selector = None @@ -38,21 +38,21 @@ module Parameters = struct } [@@deriving typed_fields] - open Bonsai.Let_syntax - let tomato_color = `Hex "#ff6347" - let default_int_field = - let%sub form = Form.Elements.Textbox.int ~allow_updates_when_focused:`Never () in - Form.Dynamic.with_default (Value.return 2) form + let default_int_field graph = + let form = Form.Elements.Textbox.int ~allow_updates_when_focused:`Never () graph in + Form.Dynamic.with_default (Bonsai.return 2) form graph ;; - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = function + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> + match typed_field with | Color -> - let%sub form = Form.Elements.Color_picker.hex () in - Form.Dynamic.with_default (Value.return tomato_color) form - | Width -> default_int_field - | Height -> default_int_field + let form = Form.Elements.Color_picker.hex () graph in + Form.Dynamic.with_default (Bonsai.return tomato_color) form graph + | Width -> default_int_field graph + | Height -> default_int_field graph ;; let label_for_field = `Inferred @@ -65,7 +65,7 @@ module Interpolation = struct {|You can interpolate variables using the same syntax as [ppx_string].|} ;; - let view = + let view graph = let f ~color ~width ~height = [%demo Vdom.Node.div @@ -77,8 +77,8 @@ module Interpolation = struct ] []] in - let%sub form = Form.Typed.Record.make (module Parameters) in - let%sub data = + let form = Form.Typed.Record.make (module Parameters) graph in + let data = let%arr form = form in Form.value_or_default form @@ -109,7 +109,7 @@ module Typed_interpolation = struct interpolated variable. It will call that module's [to_string_css]. |} ;; - let view = + let view graph = let f ~color ~width ~height = [%demo Vdom.Node.div @@ -121,8 +121,8 @@ module Typed_interpolation = struct ] []] in - let%sub form = Form.Typed.Record.make (module Parameters) in - let%sub data = + let form = Form.Typed.Record.make (module Parameters) graph in + let data = let%arr form = form in Form.value_or_default form @@ -152,8 +152,8 @@ module Nested_css = struct let name = {|Nested CSS|} let description = {|You can use css's relatively new nesting feature with this too.|} - let view = - Bonsai.const + let view _graph = + Bonsai.return [%demo Vdom.Node.div ~attrs: @@ -181,7 +181,7 @@ module Stylesheet_interpolation = struct {| The [ppx_string] syntax also works on [%css stylesheet], letting you target pseudoselectors+more. |} ;; - let view = + let view graph = let f ~color ~width ~height = [%demo let module Style = @@ -202,8 +202,8 @@ module Stylesheet_interpolation = struct in Vdom.Node.div ~attrs:[ Style.square ] []] in - let%sub form = Form.Typed.Record.make (module Parameters) in - let%sub data = + let form = Form.Typed.Record.make (module Parameters) graph in + let data = let%arr form = form in Form.value_or_default form @@ -229,8 +229,8 @@ module Stylesheet_interpolation = struct let filter_attrs = Some (fun k _ -> not (String.is_prefix k ~prefix:"style")) end -let component = - let%sub theme, theme_picker = Gallery.Theme_picker.component ~default:Kado () in +let component graph = + let%sub theme, theme_picker = Gallery.Theme_picker.component ~default:Kado () graph in View.Theme.set_for_app theme (Gallery.make_sections @@ -245,6 +245,7 @@ let component = ; Gallery.make_demo (module Stylesheet_interpolation) ] ) ]) + graph ;; let () = diff --git a/examples/styled_components_internal/main.ml b/examples/styled_components_internal/main.ml index 16254327..8c657851 100644 --- a/examples/styled_components_internal/main.ml +++ b/examples/styled_components_internal/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax module Gallery = Bonsai_web_ui_gallery @@ -7,7 +7,7 @@ let create ~title ~expect demo = let module Out = struct let name = title let description = expect - let view = Bonsai.const demo + let view _graph = Bonsai.return demo let selector = None let filter_attrs = Some (fun k _ -> not (String.is_prefix k ~prefix:"style")) end @@ -15,8 +15,8 @@ let create ~title ~expect demo = Gallery.make_demo (module Out) ;; -let component = - let%sub theme, theme_picker = Gallery.Theme_picker.component ~default:Kado () in +let component graph = + let%sub theme, theme_picker = Gallery.Theme_picker.component ~default:Kado () graph in View.Theme.set_for_app theme (Gallery.make_sections @@ -149,6 +149,7 @@ let component = ]] ] ) ]) + graph ;; let () = diff --git a/examples/swap_input_node_positions_bug_demo/main.ml b/examples/swap_input_node_positions_bug_demo/main.ml index c2d1f0ce..73eb87ce 100644 --- a/examples/swap_input_node_positions_bug_demo/main.ml +++ b/examples/swap_input_node_positions_bug_demo/main.ml @@ -1,10 +1,10 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax -let text_input = - let%sub text_contents, set_text_contents = - Bonsai.state_opt () ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] +let text_input graph = + let text_contents, set_text_contents = + Bonsai.state_opt graph ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] in let%arr text_contents = text_contents and set_text_contents = set_text_contents in @@ -16,9 +16,9 @@ let text_input = () ;; -let date_input = - let%sub date_contents, set_date_contents = - Bonsai.state_opt () ~sexp_of_model:[%sexp_of: Date.t] ~equal:[%equal: Date.t] +let date_input graph = + let date_contents, set_date_contents = + Bonsai.state_opt graph ~sexp_of_model:[%sexp_of: Date.t] ~equal:[%equal: Date.t] in let%arr date_contents = date_contents and set_date_contents = set_date_contents in @@ -30,9 +30,9 @@ let date_input = () ;; -let text_input_first_input = - let%sub text_input_first_contents, set_text_input_first_contents = - Bonsai.state false ~sexp_of_model:[%sexp_of: Bool.t] ~equal:[%equal: Bool.t] +let text_input_first_input graph = + let text_input_first_contents, set_text_input_first_contents = + Bonsai.state false ~sexp_of_model:[%sexp_of: Bool.t] ~equal:[%equal: Bool.t] graph in let%arr text_input_first_contents = text_input_first_contents and set_text_input_first_contents = set_text_input_first_contents in @@ -50,12 +50,12 @@ let text_input_first_input = () ) ;; -let wrap_in_div nodes = nodes |> Value.all >>| Vdom.Node.div |> return +let wrap_in_div nodes = nodes |> Bonsai.all >>| Vdom.Node.div -let component = - let%sub text_input = text_input in - let%sub date_input = date_input in - let%sub text_input_first, text_input_first_input = text_input_first_input in +let component graph = + let text_input = text_input graph in + let date_input = date_input graph in + let%sub text_input_first, text_input_first_input = text_input_first_input graph in if%sub text_input_first then wrap_in_div [ text_input_first_input; text_input; date_input ] else wrap_in_div [ text_input_first_input; date_input; text_input ] diff --git a/examples/tabs/main.ml b/examples/tabs/main.ml index 75ff8b2f..bd229ae2 100644 --- a/examples/tabs/main.ml +++ b/examples/tabs/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Tabs = Bonsai_web_ui_tabs @@ -11,26 +11,29 @@ module T = struct [@@deriving sexp, equal, compare, enumerate] end -let component = - let%sub tab_state = Tabs.tab_state (module T) ~initial:T.A ~equal:[%equal: T.t] in - let%sub contents = +let component graph = + let tab_state = Tabs.tab_state (module T) ~initial:T.A ~equal:[%equal: T.t] graph in + let contents = Tabs.tab_ui (module T) ~equal:[%equal: T.t] tab_state - ~all_tabs:(Value.return T.all) - ~f:(fun ~change_tab tab -> + ~all_tabs:(Bonsai.return T.all) + ~f:(fun ~change_tab tab graph -> Bonsai.enum (module T) ~match_:tab - ~with_:(function + ~with_:(fun tab _graph -> + match tab with | A -> let%arr change_tab = change_tab in Vdom.Node.button ~attrs:[ Vdom.Attr.on_click (fun _ -> change_tab T.C) ] [ Vdom.Node.text "jump to c" ] - | B -> Bonsai.const (Vdom.Node.text "why are you even here") - | C -> Bonsai.const (Vdom.Node.text "hello!"))) + | B -> Bonsai.return (Vdom.Node.text "why are you even here") + | C -> Bonsai.return (Vdom.Node.text "hello!")) + graph) + graph in let%arr contents = contents in Tabs.Result.combine_trivially contents diff --git a/examples/tailwind_colors/main.ml b/examples/tailwind_colors/main.ml index 9930bd60..fe00bcff 100644 --- a/examples/tailwind_colors/main.ml +++ b/examples/tailwind_colors/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Vdom let brightness_suffix : Tailwind_colors.Brightness.t -> string = function @@ -19,8 +19,8 @@ let abs_position_px t b l r = Css_gen.position ~top:(`Px t) ~bottom:(`Px b) ~left:(`Px l) ~right:(`Px r) `Absolute ;; -let component = - Bonsai.const +let component _graph = + Bonsai.return (Node.div (List.map Tailwind_colors.Hue.all ~f:(fun hue -> Node.div diff --git a/examples/testing_example/lib/app.ml b/examples/testing_example/lib/app.ml index 4bd6f057..8cb7e0b0 100644 --- a/examples/testing_example/lib/app.ml +++ b/examples/testing_example/lib/app.ml @@ -1,16 +1,17 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax (* $MDX part-begin=hello-world-component *) -let hello_world : Vdom.Node.t Computation.t = - Bonsai.const (Vdom.Node.span [ Vdom.Node.text "hello world" ]) +let hello_world : Bonsai.graph -> Vdom.Node.t Bonsai.t = + fun _graph -> Bonsai.return (Vdom.Node.span [ Vdom.Node.text "hello world" ]) ;; (* $MDX part-end *) (* $MDX part-begin=hello-user-component *) -let hello_user (name : string Value.t) : Vdom.Node.t Computation.t = +let hello_user (name : string Bonsai.t) : Bonsai.graph -> Vdom.Node.t Bonsai.t = + fun _graph -> let%arr name = name in Vdom.Node.span [ Vdom.Node.textf "hello %s" name ] ;; @@ -18,11 +19,12 @@ let hello_user (name : string Value.t) : Vdom.Node.t Computation.t = (* $MDX part-end *) (* $MDX part-begin=hello-text-box-component *) -let hello_textbox : Vdom.Node.t Computation.t = - let%sub state, set = - Bonsai.state "" ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] +let hello_textbox : Bonsai.graph -> Vdom.Node.t Bonsai.t = + fun graph -> + let state, set = + Bonsai.state "" ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] graph in - let%sub message = hello_user state in + let message = hello_user state graph in let%arr message = message and set = set in Vdom.Node.div diff --git a/examples/testing_example/lib/app.mli b/examples/testing_example/lib/app.mli index a125f255..b5adf475 100644 --- a/examples/testing_example/lib/app.mli +++ b/examples/testing_example/lib/app.mli @@ -1,6 +1,6 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -val hello_world : Vdom.Node.t Computation.t -val hello_user : string Value.t -> Vdom.Node.t Computation.t -val hello_textbox : Vdom.Node.t Computation.t +val hello_world : Bonsai.graph -> Vdom.Node.t Bonsai.t +val hello_user : string Bonsai.t -> Bonsai.graph -> Vdom.Node.t Bonsai.t +val hello_textbox : Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/testing_example/test/app_test.ml b/examples/testing_example/test/app_test.ml index af021518..c7953b77 100644 --- a/examples/testing_example/test/app_test.ml +++ b/examples/testing_example/test/app_test.ml @@ -1,6 +1,6 @@ open! Core open! Bonsai_web_test -open! Bonsai_web +open! Bonsai_web.Cont let hello_world = Bonsai_testing_example_lib.hello_world let hello_user = Bonsai_testing_example_lib.hello_user @@ -20,12 +20,12 @@ let%expect_test "it shows hello world" = (* $MDX part-begin=hello-user-test *) let%expect_test "shows hello to a user" = - let user_var = Bonsai.Var.create "Bob" in - let user = Bonsai.Var.value user_var in + let user_var = Bonsai.Expert.Var.create "Bob" in + let user = Bonsai.Expert.Var.value user_var in let handle = Handle.create (Result_spec.vdom Fn.id) (hello_user user) in Handle.show handle; [%expect {| hello Bob |}]; - Bonsai.Var.set user_var "Alice"; + Bonsai.Expert.Var.set user_var "Alice"; Handle.show handle; [%expect {| hello Alice |}] ;; @@ -34,12 +34,12 @@ let%expect_test "shows hello to a user" = (* $MDX part-begin=hello-user-diff-test *) let%expect_test "shows hello to a user" = - let user_var = Bonsai.Var.create "Bob" in - let user = Bonsai.Var.value user_var in + let user_var = Bonsai.Expert.Var.create "Bob" in + let user = Bonsai.Expert.Var.value user_var in let handle = Handle.create (Result_spec.vdom Fn.id) (hello_user user) in Handle.show handle; [%expect {| hello Bob |}]; - Bonsai.Var.set user_var "Alice"; + Bonsai.Expert.Var.set user_var "Alice"; Handle.show_diff handle; [%expect {| -| hello Bob @@ -94,8 +94,14 @@ module State_view_spec = struct end let%expect_test "test Bonsai.state" = - let component : (string * (string -> unit Vdom.Effect.t)) Computation.t = - Bonsai.state "hello" ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] + let component : Bonsai.graph -> (string * (string -> unit Vdom.Effect.t)) Bonsai.t = + fun graph -> + Tuple2.uncurry Bonsai.both + @@ Bonsai.state + "hello" + ~sexp_of_model:[%sexp_of: String.t] + ~equal:[%equal: String.t] + graph in let handle = Handle.create (module State_view_spec) component in Handle.show handle; diff --git a/examples/time/src/bonsai_time_example.ml b/examples/time/src/bonsai_time_example.ml index 83690b64..c67f614c 100644 --- a/examples/time/src/bonsai_time_example.ml +++ b/examples/time/src/bonsai_time_example.ml @@ -1,5 +1,5 @@ open! Core -open Bonsai_web +open Bonsai_web.Cont open Bonsai.Let_syntax (* $MDX part-begin=untestable-clock-component *) @@ -11,9 +11,9 @@ let _untestable_component = (* $MDX part-end *) (* $MDX part-begin=testable-clock-component *) -let component = - let%sub now = Bonsai.Incr.with_clock Bonsai.Time_source.watch_now in - return (now >>| Time_ns.to_string_utc >>| Vdom.Node.text) +let component graph = + let now = Bonsai.Incr.with_clock ~f:Bonsai.Time_source.watch_now graph in + now >>| Time_ns.to_string_utc >>| Vdom.Node.text ;; (* $MDX part-end *) diff --git a/examples/time/src/bonsai_time_example.mli b/examples/time/src/bonsai_time_example.mli index c6b6def1..6f51d202 100644 --- a/examples/time/src/bonsai_time_example.mli +++ b/examples/time/src/bonsai_time_example.mli @@ -1,3 +1,3 @@ -open Bonsai_web +open Bonsai_web.Cont -val component : Vdom.Node.t Computation.t +val component : Bonsai.graph -> Vdom.Node.t Bonsai.t diff --git a/examples/time/test/app_test.ml b/examples/time/test/app_test.ml index 86dab60a..c9843dd8 100644 --- a/examples/time/test/app_test.ml +++ b/examples/time/test/app_test.ml @@ -1,6 +1,6 @@ open! Core open! Bonsai_web_test -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai_time_example (* $MDX part-begin=test-clock-component *) diff --git a/examples/timetravel/main.ml b/examples/timetravel/main.ml index 03c47ca0..69f57cbb 100644 --- a/examples/timetravel/main.ml +++ b/examples/timetravel/main.ml @@ -1,5 +1,5 @@ open! Core -open Bonsai_web +open Bonsai_web.Cont let timetraveled_component = Bonsai.map diff --git a/examples/timetravel/spacetime.ml b/examples/timetravel/spacetime.ml index 82e9e1f7..756051fc 100644 --- a/examples/timetravel/spacetime.ml +++ b/examples/timetravel/spacetime.ml @@ -1,5 +1,5 @@ open! Core -open Bonsai_web +open Bonsai_web.Cont module Model = struct type 'm t = diff --git a/examples/timetravel/spacetime.mli b/examples/timetravel/spacetime.mli index 234d2d81..026672bc 100644 --- a/examples/timetravel/spacetime.mli +++ b/examples/timetravel/spacetime.mli @@ -1,5 +1,5 @@ open! Core -open Bonsai_web +open Bonsai_web.Cont module Model : sig type 'model t diff --git a/examples/todomvc/main.ml b/examples/todomvc/main.ml index 3ca6e410..198f4d54 100644 --- a/examples/todomvc/main.ml +++ b/examples/todomvc/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax open! Vdom module Style = Todomvc @@ -8,12 +8,12 @@ module Style = Todomvc to use Bonsai_web_ui_url_var. As of this writing, that module does not support tracking URL Fragments. As a lightweight workaround, I created the Url_hash module. *) module Url_hash : sig - val get : unit -> string Value.t + val get : unit -> string Bonsai.t end = struct (* is_watching ensures we only add one event listener over the lifetime of the app. *) let is_watching = ref false - let hash_var = Bonsai.Var.create "/" - let hash_val = Bonsai.Var.value hash_var + let hash_var = Bonsai.Expert.Var.create "/" + let hash_val = Bonsai.Expert.Var.value hash_var let on_hash_change (f : string -> unit) = let open Js_of_ocaml in @@ -30,7 +30,7 @@ end = struct let get () = if not !is_watching then ( - let _discarded_id = on_hash_change (Bonsai.Var.set hash_var) in + let _discarded_id = on_hash_change (Bonsai.Expert.Var.set hash_var) in is_watching := true); hash_val ;; @@ -127,9 +127,9 @@ let apply_action context (model : Model.t) (action : Action.t) = new_model ;; -let header_component ~inject = - let%sub state, set_state = - Bonsai.state "" ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] +let header_component ~inject graph = + let state, set_state = + Bonsai.state "" ~sexp_of_model:[%sexp_of: String.t] ~equal:[%equal: String.t] graph in let%arr state = state and set_state = set_state @@ -156,11 +156,12 @@ let header_component ~inject = ;; let todo_item_component - (todo : Model.todo Value.t) - ~(inject : (Action.t -> unit Effect.t) Value.t) + (todo : Model.todo Bonsai.t) + ~(inject : (Action.t -> unit Effect.t) Bonsai.t) + graph = - let%sub editing, set_editing = - Bonsai.state false ~sexp_of_model:[%sexp_of: Bool.t] ~equal:[%equal: Bool.t] + let editing, set_editing = + Bonsai.state false ~sexp_of_model:[%sexp_of: Bool.t] ~equal:[%equal: Bool.t] graph in let%arr inject = inject and todo = todo @@ -229,7 +230,7 @@ let todo_item_component [ view; task_name_input ] ;; -let todo_list (model : Model.t Value.t) ~inject = +let todo_list (model : Model.t Bonsai.t) ~inject graph = let filtered_model = let%map model = model and hash = Url_hash.get () in @@ -243,11 +244,12 @@ let todo_list (model : Model.t Value.t) ~inject = let%map model = model in Map.count model ~f:(fun todo -> not todo.completed) in - let%sub todo_items = + let todo_items = Bonsai.assoc (module Int) filtered_model ~f:(fun _id todo -> todo_item_component ~inject todo) + graph in let%arr todo_items = todo_items and active_count = active_count @@ -281,8 +283,9 @@ let todo_list (model : Model.t Value.t) ~inject = let pluralize count word = if count > 1 then word ^ "s" else word let footer_component - (state : Model.t Value.t) - ~(inject : (Action.t -> unit Effect.t) Value.t) + (state : Model.t Bonsai.t) + ~(inject : (Action.t -> unit Effect.t) Bonsai.t) + _graph = let%arr inject = inject and active, completed = @@ -337,20 +340,20 @@ let info = ] ;; -let root_component = +let root_component graph = let default_model = Bonsai_web.Persistent_var.get persisted_model in - let%sub state, inject = + let state, inject = Bonsai.state_machine0 - () + graph ~sexp_of_model:[%sexp_of: Model.t] ~sexp_of_action:[%sexp_of: Action.t] ~default_model ~apply_action ~equal:[%equal: Model.t] in - let%sub header_component = header_component ~inject in - let%sub todo_list = todo_list ~inject state in - let%sub footer_component = footer_component state ~inject in + let header_component = header_component ~inject graph in + let todo_list = todo_list ~inject state graph in + let footer_component = footer_component state ~inject graph in let%arr header_component = header_component and todo_list = todo_list and footer_component = footer_component in diff --git a/examples/treemapviz/main.ml b/examples/treemapviz/main.ml index 4011e72c..7eb303d5 100644 --- a/examples/treemapviz/main.ml +++ b/examples/treemapviz/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax let percent_range_generator = Base_quickcheck.Generator.float_inclusive (-2.8) 2.8 @@ -222,30 +222,30 @@ module Dimensions = struct [@@deriving sexp, equal] end -let create_treemap ~elements = - let%sub dimensions, set_dimensions = +let create_treemap ~elements graph = + let dimensions, set_dimensions = Bonsai.state_opt - () + graph ~sexp_of_model:[%sexp_of: Dimensions.t] ~equal:[%equal: Dimensions.t] in - let%sub tracker = + let tracker = let%arr set_dimensions = set_dimensions in Bonsai_web_ui_element_size_hooks.Size_tracker.on_change (fun ~width ~height -> set_dimensions (Some { Dimensions.height; width })) in - let%sub treemap = + let treemap = match%sub dimensions with - | None -> Bonsai.const Vdom.Node.none + | None -> Bonsai.return Vdom.Node.none | Some dimensions -> - let%sub dimensions = + let dimensions = let%arr dimensions = dimensions in { Dimensions.height = Float.round_nearest_half_to_even dimensions.height ; width = Float.round_up dimensions.width } in - let dimensions = Value.cutoff dimensions ~equal:[%equal: Dimensions.t] in - let%sub treemap = + let dimensions = Bonsai.cutoff dimensions ~equal:[%equal: Dimensions.t] in + let treemap = let%arr { width; height } = dimensions in Bonsai_experimental_treemapviz.create ~width @@ -281,14 +281,14 @@ let create_treemap ~elements = Vdom.Node.div ~attrs:[ tracker ] [ treemap ] ;; -let component = - let%sub stress_state, toggle_stress = Bonsai.toggle ~default_model:false in - let%sub content = +let component graph = + let stress_state, toggle_stress = Bonsai.toggle ~default_model:false graph in + let content = match%sub stress_state with - | false -> create_treemap ~elements:life_elements - | true -> create_treemap ~elements:stress_elements + | false -> create_treemap ~elements:life_elements graph + | true -> create_treemap ~elements:stress_elements graph in - let%sub button = + let button = let%arr stress_state = stress_state and toggle_stress = toggle_stress in let text = diff --git a/examples/two_instances_of_component/main.ml b/examples/two_instances_of_component/main.ml index 114a18ba..68a05788 100644 --- a/examples/two_instances_of_component/main.ml +++ b/examples/two_instances_of_component/main.ml @@ -1,10 +1,11 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax -let counter = - let%sub state = - Bonsai.state 0 ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] +let counter graph = + let state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state 0 ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] graph in let%arr current_value, set_value = state in Vdom.Node.div @@ -18,9 +19,9 @@ let counter = ] ;; -let two_counters = - let%sub counter_1 = counter in - let%sub counter_2 = counter in +let two_counters graph = + let counter_1 = counter graph in + let counter_2 = counter graph in let%arr counter_1 = counter_1 and counter_2 = counter_2 in Vdom.Node.div [ counter_1; counter_2 ] @@ -28,9 +29,9 @@ let two_counters = (* Note: because neither component that comprises [two_counters] depends on one another, it could instead be written using computation's let-syntax, like so *) -let _two_counters__computation_map_style = - let%map.Computation counter_1 = counter - and counter_2 = counter in +let _two_counters__computation_map_style graph = + let%map counter_1 = counter graph + and counter_2 = counter graph in Vdom.Node.div [ counter_1; counter_2 ] ;; diff --git a/examples/typeahead/main.ml b/examples/typeahead/main.ml index 958085ad..b2c1cc37 100644 --- a/examples/typeahead/main.ml +++ b/examples/typeahead/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont module Pokemon = struct module T = struct @@ -42,14 +42,16 @@ end (* thanks to the good folks in webdev-public, you can no longer fool this into letting you choose a pokemon as your favourite if its already your not favourite! *) -let components = +let components graph = let open! Bonsai.Let_syntax in let open! Bonsai_web_ui_typeahead in - let%sub all_options = - Bonsai.state - Pokemon.all - ~sexp_of_model:[%sexp_of: Pokemon.t list] - ~equal:[%equal: Pokemon.t list] + let all_options = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state + Pokemon.all + ~sexp_of_model:[%sexp_of: Pokemon.t list] + ~equal:[%equal: Pokemon.t list] + graph in let typeahead_single ?handle_unknown_option () = Typeahead.create @@ -63,20 +65,20 @@ let components = List.filter Pokemon.all ~f:(fun pokemon -> not (Pokemon.equal favourite_pokemon pokemon))) |> inject_all_options) - ~to_string:(Bonsai.Value.return Pokemon.to_string) + ~to_string:(Bonsai.return Pokemon.to_string) ~placeholder:"Select a pokemon" ?handle_unknown_option - ~all_options:(Value.return Pokemon.all) + ~all_options:(Bonsai.return Pokemon.all) (module Pokemon) ~equal:[%equal: Pokemon.t] in let%sub { selected = favourite_pokemon; view = typeahead_single_vdom; _ } = - typeahead_single () + typeahead_single () graph in let%sub { view = typeahead_multi_vdom; _ } = Typeahead.create_multi (module Pokemon) - ~to_string:(Value.return Pokemon.to_string) + ~to_string:(Bonsai.return Pokemon.to_string) ~on_set_change: (let%map inject_all_options = all_options >>| snd and favourite_pokemon = favourite_pokemon in @@ -90,30 +92,32 @@ let components = Set.diff all_pokemon not_good_pokemon |> Set.to_list |> inject_all_options) ~placeholder:"Select many pokemon" ~all_options:(all_options >>| fst) + graph in let%sub { view = typeahead_single_with_custom_input_vdom; _ } = typeahead_single ~handle_unknown_option: - (Value.return (fun input -> + (Bonsai.return (fun input -> Option.some_if (Int.equal 5 (String.length input)) (Pokemon.of_string input))) () + graph in let typeahead_multi_with_custom_input ~all_options = Typeahead.create_multi - ~to_string:(Value.return Pokemon.to_string) + ~to_string:(Bonsai.return Pokemon.to_string) ~placeholder:"Select many pokemon" ~handle_unknown_option: - (Value.return (fun input -> + (Bonsai.return (fun input -> (* custom [handle_unknown_option] that does a check on unknown inputs *) Option.some_if (String.contains ~pos:0 input 'B') (Pokemon.of_string input))) ~all_options (module Pokemon) in let%sub { view = typeahead_multi_with_empty_options_vdom; _ } = - typeahead_multi_with_custom_input ~all_options:(Value.return []) + typeahead_multi_with_custom_input ~all_options:(Bonsai.return []) graph in let%sub { view = typeahead_multi_with_custom_input_vdom; _ } = - typeahead_multi_with_custom_input ~all_options:(Value.return Pokemon.all) + typeahead_multi_with_custom_input ~all_options:(Bonsai.return Pokemon.all) graph in let%arr typeahead_single_vdom = typeahead_single_vdom and typeahead_multi_vdom = typeahead_multi_vdom diff --git a/examples/url_var/bin/main.ml b/examples/url_var/bin/main.ml index b13c726c..3f199e22 100644 --- a/examples/url_var/bin/main.ml +++ b/examples/url_var/bin/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Css = @@ -17,8 +17,8 @@ stylesheet let c s = s let examples_column = c Css.examples_column -let component = - let%sub examples = Bonsai_web_ui_url_var_example_urls.examples in +let component graph = + let examples = Bonsai_web_ui_url_var_example_urls.examples graph in let%arr examples = examples in Vdom.Node.div [ Vdom.Node.div ~attrs:[ examples_column ] examples ] ;; diff --git a/examples/url_var/lib/bonsai_web_ui_url_var_example_urls.mli b/examples/url_var/lib/bonsai_web_ui_url_var_example_urls.mli index 2ee974f0..d28855b4 100644 --- a/examples/url_var/lib/bonsai_web_ui_url_var_example_urls.mli +++ b/examples/url_var/lib/bonsai_web_ui_url_var_example_urls.mli @@ -1,4 +1,4 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -val examples : Vdom.Node.t list Bonsai.Computation.t +val examples : Bonsai.graph -> Vdom.Node.t list Bonsai.t diff --git a/examples/url_var/lib/url_example.ml b/examples/url_var/lib/url_example.ml index b7cadbaa..5a75c837 100644 --- a/examples/url_var/lib/url_example.ml +++ b/examples/url_var/lib/url_example.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Url_var = Bonsai_web_ui_url_var module Form = Bonsai_web_ui_form.With_automatic_view @@ -118,13 +118,14 @@ button:hover { |}] (* This form is the one that reads/write the URI. *) -let uri_form ~default = - let%sub form = - let%sub form = +let uri_form ~default graph = + let form = + let form = Form.Elements.Textbox.string ~allow_updates_when_focused:`Never - ~extra_attrs:(Value.return [ Css.uri_input ]) + ~extra_attrs:(Bonsai.return [ Css.uri_input ]) () + graph in let%arr form = form in let uri_form = @@ -136,7 +137,7 @@ let uri_form ~default = ~parse_exn:Url_var.Components.of_uri ~unparse:Url_var.Components.to_path_and_query in - Form.Dynamic.with_default default form + Form.Dynamic.with_default default form graph ;; (* This form is the one that reads/writes the parsed sexp. *) @@ -146,13 +147,15 @@ let typed_url_form ~parser (module M : Sexpable with type t = a) ~fallback + graph = - let%sub form = - let%sub form = + let form = + let form = Form.Elements.Textbox.sexpable ~allow_updates_when_focused:`Never - ~extra_attrs:(Value.return [ Css.sexp_input ]) + ~extra_attrs:(Bonsai.return [ Css.sexp_input ]) (module M) + graph in let%arr form = form in Form.project @@ -175,10 +178,10 @@ let typed_url_form try (Projection.parse_exn parser typed_components).result with | _ -> fallback) in - Form.Dynamic.with_default default form + Form.Dynamic.with_default default form graph ;; -let component (type a) (t : a t) = +let component (type a) (t : a t) graph = let did_fallback_occur ~components_value = match components_value with | Error _ -> false @@ -204,7 +207,7 @@ let component (type a) (t : a t) = in Url_var.Components.equal fallback_components components in - let%sub uri_form = uri_form ~default:(Value.return t.starting_components) in + let uri_form = uri_form ~default:(Bonsai.return t.starting_components) graph in let uri_form_value = let%map uri_form = uri_form in Form.value uri_form @@ -223,12 +226,13 @@ let component (type a) (t : a t) = | Ok x -> Form.set uri_form x | Error _ -> Effect.return () in - let%sub typed_url_form = + let typed_url_form = typed_url_form - ~default:(Value.return t.starting_components) + ~default:(Bonsai.return t.starting_components) ~parser:t.parser t.type_ ~fallback:t.fallback + graph in let typed_url_form_value = let%map typed_url_form = typed_url_form in @@ -254,6 +258,7 @@ let component (type a) (t : a t) = ~store_value:typed_url_form_value ~interactive_set:uri_form_set ~interactive_value:uri_form_value + graph in let%arr uri_form = uri_form and typed_url_form = typed_url_form @@ -683,7 +688,7 @@ let%expect_test _ = |}] ;;] -let error_example_component = +let error_example_component _graph = let out = Vdom.Node.div ~attrs:[ Css.paper; Css.column ] @@ -702,7 +707,7 @@ let error_example_component = ] ] in - Bonsai.const out + Bonsai.return out ;; module Simple_record_example = @@ -920,10 +925,10 @@ let catchall_example = } ;; -let examples = +let examples graph = List.map [ T reading_from_query; T reading_from_path ] ~f:(fun (T example) -> - component example) - @ [ error_example_component ] + component example graph) + @ [ error_example_component graph ] @ List.map [ T foo_bar_example ; T simple_record_example @@ -934,8 +939,8 @@ let examples = ; T folder_example ; T catchall_example ] - ~f:(fun (T example) -> component example) - |> Computation.all + ~f:(fun (T example) -> component example graph) + |> Bonsai.all ;; (* $MDX part-begin=search_example *) diff --git a/examples/url_var/lib/url_example.mli b/examples/url_var/lib/url_example.mli index 2ee974f0..d28855b4 100644 --- a/examples/url_var/lib/url_example.mli +++ b/examples/url_var/lib/url_example.mli @@ -1,4 +1,4 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont -val examples : Vdom.Node.t list Bonsai.Computation.t +val examples : Bonsai.graph -> Vdom.Node.t list Bonsai.t diff --git a/examples/url_var_all_features/bin/main.ml b/examples/url_var_all_features/bin/main.ml index 05c8b612..b4d13df2 100644 --- a/examples/url_var_all_features/bin/main.ml +++ b/examples/url_var_all_features/bin/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont module Lib = All_url_var_features_example let url_var = diff --git a/examples/url_var_all_features/lib/all_url_var_features_example.ml b/examples/url_var_all_features/lib/all_url_var_features_example.ml index b21c6126..8fe21293 100644 --- a/examples/url_var_all_features/lib/all_url_var_features_example.ml +++ b/examples/url_var_all_features/lib/all_url_var_features_example.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Url_var = Bonsai_web_ui_url_var open Url_var.Typed @@ -21,14 +21,14 @@ module Location = struct | Y -> from_query_optional_with_default ~equal:Int.equal int ~default:100 ;; - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = let open Form.Elements.Textbox in function | X -> int ~allow_updates_when_focused:`Never () | Y -> int ~allow_updates_when_focused:`Never () ;; - let form_of_t : t Form.t Computation.t = + let form_of_t : Bonsai.graph -> t Form.t Bonsai.t = Form.Typed.Record.make (module struct module Typed_field = Typed_field @@ -70,33 +70,35 @@ module Record = struct | Remaining_words_on_path -> with_prefix [] (from_remaining_path string) ;; - let form_of_t : t Form.t Computation.t = + let form_of_t : Bonsai.graph -> t Form.t Bonsai.t = + fun graph -> Form.Typed.Record.make (module struct module Typed_field = Typed_field - let form_for_field : type a. a Typed_field.t -> a Form.t Computation.t = + let form_for_field : type a. a Typed_field.t -> Bonsai.graph -> a Form.t Bonsai.t = + fun typed_field graph -> let open Form.Elements.Textbox in let open Form.Elements in - function - | An_int -> int ~allow_updates_when_focused:`Never () - | Many_floats -> Multiple.list (float ~allow_updates_when_focused:`Never ()) + match typed_field with + | An_int -> int ~allow_updates_when_focused:`Never () graph + | Many_floats -> + Multiple.list (float ~allow_updates_when_focused:`Never ()) graph | Optional_string -> - let%sub form = string ~allow_updates_when_focused:`Never () in + let form = string ~allow_updates_when_focused:`Never () graph in let%arr form = form in Form.optional form ~is_some:(fun x -> not (String.equal x "")) ~none:"" - | Many_locations -> - let location_form = Location.form_of_t in - Multiple.list location_form - | Nested -> Location.form_of_t - | Username_on_path -> string ~allow_updates_when_focused:`Never () - | Comment_id_on_path -> int ~allow_updates_when_focused:`Never () + | Many_locations -> Multiple.list Location.form_of_t graph + | Nested -> Location.form_of_t graph + | Username_on_path -> string ~allow_updates_when_focused:`Never () graph + | Comment_id_on_path -> int ~allow_updates_when_focused:`Never () graph | Remaining_words_on_path -> - Multiple.list (string ~allow_updates_when_focused:`Never ()) + Multiple.list (string ~allow_updates_when_focused:`Never ()) graph ;; let label_for_field = `Inferred end) + graph ;; module Path_order = Path_order (Typed_field) @@ -122,10 +124,13 @@ module Variant = struct (module struct module Typed_variant = Typed_variant - let form_for_variant : type a. a Typed_variant.t -> a Form.t Computation.t - = function - | Post -> Bonsai.const (Form.return ()) - | Comments -> Bonsai.const (Form.return ()) + let form_for_variant + : type a. a Typed_variant.t -> Bonsai.graph -> a Form.t Bonsai.t + = + fun typed_field _graph -> + match typed_field with + | Post -> Bonsai.return (Form.return ()) + | Comments -> Bonsai.return (Form.return ()) ;; let label_for_variant = `Inferred @@ -154,11 +159,14 @@ module Query_variant = struct (module struct module Typed_variant = Typed_variant - let form_for_variant : type a. a Typed_variant.t -> a Form.t Computation.t - = function - | A -> Form.Elements.Textbox.int ~allow_updates_when_focused:`Never () - | B -> Form.Elements.Textbox.float ~allow_updates_when_focused:`Never () - | C -> Form.Elements.Textbox.string ~allow_updates_when_focused:`Never () + let form_for_variant + : type a. a Typed_variant.t -> Bonsai.graph -> a Form.t Bonsai.t + = + fun typed_field graph -> + match typed_field with + | A -> Form.Elements.Textbox.int ~allow_updates_when_focused:`Never () graph + | B -> Form.Elements.Textbox.float ~allow_updates_when_focused:`Never () graph + | C -> Form.Elements.Textbox.string ~allow_updates_when_focused:`Never () graph ;; let label_for_variant = `Inferred @@ -186,17 +194,20 @@ module T = struct | Unable_to_parse -> Parser.with_remaining_path [ "unable" ] Parser.unit ;; - let form_of_t = + let form_of_t graph = Form.Typed.Variant.make (module struct module Typed_variant = Typed_variant - let form_for_variant : type a. a Typed_variant.t -> a Form.t Computation.t - = function - | Homepage -> Bonsai.const (Form.return ()) + let form_for_variant + : type a. a Typed_variant.t -> Bonsai.graph -> a Form.t Bonsai.t + = + fun typed_field graph -> + match typed_field with + | Homepage -> Bonsai.return (Form.return ()) | Some_string_option -> - let%sub text = - Form.Elements.Textbox.string ~allow_updates_when_focused:`Never () + let text = + Form.Elements.Textbox.string ~allow_updates_when_focused:`Never () graph in let%arr text = text in Form.optional @@ -205,15 +216,16 @@ module T = struct | "" -> false | _ -> true) ~none:"" - | Query_variant -> Query_variant.form_of_t - | Variant -> Variant.form_of_t - | Record -> Record.form_of_t - | Unable_to_parse -> Bonsai.const (Form.return ()) + | Query_variant -> Query_variant.form_of_t graph + | Variant -> Variant.form_of_t graph + | Record -> Record.form_of_t graph + | Unable_to_parse -> Bonsai.return (Form.return ()) ;; let label_for_variant = `Inferred let initial_choice = `First_constructor end) + graph ;; end @@ -245,16 +257,16 @@ let%expect_test _ = let fallback _exn _components = T.Unable_to_parse -let component ~url_var = +let component ~url_var graph = let url_value = Url_var.value url_var in - let%sub modify_history, toggle_modify_history = Bonsai.toggle ~default_model:true in - let%sub set_url_var = + let modify_history, toggle_modify_history = Bonsai.toggle ~default_model:true graph in + let set_url_var = let%arr modify_history = modify_history in let how = if modify_history then `Push else `Replace in fun query -> Url_var.set_effect url_var query ~how in - let%sub form = T.form_of_t in - let%sub store_value = + let form = T.form_of_t graph in + let store_value = let%arr url_value = url_value in Some url_value in @@ -265,9 +277,10 @@ let component ~url_var = ~store_value ~store_set:set_url_var form + graph in - let%sub update_button = - let%sub update_effect = + let update_button = + let update_effect = let%arr modify_history = modify_history in let how = if modify_history then `Push else `Replace in fun ~f -> Url_var.update_effect url_var ~how ~f diff --git a/examples/url_var_all_features/lib/all_url_var_features_example.mli b/examples/url_var_all_features/lib/all_url_var_features_example.mli index 38dd8d1a..922ba899 100644 --- a/examples/url_var_all_features/lib/all_url_var_features_example.mli +++ b/examples/url_var_all_features/lib/all_url_var_features_example.mli @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont module Url_var = Bonsai_web_ui_url_var module T : sig @@ -8,4 +8,8 @@ end val fallback : Exn.t -> Bonsai_web_ui_url_var.Components.t -> T.t val versioned_parser : T.t Bonsai_web_ui_url_var.Typed.Versioned_parser.t -val component : url_var:T.t Bonsai_web_ui_url_var.t -> Vdom.Node.t Computation.t + +val component + : url_var:T.t Bonsai_web_ui_url_var.t + -> Bonsai.graph + -> Vdom.Node.t Bonsai.t diff --git a/examples/use_ocamlgraph/main.ml b/examples/use_ocamlgraph/main.ml index c29f4d5c..44ae23c9 100644 --- a/examples/use_ocamlgraph/main.ml +++ b/examples/use_ocamlgraph/main.ml @@ -1,6 +1,6 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont module _ = Graph -let component = Bonsai.const (Vdom.Node.text "hello world") +let component _graph = Bonsai.return (Vdom.Node.text "hello world") let () = Bonsai_web.Start.start component diff --git a/examples/use_tracing/main.ml b/examples/use_tracing/main.ml index 8bad807c..5a95bc30 100644 --- a/examples/use_tracing/main.ml +++ b/examples/use_tracing/main.ml @@ -1,9 +1,9 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont module _ = Tracing_zero -let[@trace "hi"] component = - Bonsai.const +let[@trace "hi"] component _graph = + Bonsai.return (Vdom.Node.text (sprintf "Time_stamp_counter.now: %Ld\n" diff --git a/examples/vdom_input_widgets_int_repro/main.ml b/examples/vdom_input_widgets_int_repro/main.ml index f7ef6469..a6902fa4 100644 --- a/examples/vdom_input_widgets_int_repro/main.ml +++ b/examples/vdom_input_widgets_int_repro/main.ml @@ -1,16 +1,24 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax +module type S = sig + type t [@@deriving sexp_of] +end + let component (type a) - (module M : Bonsai.Model with type t = a) + (module M : S with type t = a) ~equal ~name ~some_constant_value ~node_creator + graph = - let%sub textbox_state = Bonsai.state_opt () ~sexp_of_model:[%sexp_of: M.t] ~equal in + let textbox_state = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state_opt graph ~sexp_of_model:[%sexp_of: M.t] ~equal + in let%arr state, set_state = textbox_state in let input = node_creator state set_state in let debug = state |> [%sexp_of: M.t option] |> Sexp.to_string_hum |> Vdom.Node.text in @@ -27,8 +35,8 @@ let component Vdom.Node.div [ Vdom.Node.text name; input; clear_button; set_constant; debug ] ;; -let component = - let%sub number_input = +let component graph = + let number_input = component (module Int) ~equal:[%equal: Int.t] @@ -42,8 +50,9 @@ let component = ~value ~on_input ~step:1.) + graph in - let%sub string_input = + let string_input = component (module String) ~equal:[%equal: String.t] @@ -56,6 +65,7 @@ let component = ~on_input ~allow_updates_when_focused:`Never ()) + graph in let%arr number_input = number_input and string_input = string_input in diff --git a/examples/vdom_keyboard/main.ml b/examples/vdom_keyboard/main.ml index a4eb9d76..3bca4d95 100644 --- a/examples/vdom_keyboard/main.ml +++ b/examples/vdom_keyboard/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax open Vdom_keyboard @@ -33,23 +33,24 @@ stylesheet } |}] -let offset_state_machine = - Bonsai.state_machine0 - () - ~sexp_of_model:[%sexp_of: Int.t] - ~equal:[%equal: Int.t] - ~sexp_of_action:[%sexp_of: Int.t] - ~default_model:400 - ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) x delta -> x + delta) +let offset_state_machine graph = + Tuple2.uncurry Bonsai.both + @@ Bonsai.state_machine0 + graph + ~sexp_of_model:[%sexp_of: Int.t] + ~equal:[%equal: Int.t] + ~sexp_of_action:[%sexp_of: Int.t] + ~default_model:400 + ~apply_action:(fun (_ : _ Bonsai.Apply_action_context.t) x delta -> x + delta) ;; -let component = - let%sub x, add_x = offset_state_machine in - let%sub y, add_y = offset_state_machine in - let%sub show_help, set_show_help = - Bonsai.state false ~sexp_of_model:[%sexp_of: Bool.t] ~equal:[%equal: Bool.t] +let component graph = + let%sub x, add_x = offset_state_machine graph in + let%sub y, add_y = offset_state_machine graph in + let show_help, set_show_help = + Bonsai.state false ~sexp_of_model:[%sexp_of: Bool.t] ~equal:[%equal: Bool.t] graph in - let%sub handler = + let handler = let%arr add_x = add_x and add_y = add_y and set_show_help = set_show_help in @@ -121,7 +122,7 @@ let component = (fun _ev -> set_show_help false) ] in - let%sub help_view = + let help_view = match%sub show_help with | true -> let%arr handler = handler in @@ -131,7 +132,7 @@ let component = (Keyboard_event_handler.get_help_text handler) Help_text.View_spec.plain ] - | false -> Bonsai.const Vdom.Node.none + | false -> Bonsai.return Vdom.Node.none in let%arr x = x and y = y diff --git a/examples/visibility/main.ml b/examples/visibility/main.ml index d27c67c1..ab6e9920 100644 --- a/examples/visibility/main.ml +++ b/examples/visibility/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Bonsai.Let_syntax module Vis = Bonsai_web_ui_visibility @@ -48,22 +48,25 @@ stylesheet } |}] -let visible_attr = Value.return Style.visible -let hidden_attr = Value.return Style.hidden -let data = List.init 30 ~f:(fun i -> i, ()) |> Int.Map.of_alist_exn |> Value.return +let visible_attr = Bonsai.return Style.visible +let hidden_attr = Bonsai.return Style.hidden +let data = List.init 30 ~f:(fun i -> i, ()) |> Int.Map.of_alist_exn |> Bonsai.return let view i = Vdom.Node.div ~attrs:[ Style.box ] [ Vdom.Node.textf "%d" i ] -let component = - let%sub components = +let component graph = + let components = Bonsai.assoc (module Int) data - ~f:(fun key _data -> + ~f:(fun key _data graph -> Vis.only_when_visible' ~visible_attr ~hidden_attr - (let%arr key = key in - view key, key)) + (fun _graph -> + let%arr key = key in + view key, key) + graph) + graph in let%arr components = components in let boxes, debug = components |> Map.data |> List.unzip in diff --git a/examples/weird_prt_situations/main.ml b/examples/weird_prt_situations/main.ml index 682180c0..12ab0fd8 100644 --- a/examples/weird_prt_situations/main.ml +++ b/examples/weird_prt_situations/main.ml @@ -1,29 +1,29 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax module Table = Bonsai_web_ui_partial_render_table.Basic module Column = Table.Columns.Dynamic_cells module Form = Bonsai_web_ui_form.With_automatic_view -let header text = Column.Sortable.Header.with_icon (Vdom.Node.text text) |> Value.return +let header text = Column.Sortable.Header.with_icon (Vdom.Node.text text) |> Bonsai.return let columns = - [ Column.column - ~header:(header "i") - ~cell:(fun ~key ~data:_ -> - let%arr key = key in - Vdom.Node.text (Int.to_string key)) - () - ; Column.column - ~header:(header "i * 3") - ~cell:(fun ~key ~data:_ -> - let%arr key = key in - Vdom.Node.text (Int.to_string (key * 3))) - () - ] - |> Column.lift + Column.lift + [ Column.column + ~header:(header "i") + ~cell:(fun ~key ~data:_ _graph -> + let%arr key = key in + Vdom.Node.text (Int.to_string key)) + () + ; Column.column + ~header:(header "i * 3") + ~cell:(fun ~key ~data:_ _graph -> + let%arr key = key in + Vdom.Node.text (Int.to_string (key * 3))) + () + ] ;; module Css = [%css stylesheet {| @@ -47,15 +47,14 @@ module Table_id = struct [@@deriving enumerate, sexp, equal, compare] end -let component = - let%sub data = - Bonsai.const (Int.Map.of_alist_exn (List.init 100 ~f:(fun i -> i, ()))) - in - let%sub focused_table, set_focused_table = +let component graph = + let data = Bonsai.return (Int.Map.of_alist_exn (List.init 100 ~f:(fun i -> i, ()))) in + let focused_table, set_focused_table = Bonsai.state First_table ~sexp_of_model:[%sexp_of: Table_id.t] ~equal:[%equal: Table_id.t] + graph in let on_change which = let%map set_focused_table = set_focused_table in @@ -67,19 +66,21 @@ let component = Table.component (module Int) ~focus:(By_row { on_change = on_change First_table }) - ~row_height:(Value.return (`Px 30)) + ~row_height:(Bonsai.return (`Px 30)) ~columns data + graph in let%sub { view = table2; focus = focus2; _ } = Table.component (module Int) ~focus:(By_row { on_change = on_change Second_table }) - ~row_height:(Value.return (`Px 30)) + ~row_height:(Bonsai.return (`Px 30)) ~columns data + graph in - let%sub () = + let () = Bonsai.Edge.on_change ~sexp_of_model:[%sexp_of: Table_id.t] ~equal:[%equal: Table_id.t] @@ -90,13 +91,14 @@ let component = function | Table_id.First_table -> Table.Focus.By_row.unfocus focus2 | Second_table -> Table.Focus.By_row.unfocus focus1) + graph in - let%sub which_form = Form.Elements.Dropdown.enumerable (module Which) in - let%sub which = + let which_form = Form.Elements.Dropdown.enumerable (module Which) graph in + let which = let%arr which_form = which_form in Form.value_or_default ~default:Which.Stacked_tables which_form in - let%sub tables = + let tables = match%sub which with | Stacked_tables -> let%arr table1 = table1 diff --git a/examples/widget/main.ml b/examples/widget/main.ml index 098f627a..a4a5cfa3 100644 --- a/examples/widget/main.ml +++ b/examples/widget/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax open Js_of_ocaml module Form = Bonsai_web_ui_form.With_automatic_view @@ -65,43 +65,55 @@ module T = struct let destroy _input state _element = Dom_html.window##clearInterval state.interval end -let canvas_thing = - let%sub scope_form = - Form.Elements.Number.int ~default:1 ~step:1 ~allow_updates_when_focused:`Never () - in - let%sub how_many = - Form.Elements.Number.int ~default:1 ~step:1 ~allow_updates_when_focused:`Never () +let canvas_thing graph = + let scope_form = + Form.Elements.Number.int + ~default:1 + ~step:1 + ~allow_updates_when_focused:`Never + () + graph in - let%sub color_picker = Form.Elements.Color_picker.hex () in - let%sub color = - Bonsai.pure (Form.value_or_default ~default:(`Hex "#000")) color_picker + let how_many = + Form.Elements.Number.int + ~default:1 + ~step:1 + ~allow_updates_when_focused:`Never + () + graph in - let%sub scope = Bonsai.pure (Form.value_or_default ~default:0) scope_form in - let%sub widget = - Bonsai.scope_model (module Int) ~on:scope (Widget.component (module T) color) + let color_picker = Form.Elements.Color_picker.hex () graph in + let color = Bonsai.map ~f:(Form.value_or_default ~default:(`Hex "#000")) color_picker in + let scope = Bonsai.map ~f:(Form.value_or_default ~default:0) scope_form in + let widget = + Bonsai.scope_model + (module Int) + ~on:scope + ~for_:(Widget.component (module T) color) + graph in - let%sub last_values, set_values = - Bonsai.state [] ~sexp_of_model:[%sexp_of: int list] ~equal:[%equal: int list] + let last_values, set_values = + Bonsai.state [] ~sexp_of_model:[%sexp_of: int list] ~equal:[%equal: int list] graph in - let%sub reset_effect = + let reset_effect = let%arr { modify; _ } = widget in modify (fun _input state -> state.frame <- 0) in - let%sub read_effect = + let read_effect = let%arr { read; _ } = widget and set_values = set_values in let%bind.Effect l = read (fun _input state -> state.frame) in set_values l in - let%sub color_picker = + let color_picker = let%arr color_picker = color_picker in color_picker |> Form.label "color" |> Form.view_as_vdom in - let%sub scope_form = + let scope_form = let%arr scope_form = scope_form in scope_form |> Form.label "scope" |> Form.view_as_vdom in - let%sub theme = View.Theme.current in + let theme = View.Theme.current graph in let%arr { view; _ } = widget and color_picker = color_picker and theme = theme diff --git a/examples/widget_bug_repro/main.ml b/examples/widget_bug_repro/main.ml index cfc4af0c..056c7db3 100644 --- a/examples/widget_bug_repro/main.ml +++ b/examples/widget_bug_repro/main.ml @@ -1,5 +1,5 @@ open! Core -open! Bonsai_web +open! Bonsai_web.Cont open Bonsai.Let_syntax let widget = @@ -47,7 +47,7 @@ let inner_html_b = () ;; -let component true_or_false ~toggle = +let component true_or_false ~toggle _graph = let%arr true_or_false = true_or_false in let widget = widget true_or_false in let ids = @@ -66,9 +66,9 @@ let component true_or_false ~toggle = ;; let () = - let var = Bonsai.Var.create true in - let toggle = Bonsai.Effect.of_sync_fun (Bonsai.Var.set var) in + let var = Bonsai.Expert.Var.create true in + let toggle = Bonsai.Effect.of_sync_fun (Bonsai.Expert.Var.set var) in let toggle a = toggle a in - let value = Bonsai.Var.value var in + let value = Bonsai.Expert.Var.value var in Bonsai_web.Start.start (component value ~toggle) ;; diff --git a/for_bonsai_devs.md b/for_bonsai_devs.md new file mode 100644 index 00000000..d46e0d32 --- /dev/null +++ b/for_bonsai_devs.md @@ -0,0 +1,3 @@ +# For Bonsai Developers + +These docs are intended for developers of Bonsai. diff --git a/src/annotate_incr.ml b/src/annotate_incr.ml index f4d55b74..74b9815e 100644 --- a/src/annotate_incr.ml +++ b/src/annotate_incr.ml @@ -111,7 +111,7 @@ end let annotate_packed = Memo.general ~hashable:Kind.hashable (fun kind incr -> - let label = [ Kind.name kind ] in + let label = [ "kind"; Kind.name kind ] in let (`Hex color) = Kind.color kind in let attrs = String.Map.of_alist_exn [ "style", "filled"; "fillcolor", color ] in Incr.Packed.append_user_info_graphviz incr ~label ~attrs) diff --git a/src/bonsai.ml b/src/bonsai.ml index 7afbf1ee..d974cac0 100644 --- a/src/bonsai.ml +++ b/src/bonsai.ml @@ -38,6 +38,7 @@ module Private = struct let top_level_handle = Cont.Conv.top_level_handle let handle = Cont.Conv.handle let perform = Cont.Conv.perform + let set_perform_on_exception = Cont.Expert.For_bonsai_internal.set_perform_on_exception end include Proc_layer2 diff --git a/src/bonsai.mli b/src/bonsai.mli index d3797388..5c8aaf5e 100644 --- a/src/bonsai.mli +++ b/src/bonsai.mli @@ -64,6 +64,7 @@ module Private : sig val gather : 'result Computation.t -> ('result, unit) Computation.packed_info val pre_process : 'result Computation.t -> 'result Computation.t + val set_perform_on_exception : (exn -> unit) -> unit end module Arrow_deprecated : sig diff --git a/src/computation.ml b/src/computation.ml index c8ebd0c3..66507d96 100644 --- a/src/computation.ml +++ b/src/computation.ml @@ -30,7 +30,8 @@ type ('model, 'action, 'input, 'result, 'extra) info = ; apply_action : ('input, 'action, 'model) apply_action ; run : ('model, 'action, 'input, 'result, 'extra) eval_fun ; reset : ('action, 'model) reset - ; can_contain_path : bool + ; may_contain_path : May_contain.Path.t + ; may_contain_lifecycle : May_contain.Lifecycle.t } type ('result, 'extra) packed_info = @@ -122,7 +123,7 @@ type 'result t = | Assoc_simpl : { map : ('k, 'v, 'cmp) Map.t Value.t ; by : Path.t -> 'k -> 'v -> 'result - ; can_contain_path : bool + ; may_contain_path : May_contain.Path.t } -> ('k, 'result, 'cmp) Map.t t | Switch : diff --git a/src/constant_fold.ml b/src/constant_fold.ml index 40e98121..18df90e4 100644 --- a/src/constant_fold.ml +++ b/src/constant_fold.ml @@ -92,12 +92,12 @@ include struct ~key_id ~data_id in - let can_contain_path = + let may_contain_path = match can_contain_path with - | `Can_contain_path -> true - | `Cannot_contain_path -> false + | `Can_contain_path -> May_contain.Path.Yes_or_maybe + | `Cannot_contain_path -> No in - Computation.Assoc_simpl { map; by; can_contain_path } + Computation.Assoc_simpl { map; by; may_contain_path } ;; end diff --git a/src/cont.ml b/src/cont.ml index 4a2c1f1f..5bbd047c 100644 --- a/src/cont.ml +++ b/src/cont.ml @@ -9,109 +9,12 @@ module type Comparator = Module_types.Comparator type ('k, 'cmp) comparator = ('k, 'cmp) Module_types.comparator -module Cont_primitives : sig - (** Bonsai's [Cont] implementation is based on continuation-passing style. - - Previously, in [Proc], calls to [sub] and [arr] directly added the corresponding - evaluation/lookup nodes to the computation graph, which was available through - the [Computation.t]'s underlying type representation. - - As before, [Computation.t] is the static structure of the computation graph, - and [Bonsai.t = Value.t] is the incrementally-computed "runtime" value. - We now use that representation as an intermediate representation, - but end-users don't construct the computation graph directly. - - Instead, we pass around a ['graph'] value. When components are combined or - composed, Bonsai's internals add the new computation into the computation - graph. - - The primitive API (not exposed to end-users) tries to simulate algebraic effects. - - [perform] takes a [local_ graph -> Computation.t] component, - evaluates it, and adds it into the computation graph. - It returns a [Value.t] memoized alias to this new computation. - - [handle] places a [Value.t] in the context of a fresh computation graph. - It then wraps the result in a [Computation.t]. - This is generally used to construct a portion of the computation graph - in isolation. The subgraph is then added to the main graph via [perform]. - - At the top level, [top_level_handle] is similar to [handle], but only takes [f]. - In the context of a fresh graph, it runs [f graph] to obtain the total composed - [Value.t] for a Bonsai app. Then, it raises that to a [Computation.t], - and passes {i that i} through the [graph.f] that we've been assembling - all this time. - - But what's going on under the surface? And what is [graph]? - And why can the new Bonsai API use [let%map]? - - --- - - If you look at computation.ml, you'll see that [Computation.t] is a nested variant - of nodes that store things. These include state, assoc/switch, store/fetch for - Dynamic_scope, and most commonly used, sub. This is the memoization-like - pattern that powers work-sharing for incrementality: we can store a reusable - value with a unique name (via Type_equal.id), which downstream incremental [Value.t]s - can reference (and reuse!) instead of repeating work. [Sub] has 3 fields: - - - [from] is the computation being memoized - - [via] is the [Type_equal.id] identifier - - [into] is the downstream computation that uses the value of [from]. - - So if we want to depend on several [sub]ed computations, our graph will look as follows: - - {v - Sub { from=comp1; via=; into = - Sub { from=comp2; via=; into = - ... into = thing_we_want_to_compute ... - } - } - v} - - --- - - The [graph] we pass around is a ref to a function that will eventually compute this - [Computation.t]. We can't construct it eagerly as we go, since that would require - making the fields of [Computation.t] mutable, so we need to do some functional - trickery. - - [graph] starts as [Fn.id]. At each invocation of [perform], - we overwrite it to still return its input, but nested in a new layer of [Sub]. - Let's look at how this works in practice: - - {v - 1.let my_component graph = - 2. let%sub model1, _inject = Bonsai.state 5 in - 3. let%sub model2, _inject = Bonsai.state 6 in - 4. model2 - - 1. graph = { f = fun r -> r } - 2. graph = { f = fun r -> Sub { from=state 5; via=; into=r } } - 3. graph = { f = fun r -> Sub { from=state 5; via=; into=Sub { from=state 6; via=; into=r } } } - 4. graph.f () ==> Sub { from=state 5; via=; into=Sub { from=state 6; via=; into= } } - v} - - Instead of [state 5/6], we'd actually have something more like [Leaf0 { ... }], - but that's not really relevant. We also do some optimizations if [sub]ing isn't - actually necessary. - - Handle is much simpler: we run our [Value.t] construction function ([f]) on a fresh - [graph], which becomes constructed via calls to [perform] presumably inside [f]. - Then, we call [graph.f] on that result, so that it is inside the nested [Sub]s, - giving it access to those memoized aliases. Finally, we wrap this in a [Computation.t], - which can be [perform]ed into other computations. - - --- - - But why can we now use [let%map.Bonsai] instead of [let%sub] and [let%arr] combos? - After all, we're not passing [graph] to [let%map]... - - In short, we cheat and make [graph] global. We still require users to pass it - when possible, so that creating costly stateful nodes is an explicit operation. - However, we don't want people to use these "magic" [let%map] and [match%sub] calls - unless they are in the dynamic scope of a Bonsai top level. - We enforce this by maintaining a global boolean ref for whether we're in a top level, - and erroring on [perform] if we are not. - *) +module For_bonsai_internal = struct + let perform_on_exception = ref ignore + let set_perform_on_exception perform = perform_on_exception := perform +end +module Cont_primitives : sig type graph (* Main primitives; see above for explanation. *) @@ -134,7 +37,7 @@ end = struct (* Introduce the optimization [let%sub a = return foo in use a] => [use foo] This only makes sense if the Value.t being returned is either a constant or an already-bound named value, otherwise you risk losing value sharing. *) - { Value.value; id; here = None } + { Value.value; id; here } | computation_to_perform -> (* Mint a fresh type-id to hold the result of performing this graph modification *) let via : a Type_equal.Id.t = Type_equal.Id.create ~name:"" [%sexp_of: opaque] in @@ -148,9 +51,7 @@ end = struct | eventual_result -> (* old_f takes the eventual innermost result, and wraps it in 0+ layers of subs. We replace it with a new function that adds another layer to the inside. *) - old_f - (Sub - { from = computation_to_perform; via; into = eventual_result; here = None }) + old_f (Sub { from = computation_to_perform; via; into = eventual_result; here }) in (* write the new hole into the graph, and return a new value referencing the type-id that will be populated when [new_f] is invoked. *) @@ -172,6 +73,7 @@ end = struct r with | exn -> + !For_bonsai_internal.perform_on_exception exn; graph.f <- backup_f; Proc.read (Value.return_exn exn) ;; @@ -509,6 +411,7 @@ module Expert = struct let delay = delay module Var = Var + module For_bonsai_internal = For_bonsai_internal end let freeze ?sexp_of_model ?equal v graph = diff --git a/src/cont.mli b/src/cont.mli index 474f6dd9..1077b0ec 100644 --- a/src/cont.mli +++ b/src/cont.mli @@ -536,6 +536,10 @@ module Expert : sig (** Retrieves the underlying ['a t] Ui_incr.t var. *) val incr_var : 'a t -> 'a Ui_incr.Var.t end + + module For_bonsai_internal : sig + val set_perform_on_exception : (exn -> unit) -> unit + end end (** Just in subfeature to reimplement proc on top of cont *) diff --git a/src/driver/bonsai_driver.ml b/src/driver/bonsai_driver.ml index f2556014..27eea626 100644 --- a/src/driver/bonsai_driver.ml +++ b/src/driver/bonsai_driver.ml @@ -62,7 +62,8 @@ let create_direct ; action = _ ; run = _ ; reset = _ - ; can_contain_path = _ + ; may_contain_lifecycle = _ + ; may_contain_path = _ } as computation_info)) = optimized_info diff --git a/src/eval.ml b/src/eval.ml index 1d6f81ad..817b009c 100644 --- a/src/eval.ml +++ b/src/eval.ml @@ -5,16 +5,28 @@ open Incr.Let_syntax let ( >>> ) f inject b = inject (f b) let () = Incr.State.(set_max_height_allowed t 1024) -let unzip3_mapi' map ~f = - let first, second_and_third = - Incr_map.unzip_mapi' map ~f:(fun ~key ~data -> - let a, b, c = f ~key ~data in - let bc = Incr.both b c in - annotate Lifecycle_apply_action_pair bc; - a, bc) - in - let second, third = Incr_map.unzip second_and_third in - first, second, third +let unzip3_mapi' map ~may_contain_lifecycle ~comparator ~f = + match (may_contain_lifecycle : May_contain.Lifecycle.t) with + | No -> + (* if we know that [f] always returns a triple whose last element (the lifecycle + incremental) is always the empty lifecycle collection, then we can drop it + here, and avoid nesting unzips *) + let first, second = + Incr_map.unzip_mapi' map ~f:(fun ~key ~data -> + let a, b, _ = f ~key ~data in + a, b) + in + first, second, Incr.return (Map.empty comparator) + | Yes_or_maybe -> + let first, second_and_third = + Incr_map.unzip_mapi' map ~f:(fun ~key ~data -> + let a, b, c = f ~key ~data in + let bc = Incr.both b c in + annotate Lifecycle_apply_action_pair bc; + a, bc) + in + let second, third = Incr_map.unzip second_and_third in + first, second, third ;; let do_nothing_lifecycle = Incr.return Lifecycle.Collection.empty @@ -38,11 +50,13 @@ let rec gather ; apply_action = unusable_apply_action ; reset = reset_unit_model ; run - ; can_contain_path = false + ; may_contain_path = No + ; may_contain_lifecycle = No }) | Leaf1 { model; input_id; dynamic_action; apply_action; input; reset } -> let wrap_leaf inject = Action.dynamic_leaf >>> inject in let run ~environment ~path:_ ~clock:_ ~model ~inject = + annotate Model model; let input = Value.eval environment input in (* It's important to create [inject_dynamic] outside of the [let%mapn] so that it remains [phys_equal] when the [model] changes. *) @@ -73,11 +87,13 @@ let rec gather ; apply_action ; reset ; run - ; can_contain_path = false + ; may_contain_path = No + ; may_contain_lifecycle = No }) | Leaf0 { model; static_action; apply_action; reset } -> let wrap_leaf inject = Action.static_leaf >>> inject in let run ~environment:_ ~path:_ ~clock:_ ~model ~inject = + annotate Model model; (* It's important to create [inject_static] outside of the [let%mapn] so that it remains [phys_equal] when the [model] changes. *) let inject_static = wrap_leaf inject in @@ -106,7 +122,8 @@ let rec gather ; apply_action ; reset ; run - ; can_contain_path = false + ; may_contain_path = No + ; may_contain_lifecycle = No }) | Leaf_incr { input; compute } -> let run ~environment ~path:_ ~clock ~model:_ ~inject:_ = @@ -122,7 +139,8 @@ let rec gather ; apply_action = unusable_apply_action ; reset = reset_unit_model ; run - ; can_contain_path = false + ; may_contain_path = No + ; may_contain_lifecycle = No }) | Sub { into = Sub { into = Sub _; _ }; _ } as t -> Eval_sub.chain t ~gather:{ f = gather } @@ -145,7 +163,8 @@ let rec gather ; action = gathered.action ; apply_action = gathered.apply_action ; reset = gathered.reset - ; can_contain_path = gathered.can_contain_path + ; may_contain_path = gathered.may_contain_path + ; may_contain_lifecycle = gathered.may_contain_lifecycle }) | Fetch { id; default; for_some } -> let run ~environment ~path:_ ~clock:_ ~model:_ ~inject:_ = @@ -164,7 +183,8 @@ let rec gather ; apply_action = unusable_apply_action ; reset = reset_unit_model ; run - ; can_contain_path = false + ; may_contain_path = No + ; may_contain_lifecycle = No }) | Assoc { map; key_comparator; key_id; cmp_id; data_id; by } -> let module Cmp = (val key_comparator) in @@ -178,7 +198,8 @@ let rec gather ; apply_action ; run ; reset - ; can_contain_path + ; may_contain_path + ; may_contain_lifecycle }) = gather by @@ -195,51 +216,61 @@ let rec gather unstage (Path.Elem.keyed ~compare:Cmp.comparator.compare key_id) in let results_map, input_map, lifecycle_map = - unzip3_mapi' input_and_models_map ~f:(fun ~key ~data:input_and_model -> - annotate Model_and_input input_and_model; - let path = - if can_contain_path - then Path.append path Path.Elem.(Assoc (create_keyed key)) - else path - in - let%pattern_bind value, model = input_and_model in - let key_incr = Incr.const key in - annotate Assoc_key key_incr; - annotate Assoc_input value; - let environment = - (* It is safe to reuse the same [key_id] and [data_id] for each pair in the map, - since they all start with a fresh "copy" of the outer environment. *) - environment - |> Environment.add_exn ~key:key_id ~data:key_incr - |> Environment.add_exn ~key:data_id ~data:value - in - let snapshot, () = - run ~environment ~path ~clock ~inject:(wrap_assoc ~key inject) ~model - |> Trampoline.run - in - ( Snapshot.result snapshot - , Input.to_incremental (Snapshot.input snapshot) - , Snapshot.lifecycle_or_empty snapshot )) + unzip3_mapi' + input_and_models_map + ~comparator:(module Cmp) + ~may_contain_lifecycle + ~f:(fun ~key ~data:input_and_model -> + annotate Model_and_input input_and_model; + let path = + match may_contain_path with + | Yes_or_maybe -> Path.append path Path.Elem.(Assoc (create_keyed key)) + | No -> path + in + let%pattern_bind value, model = input_and_model in + let key_incr = Incr.const key in + annotate Assoc_key key_incr; + annotate Assoc_input value; + let environment = + (* It is safe to reuse the same [key_id] and [data_id] for each pair in the map, + since they all start with a fresh "copy" of the outer environment. *) + environment + |> Environment.add_exn ~key:key_id ~data:key_incr + |> Environment.add_exn ~key:data_id ~data:value + in + let snapshot, () = + run ~environment ~path ~clock ~inject:(wrap_assoc ~key inject) ~model + |> Trampoline.run + in + ( Snapshot.result snapshot + , Input.to_incremental (Snapshot.input snapshot) + , Snapshot.lifecycle_or_empty snapshot )) in annotate Assoc_results results_map; annotate Assoc_lifecycles lifecycle_map; annotate Assoc_inputs input_map; let lifecycle = - Incr_map.unordered_fold_nested_maps - lifecycle_map - ~init:Path.Map.empty - ~add:(fun ~outer_key:_ ~inner_key:key ~data acc -> - Map.update acc key ~f:(function - | Some _ -> Path.raise_duplicate key - | None -> data)) - ~remove:(fun ~outer_key:_ ~inner_key:key ~data:_ acc -> Map.remove acc key) - in - annotate Assoc_lifecycles lifecycle; + (* if we can prove that the body of the assoc doesn't contain a + lifecycle node, then return None, dropping the constant incremental + node on the floor. *) + match may_contain_lifecycle with + | No -> None + | Yes_or_maybe -> + let unfolded = + Incr_map.unordered_fold_nested_maps + lifecycle_map + ~init:Path.Map.empty + ~add:(fun ~outer_key:_ ~inner_key:key ~data acc -> + Map.update acc key ~f:(function + | Some _ -> Path.raise_duplicate key + | None -> data)) + ~remove:(fun ~outer_key:_ ~inner_key:key ~data:_ acc -> Map.remove acc key) + in + annotate Assoc_lifecycles unfolded; + Some unfolded + in Trampoline.return - ( Snapshot.create - ~result:results_map - ~input:(Input.dynamic input_map) - ~lifecycle:(Some lifecycle) + ( Snapshot.create ~result:results_map ~input:(Input.dynamic input_map) ~lifecycle , () ) in let apply_action @@ -278,7 +309,8 @@ let rec gather ; apply_action ; reset ; run - ; can_contain_path + ; may_contain_path + ; may_contain_lifecycle }) | Assoc_on { map @@ -310,7 +342,8 @@ let rec gather ; apply_action ; run ; reset - ; can_contain_path + ; may_contain_path + ; may_contain_lifecycle }) = gather by @@ -322,70 +355,81 @@ let rec gather unstage (Path.Elem.keyed ~compare:Io_comparator.comparator.compare io_key_id) in let results_map, input_map, lifecycle_map = - unzip3_mapi' map_input ~f:(fun ~key:io_key ~data:value -> - let%pattern_bind results_map, input_map, lifecycle_map = - let path = - if can_contain_path - then Path.append path Path.Elem.(Assoc (create_keyed io_key)) - else path + unzip3_mapi' + map_input + ~may_contain_lifecycle + ~comparator:(module Io_comparator) + ~f:(fun ~key:io_key ~data:value -> + let%pattern_bind results_map, input_map, lifecycle_map = + let path = + match may_contain_path with + | Yes_or_maybe -> Path.append path Path.Elem.(Assoc (create_keyed io_key)) + | No -> path + in + let key_incr = Incr.const io_key in + annotate Assoc_key key_incr; + annotate Assoc_input value; + let environment = + (* It is safe to reuse the same [key_id] and [data_id] for each pair in the map, + since they all start with a fresh "copy" of the outer environment. *) + environment + |> Environment.add_exn ~key:io_key_id ~data:key_incr + |> Environment.add_exn ~key:data_id ~data:value + in + let model_key = + let%map value = value in + get_model_key io_key value + in + Incr.set_cutoff + model_key + (Incr.Cutoff.of_compare model_key_comparator.compare); + let%bind model_key = model_key in + let model = + match%map Incr_map.Lookup.find model_lookup model_key with + | None -> model_info.default + | Some (_prev_io_key, model) -> model + in + annotate Model model; + let snapshot, () = + run + ~environment + ~path + ~clock + ~inject:(wrap_assoc_on ~io_key ~model_key inject) + ~model + |> Trampoline.run + in + let%mapn result = Snapshot.result snapshot + and input = Input.to_incremental (Snapshot.input snapshot) + and lifecycle = Snapshot.lifecycle_or_empty snapshot in + result, input, lifecycle in - let key_incr = Incr.const io_key in - annotate Assoc_key key_incr; - annotate Assoc_input value; - let environment = - (* It is safe to reuse the same [key_id] and [data_id] for each pair in the map, - since they all start with a fresh "copy" of the outer environment. *) - environment - |> Environment.add_exn ~key:io_key_id ~data:key_incr - |> Environment.add_exn ~key:data_id ~data:value - in - let model_key = - let%map value = value in - get_model_key io_key value - in - Incr.set_cutoff - model_key - (Incr.Cutoff.of_compare model_key_comparator.compare); - let%bind model_key = model_key in - let model = - match%map Incr_map.Lookup.find model_lookup model_key with - | None -> model_info.default - | Some (_prev_io_key, model) -> model - in - let snapshot, () = - run - ~environment - ~path - ~clock - ~inject:(wrap_assoc_on ~io_key ~model_key inject) - ~model - |> Trampoline.run - in - let%mapn result = Snapshot.result snapshot - and input = Input.to_incremental (Snapshot.input snapshot) - and lifecycle = Snapshot.lifecycle_or_empty snapshot in - result, input, lifecycle - in - results_map, input_map, lifecycle_map) + results_map, input_map, lifecycle_map) in annotate Assoc_results results_map; annotate Assoc_lifecycles lifecycle_map; let lifecycle = - Incr_map.unordered_fold_nested_maps - lifecycle_map - ~init:Path.Map.empty - ~add:(fun ~outer_key:_ ~inner_key:key ~data acc -> - Map.update acc key ~f:(function - | Some _ -> Path.raise_duplicate key - | None -> data)) - ~remove:(fun ~outer_key:_ ~inner_key:key ~data:_ acc -> Map.remove acc key) - in - annotate Assoc_lifecycles lifecycle; + (* if we can prove that the body of the assoc_on doesn't contain a + lifecycle node, then return None, dropping the constant incremental + node on the floor. *) + match may_contain_lifecycle with + | No -> None + | Yes_or_maybe -> + let unfolded = + Incr_map.unordered_fold_nested_maps + lifecycle_map + ~init:Path.Map.empty + ~add:(fun ~outer_key:_ ~inner_key:key ~data acc -> + Map.update acc key ~f:(function + | Some _ -> Path.raise_duplicate key + | None -> data)) + ~remove:(fun ~outer_key:_ ~inner_key:key ~data:_ acc -> Map.remove acc key) + in + annotate Assoc_lifecycles unfolded; + Some unfolded + in Trampoline.return - ( Snapshot.create - ~result:results_map - ~input:(Input.dynamic input_map) - ~lifecycle:(Some lifecycle) + ( Snapshot.create ~result:results_map ~input:(Input.dynamic input_map) ~lifecycle , () ) in let apply_action @@ -438,9 +482,10 @@ let rec gather ; apply_action ; reset ; run - ; can_contain_path + ; may_contain_path + ; may_contain_lifecycle }) - | Assoc_simpl { map; by; can_contain_path } -> + | Assoc_simpl { map; by; may_contain_path } -> let run ~environment ~path ~clock:_ ~model:_ ~inject:_ = let map_input = Value.eval environment map in let result = Incr_map.mapi map_input ~f:(fun ~key ~data -> by path key data) in @@ -454,20 +499,34 @@ let rec gather ; apply_action = unusable_apply_action ; reset = reset_unit_model ; run - ; can_contain_path + ; may_contain_path + ; may_contain_lifecycle = No }) | Switch { match_; arms; here = _ } -> let wrap_switch ~branch ~type_id inject = Action.switch ~branch ~type_id >>> inject in let%bind.Trampoline gathered = Trampoline.all_map (Map.map arms ~f:gather) in - let can_contain_path, needs_disambiguation = + let may_contain_lifecycle = + Map.fold + gathered + ~init:May_contain.Lifecycle.No + ~f:(fun ~key:_ ~data:(T gathered) acc -> + May_contain.Lifecycle.merge acc gathered.may_contain_lifecycle) + in + let may_contain_path, needs_disambiguation = let num_contain_path = - Map.count gathered ~f:(fun (T { can_contain_path; _ }) -> can_contain_path) + Map.count gathered ~f:(function + | T { may_contain_path = Yes_or_maybe; _ } -> true + | T { may_contain_path = No; _ } -> false) + in + let may_contain_path = + if num_contain_path > 0 then May_contain.Path.Yes_or_maybe else No in - num_contain_path > 0, num_contain_path > 1 + may_contain_path, num_contain_path > 1 in let run ~environment ~path ~clock ~model ~inject = + annotate Model model; let index = Value.eval environment match_ in - let%pattern_bind result, input, lifecycle = + let result_input_and_lifecycle = let%bind index = index in (* !!!This is a load-bearing bind!!! @@ -484,7 +543,8 @@ let rec gather ; action = action_info ; apply_action = _ ; reset = _ - ; can_contain_path = _ + ; may_contain_path = _ + ; may_contain_lifecycle = _ ; run }) = @@ -513,13 +573,20 @@ let rec gather let%mapn input = Input.to_incremental (Snapshot.input snapshot) in Meta.Input.Hidden.T { input; type_id = input_info; key = index } in - let%mapn result = Snapshot.result snapshot - and lifecycle = Snapshot.lifecycle_or_empty snapshot - and input = input in - result, input, lifecycle + Incr.return (Snapshot.result snapshot, input, Snapshot.lifecycle_or_empty snapshot) in + let result = Incr.bind result_input_and_lifecycle ~f:Tuple3.get1 + and input = Incr.bind result_input_and_lifecycle ~f:Tuple3.get2 + and lifecycle = Incr.bind result_input_and_lifecycle ~f:Tuple3.get3 in let input = Input.dynamic input in - Trampoline.return (Snapshot.create ~result ~input ~lifecycle:(Some lifecycle), ()) + let lifecycle = + (* if we can prove that none of the switch cases have lifecycle functions, + then return None, dropping the incremental node on the floor. *) + match may_contain_lifecycle with + | No -> None + | Yes_or_maybe -> Some lifecycle + in + Trampoline.return (Snapshot.create ~result ~input ~lifecycle, ()) in let apply_action ~inject @@ -535,7 +602,8 @@ let rec gather ; apply_action ; run = _ ; reset = _ - ; can_contain_path = _ + ; may_contain_path = _ + ; may_contain_lifecycle = _ }) = Map.find_exn gathered index @@ -597,8 +665,9 @@ let rec gather ; action = am ; reset ; apply_action = _ + ; may_contain_lifecycle = _ ; run = _ - ; can_contain_path = _ + ; may_contain_path = _ }) = Map.find_exn gathered index @@ -631,7 +700,8 @@ let rec gather ; apply_action ; reset ; run - ; can_contain_path + ; may_contain_path + ; may_contain_lifecycle }) | Lazy lazy_computation -> let wrap_lazy ~type_id inject = Action.lazy_ ~type_id >>> inject in @@ -645,11 +715,13 @@ let rec gather ; run ; apply_action = _ ; reset = _ - ; can_contain_path = _ + ; may_contain_path = _ + ; may_contain_lifecycle = _ }) = force gathered in + annotate Model model; let input_model = let%map model = model in let (Meta.Model.Hidden.T { model; info; _ }) = @@ -696,7 +768,8 @@ let rec gather ; apply_action ; run = _ ; reset = _ - ; can_contain_path = _ + ; may_contain_path = _ + ; may_contain_lifecycle = _ }) = force gathered @@ -738,7 +811,8 @@ let rec gather ; apply_action = _ ; run = _ ; input = _ - ; can_contain_path = _ + ; may_contain_path = _ + ; may_contain_lifecycle = _ }) = force gathered @@ -769,7 +843,8 @@ let rec gather ; apply_action ; run ; reset - ; can_contain_path = true + ; may_contain_path = Yes_or_maybe + ; may_contain_lifecycle = Yes_or_maybe }) | Wrap { wrapper_model @@ -788,7 +863,8 @@ let rec gather ; apply_action ; run ; reset - ; can_contain_path + ; may_contain_path + ; may_contain_lifecycle }) = gather inner @@ -796,7 +872,9 @@ let rec gather let wrap_inner inject = Action.wrap_inner >>> inject in let wrap_outer inject = Action.wrap_outer >>> inject in let run ~environment ~path ~clock ~model ~inject = + annotate Model model; let%pattern_bind outer_model, inner_model = model in + annotate Model outer_model; let%bind.Trampoline inner_snapshot, () = let environment = environment @@ -857,7 +935,8 @@ let rec gather ; apply_action ; run ; reset - ; can_contain_path + ; may_contain_path + ; may_contain_lifecycle }) | With_model_resetter { inner; reset_id } -> let%bind.Trampoline (T @@ -867,7 +946,8 @@ let rec gather ; apply_action ; run ; reset - ; can_contain_path + ; may_contain_path + ; may_contain_lifecycle } as gathered_inner)) = gather inner @@ -923,7 +1003,8 @@ let rec gather ; apply_action ; run ; reset - ; can_contain_path + ; may_contain_path + ; may_contain_lifecycle })) | Path -> let run ~environment:_ ~path ~clock:_ ~model:_ ~inject:_ = @@ -939,7 +1020,8 @@ let rec gather ; apply_action = unusable_apply_action ; reset = reset_unit_model ; run - ; can_contain_path = true + ; may_contain_path = Yes_or_maybe + ; may_contain_lifecycle = No }) | Lifecycle lifecycle -> let run ~environment ~path ~clock:_ ~model:_ ~inject:_ = @@ -965,7 +1047,8 @@ let rec gather ; apply_action = unusable_apply_action ; reset = reset_unit_model ; run - ; can_contain_path = true + ; may_contain_path = Yes_or_maybe + ; may_contain_lifecycle = Yes_or_maybe }) ;; diff --git a/src/eval_sub.ml b/src/eval_sub.ml index 591d277f..8d18c765 100644 --- a/src/eval_sub.ml +++ b/src/eval_sub.ml @@ -8,12 +8,30 @@ module Computation_info = struct : ('r, unit) Computation.packed_info -> ('r, Environment.t option) Computation.packed_info = - fun (T { model; input; action; apply_action; reset; run; can_contain_path }) -> + fun (T + { model + ; input + ; action + ; apply_action + ; reset + ; run + ; may_contain_path + ; may_contain_lifecycle + }) -> let run ~environment ~path ~clock ~model ~inject = let%bind.Trampoline snapshot, () = run ~environment ~path ~clock ~model ~inject in Trampoline.return (snapshot, (None : Environment.t option)) in - Computation.T { model; input; action; apply_action; reset; run; can_contain_path } + Computation.T + { model + ; input + ; action + ; apply_action + ; reset + ; run + ; may_contain_path + ; may_contain_lifecycle + } ;; (* Produces a "[unit]-extra" [Computation.packed_info] from an [eval_sub]-custom packed @@ -24,14 +42,31 @@ module Computation_info = struct -> ('r, unit) Computation.packed_info = fun (Computation.T - { model; input; action; apply_action; reset; run; can_contain_path }) -> + { model + ; input + ; action + ; apply_action + ; reset + ; run + ; may_contain_path + ; may_contain_lifecycle + }) -> let run ~environment ~path ~clock ~model ~inject = let%bind.Trampoline snapshot, (_ : Environment.t option) = run ~environment ~path ~clock ~model ~inject in Trampoline.return (snapshot, ()) in - Computation.T { model; input; action; apply_action; reset; run; can_contain_path } + Computation.T + { model + ; input + ; action + ; apply_action + ; reset + ; run + ; may_contain_path + ; may_contain_lifecycle + } ;; end @@ -39,6 +74,12 @@ let ( >>> ) f inject b = inject (f b) let wrap_sub_from inject = Action.sub_from >>> inject let wrap_sub_into inject = Action.sub_into >>> inject +let both_use_path (a : May_contain.Path.t) (b : May_contain.Path.t) = + match a, b with + | Yes_or_maybe, Yes_or_maybe -> true + | _ -> false +;; + module Thread_env = struct (* values of this type are used to control if the environment of a sub is threaded into the next sub in a chain, or if there's no threading (which is the standard @@ -108,8 +149,11 @@ let baseline in model_from, model_into in - let both_use_path = info_from.can_contain_path && info_into.can_contain_path in + let both_use_path = + both_use_path info_from.may_contain_path info_into.may_contain_path + in let run ~environment ~path ~clock ~model ~inject = + annotate Model model; let%bind.Trampoline from, maybe_env = let model = Incr.map model ~f:Tuple2.get1 in let path = if both_use_path then Path.append path Path.Elem.Subst_from else path in @@ -145,7 +189,12 @@ let baseline ; apply_action ; run ; reset - ; can_contain_path = info_from.can_contain_path || info_into.can_contain_path + ; may_contain_path = + May_contain.Path.merge info_from.may_contain_path info_into.may_contain_path + ; may_contain_lifecycle = + May_contain.Lifecycle.merge + info_from.may_contain_lifecycle + info_into.may_contain_lifecycle } ;; @@ -158,7 +207,9 @@ let from_stateless ~(thread_environment : thread_env Thread_env.t) : (_, thread_env) Computation.packed_info = - let both_use_path = info_from.can_contain_path && info_into.can_contain_path in + let both_use_path = + both_use_path info_from.may_contain_path info_into.may_contain_path + in let run ~environment ~path ~clock ~model ~inject = let%bind.Trampoline from, maybe_env = let path = if both_use_path then Path.append path Path.Elem.Subst_from else path in @@ -191,7 +242,12 @@ let from_stateless ; action = info_into.action ; apply_action = info_into.apply_action ; reset = info_into.reset - ; can_contain_path = info_from.can_contain_path || info_into.can_contain_path + ; may_contain_path = + May_contain.Path.merge info_from.may_contain_path info_into.may_contain_path + ; may_contain_lifecycle = + May_contain.Lifecycle.merge + info_from.may_contain_lifecycle + info_into.may_contain_lifecycle } ;; @@ -204,7 +260,9 @@ let into_stateless ~(thread_environment : thread_env Thread_env.t) : (_, thread_env) Computation.packed_info = - let both_use_path = info_from.can_contain_path && info_into.can_contain_path in + let both_use_path = + both_use_path info_from.may_contain_path info_into.may_contain_path + in let run ~environment ~path ~clock ~model ~inject = let%bind.Trampoline from, maybe_env = let path = if both_use_path then Path.append path Path.Elem.Subst_from else path in @@ -237,7 +295,12 @@ let into_stateless ; action = info_from.action ; apply_action = info_from.apply_action ; reset = info_from.reset - ; can_contain_path = info_from.can_contain_path || info_into.can_contain_path + ; may_contain_path = + May_contain.Path.merge info_from.may_contain_path info_into.may_contain_path + ; may_contain_lifecycle = + May_contain.Lifecycle.merge + info_from.may_contain_lifecycle + info_into.may_contain_lifecycle } ;; diff --git a/src/fix_transform.ml b/src/fix_transform.ml index 14793226..491863f2 100644 --- a/src/fix_transform.ml +++ b/src/fix_transform.ml @@ -51,9 +51,9 @@ module Make let acc, up1, map = User.transform_v down acc t.map in let%bind acc, up2, by = User.transform_c down acc t.by in return (acc, combine_up up1 up2, Computation.Assoc_on { t with map; by }) - | Assoc_simpl { map; by; can_contain_path } -> + | Assoc_simpl { map; by; may_contain_path } -> let acc, up, map = User.transform_v down acc map in - return (acc, up, Computation.Assoc_simpl { map; by; can_contain_path }) + return (acc, up, Computation.Assoc_simpl { map; by; may_contain_path }) | Switch { match_; arms; here } -> let acc, up1, match_ = User.transform_v down acc match_ in let acc_and_upn_and_arms = diff --git a/src/may_contain.ml b/src/may_contain.ml new file mode 100644 index 00000000..397e760e --- /dev/null +++ b/src/may_contain.ml @@ -0,0 +1,16 @@ +open! Core + +module T = struct + type t = + | Yes_or_maybe + | No + + let merge a b = + match a, b with + | Yes_or_maybe, _ | _, Yes_or_maybe -> Yes_or_maybe + | No, No -> No + ;; +end + +module Lifecycle = T +module Path = T diff --git a/src/may_contain.mli b/src/may_contain.mli new file mode 100644 index 00000000..fac38249 --- /dev/null +++ b/src/may_contain.mli @@ -0,0 +1,12 @@ +open! Core + +module type S := sig + type t = + | Yes_or_maybe + | No + + val merge : t -> t -> t +end + +module Lifecycle : S +module Path : S diff --git a/src/proc_intf.ml b/src/proc_intf.ml index ddb636be..bda85618 100644 --- a/src/proc_intf.ml +++ b/src/proc_intf.ml @@ -892,16 +892,17 @@ module type S = sig end module Dynamic_scope : sig - (** This module implements dynamic variable scoping. Once a + (** This module implements dynamic variable scoping. Once a dynamic variable is created, you can store values in it, and lookup those same values. A lookup will find the nearest-most - grandparent [set_within] call. *) + parent "unreverted" [set] call where a "set" can be "reverted" + with [set']'s [revert]. *) type 'a t (** Creates a new variable for use with the rest of the functions. It is critically important that the exact same [Dynamic_scope.t] is used - in calls to [set_within] and the corresponding [lookup*]. *) + in calls to [set] and the corresponding [lookup]. *) val create : ?sexp_of:('a -> Sexp.t) -> name:string -> fallback:'a -> unit -> 'a t (** Creates a variable which is derived from another. Typically this is used to diff --git a/test/of_bonsai_itself/test_cont_bonsai.ml b/test/of_bonsai_itself/test_cont_bonsai.ml index 7114fdeb..4ea25443 100644 --- a/test/of_bonsai_itself/test_cont_bonsai.ml +++ b/test/of_bonsai_itself/test_cont_bonsai.ml @@ -2620,7 +2620,8 @@ let%expect_test "let syntax is collapsed upon eval" = ; run ; apply_action = _ ; reset = _ - ; can_contain_path = _ + ; may_contain_path = _ + ; may_contain_lifecycle = _ }) = computation |> pre_process |> gather @@ -5596,6 +5597,7 @@ let%expect_test "pipe" = ;; let%expect_test "multi-thunk" = + (* this test is rampantly nondeterministic *) let module Id = Core.Unique_id.Int () in let id graph = Bonsai.Expert.thunk graph ~f:(fun () -> @@ -5612,7 +5614,7 @@ let%expect_test "multi-thunk" = [%expect {| pulling id! pulling id! - "1 0" + "0 1" |}] ;; @@ -5638,32 +5640,30 @@ let%expect_test "evaluation of pure values under a match%sub" = | false -> return (-1) in let handle = Handle.create (Result_spec.sexp (module Int)) component in - (* Even though [determines_use] is false in this and other cases, we do work - unnecessarily. *) + (* In the past, even though [determines_use] is false in this and other cases, work + was performed unnecessarily. This is no longer true, but we keep this + around as a regression test *) Handle.show handle; [%expect {| - ("doing work" (depending_on 0)) -1 activating! |}]; Bonsai.Var.set determines_use true; Handle.show handle; - [%expect {| 0 |}]; + (* this is the only place that "doing work" should be printed *) + [%expect {| + ("doing work" (depending_on 0)) + 0 + |}]; Bonsai.Var.set determines_use false; Handle.show handle; [%expect {| -1 |}]; Bonsai.Var.set depending_on 1; Handle.show handle; - [%expect {| - ("doing work" (depending_on 1)) - -1 - |}]; + [%expect {| -1 |}]; Bonsai.Var.set depending_on 2; Handle.show handle; - [%expect {| - ("doing work" (depending_on 2)) - -1 - |}] + [%expect {| -1 |}] ;; let%expect_test "evaluation of pure values under an assoc" = diff --git a/test/of_bonsai_itself/test_dot.ml b/test/of_bonsai_itself/test_dot.ml deleted file mode 100644 index 24c978da..00000000 --- a/test/of_bonsai_itself/test_dot.ml +++ /dev/null @@ -1,682 +0,0 @@ -open! Core -open! Import -module Bonsai_lib = Bonsai -open Bonsai_lib -open Bonsai.Let_syntax - -let line_number_regex = Re.Str.regexp ":[0-9]+:[0-9]+" -let to_dot c = c |> Bonsai.Debug.to_dot |> Re.Str.global_replace line_number_regex "" - -let print_graph c = - let content = to_dot c in - let out = Stdlib.open_out "/tmp/foo" in - Stdlib.output_string out content; - Stdlib.flush out; - assert (Stdlib.Sys.command "graph-easy /tmp/foo --from graphviz --as boxart" = 0) -;; - -let%test_module ("regression" [@tags "no-js"]) = - (module struct - module State = struct - type t = - { a : int - ; b : int - ; c : int - } - [@@deriving fields ~getters] - end - - let%expect_test "" = - let state_var = Bonsai.Var.create { State.a = 2; b = 3; c = 4 } in - let state = Bonsai.Var.value state_var in - let a = Value.map state ~f:State.a in - let component b = - let%arr a = a - and b = b in - printf "Recomputing ; a = %d\n" a; - a + b - in - let _this_is_normal = - let c = component (Value.map state ~f:State.b) in - print_graph c; - [%expect - {| - ┌──────┐ - │ incr │ - └──────┘ - │ - │ - ▼ - ┌──────┐ - │ mapn │ - └──────┘ - │ - │ - ▼ - ┌──────┐ - │ mapn │ ◀┐ - └──────┘ │ - │ │ - │ │ - ▼ │ - ┌──────┐ │ - │ read │ │ - └──────┘ │ - ┌──────┐ │ - │ incr │ │ - └──────┘ │ - │ │ - │ │ - ▼ │ - ┌──────┐ │ - │ mapn │ ─┘ - └──────┘ - |}] - in - let _this_is_weird = - let c = component (Value.return 3) in - print_graph c; - [%expect - {| - ┌──────┐ - │ incr │ - └──────┘ - │ - │ - ▼ - ┌──────┐ - │ mapn │ - └──────┘ - │ - │ - ▼ - ┌──────┐ - │ mapn │ - └──────┘ - │ - │ - ▼ - ┌──────┐ - │ mapn │ - └──────┘ - │ - │ - ▼ - ┌──────┐ - │ read │ - └──────┘ - |}] - in - () - ;; - end) -;; - -let%expect_test ("map7 dot file" [@tags "no-js"]) = - let c = - Bonsai.read - (let%map () = opaque_const_value () - and () = opaque_const_value () - and () = opaque_const_value () - and () = opaque_const_value () - and () = opaque_const_value () - and () = opaque_const_value () - and () = opaque_const_value () in - ()) - in - print_graph c; - [%expect - {| - ┌──────┐┌──────┐ - │ incr ││ incr │ - └──────┘└──────┘ - │ │ - │ │ - ▼ ▼ - ┌──────┐ ┌──────────────┐ ┌──────┐ - │ incr │ ──▶ │ │ ◀── │ incr │ - └──────┘ │ mapn │ └──────┘ - ┌──────┐ │ │ ┌──────┐ - │ incr │ ──▶ │ │ ◀── │ incr │ - └──────┘ └──────────────┘ └──────┘ - │ ▲ - │ │ - ▼ │ - ┌──────┐┌──────┐ - │ read ││ incr │ - └──────┘└──────┘ - |}] -;; - -let%expect_test ("map7 dot file constant folding" [@tags "no-js"]) = - let c = - Bonsai.read - (let%map () = Value.return () - and () = Value.return () - and () = Value.return () - and () = Value.return () - and () = Value.return () - and () = Value.return () - and () = Value.return () in - ()) - in - print_graph c; - [%expect - {| - ┌───────┐ - │ const │ - └───────┘ - │ - │ - ▼ - ┌───────┐ - │ read │ - └───────┘ - |}] -;; - -let%expect_test ("map-10 dot file" [@tags "no-js"]) = - let c = - Bonsai.read - (let%map _1 : unit Value.t = opaque_const_value () - and _2 : unit Value.t = opaque_const_value () - and _3 : unit Value.t = opaque_const_value () - and _4 : unit Value.t = opaque_const_value () - and _5 : unit Value.t = opaque_const_value () - and _6 : unit Value.t = opaque_const_value () - and _7 : unit Value.t = opaque_const_value () - and _8 : unit Value.t = opaque_const_value () - and _9 : unit Value.t = opaque_const_value () - and _10 : unit Value.t = opaque_const_value () in - ()) - in - print_graph c; - [%expect - {| - ┌──────┐ ┌──────┐ - │ incr │ │ incr │ - └──────┘ └──────┘ - │ │ - │ │ - ▼ ▼ - ┌──────┐ ┌───────────────────┐ ┌──────┐ - │ incr │ ──▶ │ │ ◀── │ incr │ - └──────┘ │ mapn │ └──────┘ - ┌──────┐ │ │ ┌──────┐ - │ incr │ ──▶ │ │ ◀── │ incr │ - └──────┘ └───────────────────┘ └──────┘ - │ ▲ - │ │ - ▼ │ - ┌──────┐ │ - │ read │ │ - └──────┘ │ - ┌──────┐ │ - │ incr │ │ - └──────┘ │ - │ │ - │ │ - ▼ │ - ┌──────┐ ┌───────────────────┐ - │ incr │ ──▶ │ mapn │ - └──────┘ └───────────────────┘ - ▲ ▲ - │ │ - │ │ - ┌──────┐ ┌──────┐ - │ incr │ │ incr │ - └──────┘ └──────┘ - |}] -;; - -let%expect_test ("map-10 dot file constant folding optimization" [@tags "no-js"]) = - let c = - Bonsai.read - (let%map _1 : unit Value.t = Value.return () - and _2 : unit Value.t = Value.return () - and _3 : unit Value.t = Value.return () - and _4 : unit Value.t = Value.return () - and _5 : unit Value.t = Value.return () - and _6 : unit Value.t = Value.return () - and _7 : unit Value.t = Value.return () - and _8 : unit Value.t = Value.return () - and _9 : unit Value.t = Value.return () - and _10 : unit Value.t = Value.return () in - ()) - in - print_graph c; - [%expect - {| - ┌───────┐ - │ const │ - └───────┘ - │ - │ - ▼ - ┌───────┐ - │ read │ - └───────┘ - |}] -;; - -let%expect_test ("subst dot constant folding" [@tags "no-js"]) = - let c = - let%sub a = Bonsai.state 0 ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] in - let%sub b = opaque_const () in - let%sub c = return (Value.both a b) in - return (Value.both a c) - in - print_graph c; - [%expect - {| - ┌─────────────────┐ - │ incr │ - └─────────────────┘ - │ - │ - ▼ - ┌─────────────────┐ - │ read │ - └─────────────────┘ - │ - │ - │ - ┌─────────────────┐ - │ subst │ - └─────────────────┘ - │ - │ - ▼ - ┌─────────────────┐ - │ mapn │ ◀┐ - └─────────────────┘ │ - │ │ - │ │ - ▼ │ - ┌─────────────────┐ │ - │ read │ │ - └─────────────────┘ │ - │ │ - │ │ - │ │ - ┌─────────────────┐ │ - │ subst │ │ - └─────────────────┘ │ - │ │ - │ │ - ▼ │ - ┌─────────────────┐ │ - ┌▶ │ mapn │ │ - │ └─────────────────┘ │ - │ │ │ - │ │ │ - │ ▼ │ - │ ┌─────────────────┐ │ - │ │ read │ │ - │ └─────────────────┘ │ - │ ┌─────────────────┐ │ - │ │ {state machine} │ │ - │ └─────────────────┘ │ - │ │ │ - │ │ │ - │ │ │ - │ ┌─────────────────┐ │ - └─ │ subst │ ─┘ - └─────────────────┘ - |}] -;; - -let%expect_test ("subst dot" [@tags "no-js"]) = - let c = - let%sub a = Bonsai.state 0 ~sexp_of_model:[%sexp_of: Int.t] ~equal:[%equal: Int.t] in - let%sub b = opaque_const () in - let%sub c = return (Value.both a b) in - return (Value.both a c) - in - print_graph c; - [%expect - {| - ┌─────────────────┐ - │ incr │ - └─────────────────┘ - │ - │ - ▼ - ┌─────────────────┐ - │ read │ - └─────────────────┘ - │ - │ - │ - ┌─────────────────┐ - │ subst │ - └─────────────────┘ - │ - │ - ▼ - ┌─────────────────┐ - │ mapn │ ◀┐ - └─────────────────┘ │ - │ │ - │ │ - ▼ │ - ┌─────────────────┐ │ - │ read │ │ - └─────────────────┘ │ - │ │ - │ │ - │ │ - ┌─────────────────┐ │ - │ subst │ │ - └─────────────────┘ │ - │ │ - │ │ - ▼ │ - ┌─────────────────┐ │ - ┌▶ │ mapn │ │ - │ └─────────────────┘ │ - │ │ │ - │ │ │ - │ ▼ │ - │ ┌─────────────────┐ │ - │ │ read │ │ - │ └─────────────────┘ │ - │ ┌─────────────────┐ │ - │ │ {state machine} │ │ - │ └─────────────────┘ │ - │ │ │ - │ │ │ - │ │ │ - │ ┌─────────────────┐ │ - └─ │ subst │ ─┘ - └─────────────────┘ - |}] -;; - -let%expect_test "model_resetter doesn't have a dash in the label name" = - let c = Bonsai.with_model_resetter (Bonsai.const ()) in - print_endline (to_dot c); - [%expect - {| - digraph { - with_model_resetter_0 [ style=filled, shape = "Mrecord", label = "with_model_resetter"; fillcolor = "#86E3CE"; ] - named_1 [ style=filled, shape = "circle", label = ""; fillcolor = "#000000"; width=.1, height=.1] - with_model_resetter_0 -> named_1 [dir=none]; - read_2 [ style=filled, shape = "Mrecord", label = "read"; fillcolor = "#86E3CE"; ] - mapn_3 [ style=filled, shape = "oval", label = "mapn"; fillcolor = "#FFDD94"; ] - named_1 -> mapn_3; - mapn_3 -> read_2; - read_2 -> with_model_resetter_0; - } - |}] -;; - -let%expect_test ("dynamic scope" [@tags "no-js"]) = - let id = Bonsai.Dynamic_scope.create ~name:"my-id" ~fallback:"no" () in - let c = - Bonsai.Dynamic_scope.set - id - (Value.return "hello") - ~inside: - (let%sub _ = Bonsai.Dynamic_scope.lookup id in - let%sub _ = Bonsai.Dynamic_scope.lookup id in - Bonsai.const ()) - in - print_graph c; - [%expect - {| - ┌─────────┐ - │ const │ - └─────────┘ - │ - │ - ▼ - ┌─────────┐ - │ dyn_set │ - └─────────┘ - │ - │ - │ - ┌───────┐ ┌─────────┐ - │ fetch │ ◀── │ named_2 │ - └───────┘ └─────────┘ - │ │ - │ │ - │ ▼ - ┌───────┐ ┌─────────┐ - │ subst │ │ fetch │ - └───────┘ └─────────┘ - │ │ - │ │ - ▼ │ - ┌───────┐ ┌─────────┐ - │ mapn │ │ subst │ - └───────┘ └─────────┘ - │ │ - │ │ - ▼ ▼ - ┌───────┐ ┌─────────┐ - │ read │ │ mapn │ - └───────┘ └─────────┘ - │ │ - │ │ - │ ▼ - ┌───────┐ ┌─────────┐ - │ subst │ │ read │ - └───────┘ └─────────┘ - │ - │ - │ - ┌─────────┐ - │ subst │ - └─────────┘ - ┌─────────┐ - │ const │ - └─────────┘ - │ - │ - ▼ - ┌─────────┐ - │ read │ - └─────────┘ - |}] -;; - -let%expect_test ("dynamic scope (with reverter)" [@tags "no-js"]) = - let id = Bonsai.Dynamic_scope.create ~name:"my-id" ~fallback:"no" () in - let c = - Bonsai.Dynamic_scope.set' id (Value.return "hello") ~f:(fun _ -> - let%sub _ = Bonsai.Dynamic_scope.lookup id in - let%sub _ = Bonsai.Dynamic_scope.lookup id in - Bonsai.const ()) - in - print_graph c; - [%expect - {| - ┌─────────┐ - │ const │ - └─────────┘ - │ - │ - ▼ - ┌─────────┐ - │ read │ - └─────────┘ - ┌─────────┐ - │ const │ - └─────────┘ - │ - │ - ▼ - ┌─────────┐ - │ dyn_set │ - └─────────┘ - │ - │ - │ - ┌───────┐ ┌─────────┐ ┌───────┐ - │ fetch │ ◀── │ named_2 │ ──▶ │ fetch │ - └───────┘ └─────────┘ └───────┘ - │ │ │ - │ │ │ - │ ▼ │ - ┌───────┐ ┌─────────┐ ┌───────┐ - │ subst │ │ fetch │ │ subst │ - └───────┘ └─────────┘ └───────┘ - │ │ - │ │ - │ ▼ - ┌─────────┐ ┌───────┐ - │ subst │ │ mapn │ - └─────────┘ └───────┘ - │ │ - │ │ - ▼ ▼ - ┌─────────┐ ┌───────┐ - │ mapn │ │ read │ - └─────────┘ └───────┘ - │ │ - │ │ - ▼ │ - ┌─────────┐ ┌───────┐ - │ read │ │ subst │ - └─────────┘ └───────┘ - │ - │ - │ - ┌─────────┐ - │ subst │ - └─────────┘ - |}] -;; - -let%expect_test ("arrow-syntax" [@tags "no-js"]) = - let component = - let%sub a = opaque_const "hi" in - let%sub b = opaque_const 5 in - let%arr a = a - and b = b in - sprintf "%s %d" a b - in - print_graph component; - [%expect - {| - ┌───────┐ - │ incr │ - └───────┘ - │ - │ - ▼ - ┌───────┐ - │ read │ - └───────┘ - │ - │ - │ - ┌───────┐ - │ subst │ - └───────┘ - │ - │ - ▼ - ┌───────┐ - │ mapn │ ◀┐ - └───────┘ │ - │ │ - │ │ - ▼ │ - ┌───────┐ │ - │ read │ │ - └───────┘ │ - ┌───────┐ │ - │ incr │ │ - └───────┘ │ - │ │ - │ │ - ▼ │ - ┌───────┐ │ - │ read │ │ - └───────┘ │ - │ │ - │ │ - │ │ - ┌───────┐ │ - │ subst │ ─┘ - └───────┘ - |}] -;; - -let%expect_test ("both-constant-opt" [@tags "no-js"]) = - print_graph - (let%arr a = Bonsai.Value.return 1 - and b = Bonsai.Value.return 1 in - sprintf "%d %d" a b); - [%expect - {| - ┌───────┐ - │ const │ - └───────┘ - │ - │ - ▼ - ┌───────┐ - │ read │ - └───────┘ - |}]; - print_graph - (let%arr a = opaque_const_value 1 - and b = Bonsai.Value.return 1 in - sprintf "%d %d" a b); - [%expect - {| - ┌──────┐ - │ incr │ - └──────┘ - │ - │ - ▼ - ┌──────┐ - │ mapn │ - └──────┘ - │ - │ - ▼ - ┌──────┐ - │ mapn │ - └──────┘ - │ - │ - ▼ - ┌──────┐ - │ read │ - └──────┘ - |}]; - print_graph - (let%arr a = Bonsai.Value.return 1 - and b = opaque_const_value 1 in - sprintf "%d %d" a b); - [%expect - {| - ┌──────┐ - │ incr │ - └──────┘ - │ - │ - ▼ - ┌──────┐ - │ mapn │ - └──────┘ - │ - │ - ▼ - ┌──────┐ - │ mapn │ - └──────┘ - │ - │ - ▼ - ┌──────┐ - │ read │ - └──────┘ - |}] -;; diff --git a/test/of_bonsai_itself/test_dot.mli b/test/of_bonsai_itself/test_dot.mli deleted file mode 100644 index 537e8f54..00000000 --- a/test/of_bonsai_itself/test_dot.mli +++ /dev/null @@ -1 +0,0 @@ -(*_ This file intentionally left blank *) diff --git a/test/of_bonsai_itself/test_linter.ml b/test/of_bonsai_itself/test_linter.ml index a14ae952..aedd818e 100644 --- a/test/of_bonsai_itself/test_linter.ml +++ b/test/of_bonsai_itself/test_linter.ml @@ -128,7 +128,7 @@ let%expect_test "map2_with_unfolded_constants_and_sm1_with_const_input_both_warn test_lint c; [%expect {| - _none_:0:0: state_machine1 can be optimized to a state_machine0 + lib/bonsai/test/of_bonsai_itself/test_linter.ml:2:4: state_machine1 can be optimized to a state_machine0 lib/bonsai/test/of_bonsai_itself/test_linter.ml:11:4: unfolded constant |}] ;; diff --git a/test/of_bonsai_itself/test_model_action_and_input_shapes.ml b/test/of_bonsai_itself/test_model_action_and_input_shapes.ml index 07828802..2c7c6468 100644 --- a/test/of_bonsai_itself/test_model_action_and_input_shapes.ml +++ b/test/of_bonsai_itself/test_model_action_and_input_shapes.ml @@ -259,8 +259,8 @@ let%test_module "assoc" = (shapes ( (model unit) (action (Assoc "key id" (Leaf Nothing.t))) (input unit))) (incr_graph ( - (nodes 24) - (edges 33)))) + (nodes 15) + (edges 18)))) ("with optimizations" (shapes ((model unit) (action (Leaf Nothing.t)) (input unit))) @@ -284,8 +284,8 @@ let%test_module "assoc" = (action (Assoc "key id" (Leaf proc_min.ml))) (input unit))) (incr_graph ( - (nodes 24) - (edges 33)))) + (nodes 15) + (edges 18)))) |}] ;; @@ -303,8 +303,8 @@ let%test_module "assoc" = (action (Assoc "key id" (Leaf proc_min.ml))) (input input))) (incr_graph ( - (nodes 24) - (edges 33)))) + (nodes 15) + (edges 18)))) |}] ;; end) @@ -328,8 +328,8 @@ let%test_module "assoc_on" = (action (Assoc "io key id" "model key id" (Leaf Nothing.t))) (input unit))) (incr_graph ( - (nodes 22) - (edges 31)))) + (nodes 13) + (edges 16)))) ("with optimizations" (shapes ((model unit) (action (Leaf Nothing.t)) (input unit))) @@ -355,29 +355,8 @@ let%test_module "assoc_on" = (action (Assoc "io key id" "model key id" (Leaf proc_min.ml))) (input unit))) (incr_graph ( - (nodes 22) - (edges 31)))) - |}] - ;; - - let%expect_test "dynamic_state inside assoc_on" = - print - (Bonsai.Expert.assoc_on - (module Int) - (module Int) - (opaque_const_value Int.Map.empty) - ~get_model_key:(fun k _ -> k) - ~f:(fun _ _ -> stateful_dynamic_computation)); - [%expect - {| - ("with and without optimizations" - (shapes ( - (model proc_min.ml-model) - (action (Assoc "io key id" "model key id" (Leaf proc_min.ml))) - (input input))) - (incr_graph ( - (nodes 22) - (edges 31)))) + (nodes 13) + (edges 16)))) |}] ;; end) @@ -400,8 +379,8 @@ let%test_module "switch" = (action Switch) (input "enum input"))) (incr_graph ( - (nodes 12) - (edges 16)))) + (nodes 13) + (edges 15)))) |}] ;; @@ -420,8 +399,8 @@ let%test_module "switch" = (action Switch) (input "enum input"))) (incr_graph ( - (nodes 14) - (edges 19)))) + (nodes 15) + (edges 18)))) |}] ;; @@ -440,8 +419,8 @@ let%test_module "switch" = (action Switch) (input "enum input"))) (incr_graph ( - (nodes 14) - (edges 18)))) + (nodes 15) + (edges 17)))) |}] ;; @@ -460,8 +439,8 @@ let%test_module "switch" = (action Switch) (input "enum input"))) (incr_graph ( - (nodes 14) - (edges 18)))) + (nodes 15) + (edges 17)))) |}] ;; end) @@ -484,8 +463,8 @@ let%test_module "optimizable switch" = (action Switch) (input "enum input"))) (incr_graph ( - (nodes 12) - (edges 16)))) + (nodes 13) + (edges 15)))) ("with optimizations" (shapes ((model unit) (action (Leaf Nothing.t)) (input unit))) @@ -510,8 +489,8 @@ let%test_module "optimizable switch" = (action Switch) (input "enum input"))) (incr_graph ( - (nodes 14) - (edges 19)))) + (nodes 15) + (edges 18)))) ("with optimizations" (shapes ((model proc_min.ml-model) (action (Leaf proc_min.ml)) (input unit))) @@ -536,8 +515,8 @@ let%test_module "optimizable switch" = (action Switch) (input "enum input"))) (incr_graph ( - (nodes 14) - (edges 18)))) + (nodes 15) + (edges 17)))) ("with optimizations" (shapes ( @@ -563,8 +542,8 @@ let%test_module "optimizable switch" = (action Switch) (input "enum input"))) (incr_graph ( - (nodes 14) - (edges 18)))) + (nodes 15) + (edges 17)))) ("with optimizations" (shapes ( @@ -590,8 +569,8 @@ let%test_module "optimizable switch" = (action Switch) (input "enum input"))) (incr_graph ( - (nodes 14) - (edges 19)))) + (nodes 15) + (edges 18)))) ("with optimizations" (shapes ((model proc_min.ml-model) (action (Leaf proc_min.ml)) (input unit))) diff --git a/test/of_bonsai_itself/test_proc_bonsai.ml b/test/of_bonsai_itself/test_proc_bonsai.ml index 013aa60f..c02cb722 100644 --- a/test/of_bonsai_itself/test_proc_bonsai.ml +++ b/test/of_bonsai_itself/test_proc_bonsai.ml @@ -2556,7 +2556,8 @@ let%expect_test "let syntax is collapsed upon eval" = ; action ; run ; reset = _ - ; can_contain_path = _ + ; may_contain_lifecycle = _ + ; may_contain_path = _ }) = computation |> pre_process |> gather @@ -5607,6 +5608,9 @@ let%expect_test "pipe" = |}] ;; +(* NOTE: The output of this test is sensitive to the order in which incrementals are + computed in a way that other tests are not. Thunk is basically non-deterministic, which + is why it's in the expert module. *) let%expect_test "multi-thunk" = let module Id = Core.Unique_id.Int () in let id = @@ -5624,7 +5628,7 @@ let%expect_test "multi-thunk" = [%expect {| pulling id! pulling id! - "1 0" + "0 1" |}] ;; @@ -5650,32 +5654,30 @@ let%expect_test "evaluation of pure values under a match%sub" = | false -> Bonsai.const (-1) in let handle = Handle.create (Result_spec.sexp (module Int)) component in - (* Even though [determines_use] is false in this and other cases, we do work - unnecessarily. *) + (* In the past, even though [determines_use] is false in this and other cases, work + was performed unnecessarily. This is no longer true, but we keep this + around as a regression test *) Handle.show handle; [%expect {| - ("doing work" (depending_on 0)) -1 activating! |}]; Bonsai.Var.set determines_use true; Handle.show handle; - [%expect {| 0 |}]; + (* this is the only place that "doing work" should be printed *) + [%expect {| + ("doing work" (depending_on 0)) + 0 + |}]; Bonsai.Var.set determines_use false; Handle.show handle; [%expect {| -1 |}]; Bonsai.Var.set depending_on 1; Handle.show handle; - [%expect {| - ("doing work" (depending_on 1)) - -1 - |}]; + [%expect {| -1 |}]; Bonsai.Var.set depending_on 2; Handle.show handle; - [%expect {| - ("doing work" (depending_on 2)) - -1 - |}] + [%expect {| -1 |}] ;; let%expect_test "evaluation of pure values under an assoc" = @@ -5731,8 +5733,7 @@ let%expect_test "evaluation of pure values under an assoc" = [%expect {| () |}] ;; -let%expect_test "evaluation of pure values as an input to an assoc (with a state in the \ - assoc)" +let%expect_test {|evaluation of pure values as an input to an assoc (with a state in the assoc)|} = let depending_on = Bonsai.Var.create 0 in let determines_use = Bonsai.Var.create false in diff --git a/test/test_dot/src/test_instrumentation.ml b/test/test_dot/src/test_instrumentation.ml index 26224009..2a1529db 100644 --- a/test/test_dot/src/test_instrumentation.ml +++ b/test/test_dot/src/test_instrumentation.ml @@ -129,31 +129,31 @@ let%expect_test _ = [%expect {| tree: - 1_1 return -> _1 sub + 1_1 return -> _1 sub @ 1:2 1_2 incr -> 1_1 return - 2-1_1 return -> 2_1 sub + 2-1_1 return -> 2_1 sub @ 6:2 2-1_2 incr -> 2-1_1 return 2-2-1-1_1 named -> 2-2-1_2 map @ 6:2 - 2-2-1_1 return -> 2-2_1 sub + 2-2-1_1 return -> 2-2_1 sub @ 6:2 2-2-1_2 map @ 6:2 -> 2-2-1_1 return - 2-2-2_1 return -> 2-2_1 sub + 2-2-2_1 return -> 2-2_1 sub @ 6:2 2-2-2_2 named -> 2-2-2_1 return - 2-2_1 sub -> 2_1 sub - 2_1 sub -> _1 sub - _1 sub -> _0 + 2-2_1 sub @ 6:2 -> 2_1 sub @ 6:2 + 2_1 sub @ 6:2 -> _1 sub @ 1:2 + _1 sub @ 1:2 -> _0 dag: - 1_1 return -> 2-2-2_2 named, _1 sub + 1_1 return -> 2-2-2_2 named, _1 sub @ 1:2 1_2 incr -> 1_1 return - 2-1_1 return -> 2-2-1-1_1 named, 2_1 sub + 2-1_1 return -> 2-2-1-1_1 named, 2_1 sub @ 6:2 2-1_2 incr -> 2-1_1 return 2-2-1-1_1 named -> 2-2-1_2 map @ 6:2 - 2-2-1_1 return -> 2-2_1 sub + 2-2-1_1 return -> 2-2_1 sub @ 6:2 2-2-1_2 map @ 6:2 -> 2-2-1_1 return - 2-2-2_1 return -> 2-2_1 sub + 2-2-2_1 return -> 2-2_1 sub @ 6:2 2-2-2_2 named -> 2-2-2_1 return - 2-2_1 sub -> 2_1 sub - 2_1 sub -> _1 sub - _1 sub -> _0 + 2-2_1 sub @ 6:2 -> 2_1 sub @ 6:2 + 2_1 sub @ 6:2 -> _1 sub @ 1:2 + _1 sub @ 1:2 -> _0 |}] ;; @@ -178,19 +178,19 @@ let%expect_test _ = [%expect {| tree: - 1-1_1 constant -> 1_2 map @ 6:2 - 1_1 return -> _1 sub + 1-1_1 constant @ 6:2 -> 1_2 map @ 6:2 + 1_1 return -> _1 sub @ 6:2 1_2 map @ 6:2 -> 1_1 return - 2_1 return -> _1 sub + 2_1 return -> _1 sub @ 6:2 2_2 constant -> 2_1 return - _1 sub -> _0 + _1 sub @ 6:2 -> _0 dag: - 1-1_1 constant -> 1_2 map @ 6:2 - 1_1 return -> _1 sub + 1-1_1 constant @ 6:2 -> 1_2 map @ 6:2 + 1_1 return -> _1 sub @ 6:2 1_2 map @ 6:2 -> 1_1 return - 2_1 return -> _1 sub + 2_1 return -> _1 sub @ 6:2 2_2 constant -> 2_1 return - _1 sub -> _0 + _1 sub @ 6:2 -> _0 |}] ;; @@ -214,15 +214,15 @@ let%expect_test _ = [%expect {| tree: - 1_1 return -> _1 sub + 1_1 return -> _1 sub @ 1:2 1_2 incr -> 1_1 return - 2-1_1 return -> 2_1 sub + 2-1_1 return -> 2_1 sub @ 2:2 2-1_2 incr -> 2-1_1 return - 2-2-1_1 return -> 2-2_1 sub + 2-2-1_1 return -> 2-2_1 sub @ 3:2 2-2-1_2 incr -> 2-2-1_1 return - 2-2-2-1_1 return -> 2-2-2_1 sub + 2-2-2-1_1 return -> 2-2-2_1 sub @ 4:2 2-2-2-1_2 incr -> 2-2-2-1_1 return - 2-2-2-2-1_1 return -> 2-2-2-2_1 sub + 2-2-2-2-1_1 return -> 2-2-2-2_1 sub @ 5:2 2-2-2-2-1_2 incr -> 2-2-2-2-1_1 return 2-2-2-2-2-1-1_1 named -> 2-2-2-2-2-1_1 both 2-2-2-2-2-1-2-1_1 named -> 2-2-2-2-2-1-2_1 both @@ -233,23 +233,23 @@ let%expect_test _ = 2-2-2-2-2-1-2-2_1 both -> 2-2-2-2-2-1-2_1 both 2-2-2-2-2-1-2_1 both -> 2-2-2-2-2-1_1 both 2-2-2-2-2-1_1 both -> 2-2-2-2-2_2 map @ 6:2 - 2-2-2-2-2_1 return -> 2-2-2-2_1 sub + 2-2-2-2-2_1 return -> 2-2-2-2_1 sub @ 5:2 2-2-2-2-2_2 map @ 6:2 -> 2-2-2-2-2_1 return - 2-2-2-2_1 sub -> 2-2-2_1 sub - 2-2-2_1 sub -> 2-2_1 sub - 2-2_1 sub -> 2_1 sub - 2_1 sub -> _1 sub - _1 sub -> _0 + 2-2-2-2_1 sub @ 5:2 -> 2-2-2_1 sub @ 4:2 + 2-2-2_1 sub @ 4:2 -> 2-2_1 sub @ 3:2 + 2-2_1 sub @ 3:2 -> 2_1 sub @ 2:2 + 2_1 sub @ 2:2 -> _1 sub @ 1:2 + _1 sub @ 1:2 -> _0 dag: - 1_1 return -> 2-2-2-2-2-1-1_1 named, _1 sub + 1_1 return -> 2-2-2-2-2-1-1_1 named, _1 sub @ 1:2 1_2 incr -> 1_1 return - 2-1_1 return -> 2-2-2-2-2-1-2-1_1 named, 2_1 sub + 2-1_1 return -> 2-2-2-2-2-1-2-1_1 named, 2_1 sub @ 2:2 2-1_2 incr -> 2-1_1 return - 2-2-1_1 return -> 2-2-2-2-2-1-2-2-1_1 named, 2-2_1 sub + 2-2-1_1 return -> 2-2-2-2-2-1-2-2-1_1 named, 2-2_1 sub @ 3:2 2-2-1_2 incr -> 2-2-1_1 return - 2-2-2-1_1 return -> 2-2-2-2-2-1-2-2-2-1_1 named, 2-2-2_1 sub + 2-2-2-1_1 return -> 2-2-2-2-2-1-2-2-2-1_1 named, 2-2-2_1 sub @ 4:2 2-2-2-1_2 incr -> 2-2-2-1_1 return - 2-2-2-2-1_1 return -> 2-2-2-2-2-1-2-2-2-2_1 named, 2-2-2-2_1 sub + 2-2-2-2-1_1 return -> 2-2-2-2-2-1-2-2-2-2_1 named, 2-2-2-2_1 sub @ 5:2 2-2-2-2-1_2 incr -> 2-2-2-2-1_1 return 2-2-2-2-2-1-1_1 named -> 2-2-2-2-2-1_1 both 2-2-2-2-2-1-2-1_1 named -> 2-2-2-2-2-1-2_1 both @@ -260,13 +260,13 @@ let%expect_test _ = 2-2-2-2-2-1-2-2_1 both -> 2-2-2-2-2-1-2_1 both 2-2-2-2-2-1-2_1 both -> 2-2-2-2-2-1_1 both 2-2-2-2-2-1_1 both -> 2-2-2-2-2_2 map @ 6:2 - 2-2-2-2-2_1 return -> 2-2-2-2_1 sub + 2-2-2-2-2_1 return -> 2-2-2-2_1 sub @ 5:2 2-2-2-2-2_2 map @ 6:2 -> 2-2-2-2-2_1 return - 2-2-2-2_1 sub -> 2-2-2_1 sub - 2-2-2_1 sub -> 2-2_1 sub - 2-2_1 sub -> 2_1 sub - 2_1 sub -> _1 sub - _1 sub -> _0 + 2-2-2-2_1 sub @ 5:2 -> 2-2-2_1 sub @ 4:2 + 2-2-2_1 sub @ 4:2 -> 2-2_1 sub @ 3:2 + 2-2_1 sub @ 3:2 -> 2_1 sub @ 2:2 + 2_1 sub @ 2:2 -> _1 sub @ 1:2 + _1 sub @ 1:2 -> _0 |}]; let handle = Handle.create (Result_spec.string (module Int)) c in Handle.show handle; @@ -292,39 +292,39 @@ let%expect_test "diamond" = [%expect {| tree: - 1_1 return -> _1 sub + 1_1 return -> _1 sub @ 1:2 1_2 incr -> 1_1 return 2-1-1_1 named -> 2-1_2 map - 2-1_1 return -> 2_1 sub + 2-1_1 return -> 2_1 sub @ 2:2 2-1_2 map -> 2-1_1 return 2-2-1-1_1 named -> 2-2-1_2 map - 2-2-1_1 return -> 2-2_1 sub + 2-2-1_1 return -> 2-2_1 sub @ 3:2 2-2-1_2 map -> 2-2-1_1 return 2-2-2-1-1_1 named -> 2-2-2-1_1 both 2-2-2-1-2_1 named -> 2-2-2-1_1 both 2-2-2-1_1 both -> 2-2-2_2 map @ 4:2 - 2-2-2_1 return -> 2-2_1 sub + 2-2-2_1 return -> 2-2_1 sub @ 3:2 2-2-2_2 map @ 4:2 -> 2-2-2_1 return - 2-2_1 sub -> 2_1 sub - 2_1 sub -> _1 sub - _1 sub -> _0 + 2-2_1 sub @ 3:2 -> 2_1 sub @ 2:2 + 2_1 sub @ 2:2 -> _1 sub @ 1:2 + _1 sub @ 1:2 -> _0 dag: - 1_1 return -> 2-2-1-1_1 named, 2-1-1_1 named, _1 sub + 1_1 return -> 2-2-1-1_1 named, 2-1-1_1 named, _1 sub @ 1:2 1_2 incr -> 1_1 return 2-1-1_1 named -> 2-1_2 map - 2-1_1 return -> 2-2-2-1-1_1 named, 2_1 sub + 2-1_1 return -> 2-2-2-1-1_1 named, 2_1 sub @ 2:2 2-1_2 map -> 2-1_1 return 2-2-1-1_1 named -> 2-2-1_2 map - 2-2-1_1 return -> 2-2-2-1-2_1 named, 2-2_1 sub + 2-2-1_1 return -> 2-2-2-1-2_1 named, 2-2_1 sub @ 3:2 2-2-1_2 map -> 2-2-1_1 return 2-2-2-1-1_1 named -> 2-2-2-1_1 both 2-2-2-1-2_1 named -> 2-2-2-1_1 both 2-2-2-1_1 both -> 2-2-2_2 map @ 4:2 - 2-2-2_1 return -> 2-2_1 sub + 2-2-2_1 return -> 2-2_1 sub @ 3:2 2-2-2_2 map @ 4:2 -> 2-2-2_1 return - 2-2_1 sub -> 2_1 sub - 2_1 sub -> _1 sub - _1 sub -> _0 + 2-2_1 sub @ 3:2 -> 2_1 sub @ 2:2 + 2_1 sub @ 2:2 -> _1 sub @ 1:2 + _1 sub @ 1:2 -> _0 |}] ;; @@ -443,19 +443,19 @@ let%expect_test "dynamic scope" = {| tree: 1_1 constant -> _1 store - 2-1_1 fetch -> 2_1 sub + 2-1_1 fetch -> 2_1 sub @ 6:7 2-2-1_1 named -> 2-2_2 map @ 7:7 - 2-2_1 return -> 2_1 sub + 2-2_1 return -> 2_1 sub @ 6:7 2-2_2 map @ 7:7 -> 2-2_1 return - 2_1 sub -> _1 store + 2_1 sub @ 6:7 -> _1 store _1 store -> _0 dag: 1_1 constant -> 2-1_1 fetch, _1 store - 2-1_1 fetch -> 2-2-1_1 named, 2_1 sub + 2-1_1 fetch -> 2-2-1_1 named, 2_1 sub @ 6:7 2-2-1_1 named -> 2-2_2 map @ 7:7 - 2-2_1 return -> 2_1 sub + 2-2_1 return -> 2_1 sub @ 6:7 2-2_2 map @ 7:7 -> 2-2_1 return - 2_1 sub -> _1 store + 2_1 sub @ 6:7 -> _1 store _1 store -> _0 |}] ;; @@ -542,23 +542,23 @@ let%expect_test "assoc" = [%expect {| tree: - 1_1 return -> _1 sub + 1_1 return -> _1 sub @ 1:2 1_2 incr -> 1_1 return 2-1_1 constant -> 2_1 assoc 2-2-1_1 named -> 2-2_2 map @ 6:6 2-2_1 return -> 2_1 assoc 2-2_2 map @ 6:6 -> 2-2_1 return - 2_1 assoc -> _1 sub - _1 sub -> _0 + 2_1 assoc -> _1 sub @ 1:2 + _1 sub @ 1:2 -> _0 dag: - 1_1 return -> 2-2-1_1 named, _1 sub + 1_1 return -> 2-2-1_1 named, _1 sub @ 1:2 1_2 incr -> 1_1 return 2-1_1 constant -> 2_1 assoc 2-2-1_1 named -> 2-2_2 map @ 6:6 2-2_1 return -> 2_1 assoc 2-2_2 map @ 6:6 -> 2-2_1 return - 2_1 assoc -> _1 sub - _1 sub -> _0 + 2_1 assoc -> _1 sub @ 1:2 + _1 sub @ 1:2 -> _0 |}]; let handle = Handle.create @@ -596,13 +596,13 @@ let%expect_test "assoc constant folding" = {| tree: 1_1 constant -> _1 assoc - 2-1_1 constant -> 2_2 map @ 6:6 + 2-1_1 constant @ 1:2 -> 2_2 map @ 6:6 2_1 return -> _1 assoc 2_2 map @ 6:6 -> 2_1 return _1 assoc -> _0 dag: 1_1 constant -> _1 assoc - 2-1_1 constant -> 2_2 map @ 6:6 + 2-1_1 constant @ 1:2 -> 2_2 map @ 6:6 2_1 return -> _1 assoc 2_2 map @ 6:6 -> 2_1 return _1 assoc -> _0 @@ -939,67 +939,67 @@ let%expect_test "name_used_twice" = [%expect {| tree: - 1-1_1 return -> 1_1 sub + 1-1_1 return -> 1_1 sub @ -5:2 1-1_2 incr -> 1-1_1 return 1-2-1-1_1 named -> 1-2-1_2 map @ -5:2 - 1-2-1_1 return -> 1-2_1 sub + 1-2-1_1 return -> 1-2_1 sub @ -5:2 1-2-1_2 map @ -5:2 -> 1-2-1_1 return - 1-2-2_1 return -> 1-2_1 sub + 1-2-2_1 return -> 1-2_1 sub @ -5:2 1-2-2_2 incr -> 1-2-2_1 return - 1-2_1 sub -> 1_1 sub - 1_1 sub -> _1 sub + 1-2_1 sub @ -5:2 -> 1_1 sub @ -5:2 + 1_1 sub @ -5:2 -> _1 sub @ 1:2 2-1-1_1 named -> 2-1_2 map @ 1:2 - 2-1_1 return -> 2_1 sub + 2-1_1 return -> 2_1 sub @ 1:2 2-1_2 map @ 1:2 -> 2-1_1 return - 2-2-1-1_1 return -> 2-2-1_1 sub + 2-2-1-1_1 return -> 2-2-1_1 sub @ -5:2 2-2-1-1_2 incr -> 2-2-1-1_1 return 2-2-1-2-1-1_1 named -> 2-2-1-2-1_2 map @ -5:2 - 2-2-1-2-1_1 return -> 2-2-1-2_1 sub + 2-2-1-2-1_1 return -> 2-2-1-2_1 sub @ -5:2 2-2-1-2-1_2 map @ -5:2 -> 2-2-1-2-1_1 return - 2-2-1-2-2_1 return -> 2-2-1-2_1 sub + 2-2-1-2-2_1 return -> 2-2-1-2_1 sub @ -5:2 2-2-1-2-2_2 incr -> 2-2-1-2-2_1 return - 2-2-1-2_1 sub -> 2-2-1_1 sub - 2-2-1_1 sub -> 2-2_1 sub + 2-2-1-2_1 sub @ -5:2 -> 2-2-1_1 sub @ -5:2 + 2-2-1_1 sub @ -5:2 -> 2-2_1 sub @ 2:2 2-2-2-1-1_1 named -> 2-2-2-1_2 map @ 2:2 - 2-2-2-1_1 return -> 2-2-2_1 sub + 2-2-2-1_1 return -> 2-2-2_1 sub @ 2:2 2-2-2-1_2 map @ 2:2 -> 2-2-2-1_1 return - 2-2-2-2_1 return -> 2-2-2_1 sub + 2-2-2-2_1 return -> 2-2-2_1 sub @ 2:2 2-2-2-2_2 constant -> 2-2-2-2_1 return - 2-2-2_1 sub -> 2-2_1 sub - 2-2_1 sub -> 2_1 sub - 2_1 sub -> _1 sub - _1 sub -> _0 + 2-2-2_1 sub @ 2:2 -> 2-2_1 sub @ 2:2 + 2-2_1 sub @ 2:2 -> 2_1 sub @ 1:2 + 2_1 sub @ 1:2 -> _1 sub @ 1:2 + _1 sub @ 1:2 -> _0 dag: - 1-1_1 return -> 1-2-1-1_1 named, 1_1 sub + 1-1_1 return -> 1-2-1-1_1 named, 1_1 sub @ -5:2 1-1_2 incr -> 1-1_1 return 1-2-1-1_1 named -> 1-2-1_2 map @ -5:2 - 1-2-1_1 return -> 1-2_1 sub + 1-2-1_1 return -> 1-2_1 sub @ -5:2 1-2-1_2 map @ -5:2 -> 1-2-1_1 return - 1-2-2_1 return -> 1-2_1 sub + 1-2-2_1 return -> 1-2_1 sub @ -5:2 1-2-2_2 incr -> 1-2-2_1 return - 1-2_1 sub -> 1_1 sub - 1_1 sub -> 2-1-1_1 named, _1 sub + 1-2_1 sub @ -5:2 -> 1_1 sub @ -5:2 + 1_1 sub @ -5:2 -> 2-1-1_1 named, _1 sub @ 1:2 2-1-1_1 named -> 2-1_2 map @ 1:2 - 2-1_1 return -> 2_1 sub + 2-1_1 return -> 2_1 sub @ 1:2 2-1_2 map @ 1:2 -> 2-1_1 return - 2-2-1-1_1 return -> 2-2-1-2-1-1_1 named, 2-2-1_1 sub + 2-2-1-1_1 return -> 2-2-1-2-1-1_1 named, 2-2-1_1 sub @ -5:2 2-2-1-1_2 incr -> 2-2-1-1_1 return 2-2-1-2-1-1_1 named -> 2-2-1-2-1_2 map @ -5:2 - 2-2-1-2-1_1 return -> 2-2-1-2_1 sub + 2-2-1-2-1_1 return -> 2-2-1-2_1 sub @ -5:2 2-2-1-2-1_2 map @ -5:2 -> 2-2-1-2-1_1 return - 2-2-1-2-2_1 return -> 2-2-1-2_1 sub + 2-2-1-2-2_1 return -> 2-2-1-2_1 sub @ -5:2 2-2-1-2-2_2 incr -> 2-2-1-2-2_1 return - 2-2-1-2_1 sub -> 2-2-1_1 sub - 2-2-1_1 sub -> 2-2-2-1-1_1 named, 2-2_1 sub + 2-2-1-2_1 sub @ -5:2 -> 2-2-1_1 sub @ -5:2 + 2-2-1_1 sub @ -5:2 -> 2-2-2-1-1_1 named, 2-2_1 sub @ 2:2 2-2-2-1-1_1 named -> 2-2-2-1_2 map @ 2:2 - 2-2-2-1_1 return -> 2-2-2_1 sub + 2-2-2-1_1 return -> 2-2-2_1 sub @ 2:2 2-2-2-1_2 map @ 2:2 -> 2-2-2-1_1 return - 2-2-2-2_1 return -> 2-2-2_1 sub + 2-2-2-2_1 return -> 2-2-2_1 sub @ 2:2 2-2-2-2_2 constant -> 2-2-2-2_1 return - 2-2-2_1 sub -> 2-2_1 sub - 2-2_1 sub -> 2_1 sub - 2_1 sub -> _1 sub - _1 sub -> _0 + 2-2-2_1 sub @ 2:2 -> 2-2_1 sub @ 2:2 + 2-2_1 sub @ 2:2 -> 2_1 sub @ 1:2 + 2_1 sub @ 1:2 -> _1 sub @ 1:2 + _1 sub @ 1:2 -> _0 |}] ;; diff --git a/uri_parsing/README.mdx b/uri_parsing/README.mdx index cbd17cd8..dc858b36 100644 --- a/uri_parsing/README.mdx +++ b/uri_parsing/README.mdx @@ -1,10 +1,9 @@ # URI Parsing -See the Bonsai Guide [chapter on urls](../bonsai/docs/guide/10-url-routing.mdx). +See the Bonsai Guide [chapter on urls](../bonsai/docs/how_to/uri_parsing.mdx). This library contains the "typed" parsing/unparsing module for URL Var. The reason that this library exists as a separate library is that URL Var has a JavaScript browser dependency which makes it unable to run outside of a JavaScript browser context. Splitting off the parsing/unparsing logic allows it to be used on native OCaml. - diff --git a/uri_parsing/src/uri_parsing.ml b/uri_parsing/src/uri_parsing.ml index 3cd530e4..0c85ecdb 100644 --- a/uri_parsing/src/uri_parsing.ml +++ b/uri_parsing/src/uri_parsing.ml @@ -2069,8 +2069,8 @@ module Parser = struct check_that_path_orders_are_ok (M.parser_for_variant v)) ;; - let check_more_path_parsing_allowed ~allowed = - if not allowed + let check_more_path_parsing_allowed ~has_seen_end_of_path = + if has_seen_end_of_path then raise_s [%message @@ -2080,52 +2080,89 @@ module Parser = struct ;; let rec check_that_with_remaining_path_has_no_path_parsers_after_it - : type a. ?allowed:bool -> a T.t -> unit + : type a. has_seen_end_of_path:bool -> a T.t -> bool = - fun ?(allowed = true) t -> - match t with - | Unit -> () - | Project { input; _ } -> - check_that_with_remaining_path_has_no_path_parsers_after_it ~allowed input - | Optional_query_fields { t; _ } -> - check_that_with_remaining_path_has_no_path_parsers_after_it ~allowed t - | From_query_required _ -> () - | From_query_optional _ -> () - | From_query_optional_with_default _ -> () - | From_query_many _ -> () - | From_path _ -> check_more_path_parsing_allowed ~allowed - | From_remaining_path _ -> check_more_path_parsing_allowed ~allowed - | With_prefix { t; prefix } -> - if not (List.is_empty prefix) then check_more_path_parsing_allowed ~allowed; - check_that_with_remaining_path_has_no_path_parsers_after_it ~allowed t - | With_remaining_path { t; needed_path } -> - if not (List.is_empty needed_path) then check_more_path_parsing_allowed ~allowed; - check_that_with_remaining_path_has_no_path_parsers_after_it ~allowed:false t - | Record { record_module; _ } -> - let module M = - (val record_module : Record.Cached_s with type Typed_field.derived_on = a) - in - List.iter M.Typed_field.Packed.all ~f:(fun { f = T f } -> + let ( || ) a b = + (* NOTE: This is silly, but avoids (||)'s short-circuiting. *) + a || b + in + fun ~has_seen_end_of_path t -> + match t with + | Unit -> has_seen_end_of_path + | Project { input; _ } -> check_that_with_remaining_path_has_no_path_parsers_after_it - ~allowed - (M.parser_for_field f)) - | Variant { variant_module; _ } -> - let module M = - (val variant_module : Variant.Cached_s with type Typed_variant.derived_on = a) - in - List.iter M.Typed_variant.Packed.all ~f:(fun { f = T v } -> + ~has_seen_end_of_path + input + | Optional_query_fields { t; _ } -> check_that_with_remaining_path_has_no_path_parsers_after_it - ~allowed - (M.parser_for_variant v)) - | Query_based_variant { variant_module; _ } -> - let module M = - (val variant_module - : Query_based_variant.S with type Typed_variant.derived_on = a) - in - List.iter M.Typed_variant.Packed.all ~f:(fun { f = T v } -> + ~has_seen_end_of_path + t + | From_query_required _ -> has_seen_end_of_path + | From_query_optional _ -> has_seen_end_of_path + | From_query_optional_with_default _ -> has_seen_end_of_path + | From_query_many _ -> has_seen_end_of_path + | From_path _ -> + check_more_path_parsing_allowed ~has_seen_end_of_path; + has_seen_end_of_path + | From_remaining_path _ -> + check_more_path_parsing_allowed ~has_seen_end_of_path; + true + | With_prefix { t; prefix } -> + if not (List.is_empty prefix) + then check_more_path_parsing_allowed ~has_seen_end_of_path; check_that_with_remaining_path_has_no_path_parsers_after_it - ~allowed - (M.parser_for_variant v)) + ~has_seen_end_of_path + t + | With_remaining_path { t; needed_path } -> + if not (List.is_empty needed_path) + then check_more_path_parsing_allowed ~has_seen_end_of_path; + check_that_with_remaining_path_has_no_path_parsers_after_it + ~has_seen_end_of_path:true + t + | Record { record_module; _ } -> + let module M = + (val record_module : Record.Cached_s with type Typed_field.derived_on = a) + in + List.fold + ~init:has_seen_end_of_path + M.path_order + (* NOTE: This check assumes that the [check_path_orders_are_sane] check has run + beforehand. It is safe to not traverse parsers that do not have any paths + beforehand. *) + ~f:(fun has_seen_end_of_path { f = T f } -> + has_seen_end_of_path + || check_that_with_remaining_path_has_no_path_parsers_after_it + ~has_seen_end_of_path + (M.parser_for_field f)) + | Variant { variant_module; _ } -> + let module M = + (val variant_module : Variant.Cached_s with type Typed_variant.derived_on = a) + in + List.fold + ~init:has_seen_end_of_path + M.Typed_variant.Packed.all + ~f:(fun out { f = T v } -> + (* NOTE: the difference between variant's and records is subtle but important. + records will have ALL of their parser's run, while variant's will only have + one of their parsers run. + *) + out + || check_that_with_remaining_path_has_no_path_parsers_after_it + ~has_seen_end_of_path + (M.parser_for_variant v)) + | Query_based_variant { variant_module; _ } -> + let module M = + (val variant_module + : Query_based_variant.S with type Typed_variant.derived_on = a) + in + List.fold + ~init:has_seen_end_of_path + M.Typed_variant.Packed.all + ~f:(fun out { f = T v } -> + out + || check_that_with_remaining_path_has_no_path_parsers_after_it + ~has_seen_end_of_path + (M.parser_for_variant v)) ;; let rec check_that_there_are_no_ambiguous_parsers_at_any_level : type a. a T.t -> unit = @@ -2189,7 +2226,10 @@ module Parser = struct ; run_check ~name:"Sane path orders check" ~f:(fun () -> check_that_path_orders_are_ok t) ; run_check ~name:"Remaining path does not block other parsers check" ~f:(fun () -> - check_that_with_remaining_path_has_no_path_parsers_after_it t) + (ignore : bool -> unit) + (check_that_with_remaining_path_has_no_path_parsers_after_it + ~has_seen_end_of_path:false + t)) ; run_check ~name:"Ambiguous choices for picking variant constructor check" ~f:(fun () -> check_that_there_are_no_ambiguous_parsers_at_any_level t) diff --git a/uri_parsing/test/test_parser_after_from_remaining_path.ml b/uri_parsing/test/test_parser_after_from_remaining_path.ml new file mode 100644 index 00000000..5c47677c --- /dev/null +++ b/uri_parsing/test/test_parser_after_from_remaining_path.ml @@ -0,0 +1,184 @@ +open! Core +open Uri_parsing + +let%test_module "path parser after remaining_path" = + (module struct + module T = struct + type t = + { foo : string list + ; bar : string + } + [@@deriving sexp_of, typed_fields] + + let parser_for_field : type a. a Typed_field.t -> a Parser.t = function + | Foo -> Parser.from_remaining_path Value_parser.string + | Bar -> Parser.from_path Value_parser.string + ;; + + module Path_order = Parser.Record.Path_order (Typed_field) + + let path_order = Path_order.T [ Foo; Bar ] + end + + let unparseable_parser = + let parser = Parser.Record.make (module T) in + Versioned_parser.first_parser parser + ;; + + let%expect_test "path parser after from remaining path parser passes the check." = + Versioned_parser.check_ok_and_print_urls_or_errors unparseable_parser; + [%expect + {| + Error with parser. + ┌───────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────┐ + │ Check name │ Error message │ + ├───────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┤ + │ Remaining path does not block other parsers check │ "Error! There cannot be path parsers inside of a [with_remaining_path] combinator! The r │ + │ │ eason for this is that there won't be any values after that parser!" │ + └───────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────┘ + |}] + ;; + + let%expect_test "path parser after from remaining path parser passes the check." = + let projection = + Versioned_parser.eval_for_uri ~encoding_behavior:Correct unparseable_parser + in + let original = Uri.make ~path:"a/b/c/d/e" () in + print_endline (Uri.to_string original); + [%expect {| a/b/c/d/e |}]; + Expect_test_helpers_core.require_does_raise [%here] (fun () -> + projection.parse_exn original); + [%expect + {| + ("Error while parsing record field:" + (error_message "Expected a value in path, but nothing was present") + (field_name bar) + (unparseable_components ( + (path ()) + (query ())))) + |}] + ;; + end) +;; + +let%test_module "path parser after end_of_path" = + (module struct + module T = struct + type t = + { foo : unit + ; bar : string + } + [@@deriving sexp_of, typed_fields] + + let parser_for_field : type a. a Typed_field.t -> a Parser.t = function + | Foo -> Parser.end_of_path Parser.unit + | Bar -> Parser.from_path Value_parser.string + ;; + + module Path_order = Parser.Record.Path_order (Typed_field) + + let path_order = Path_order.T [ Foo; Bar ] + end + + let unparseable_parser = + let parser = Parser.Record.make (module T) in + Versioned_parser.first_parser parser + ;; + + let%expect_test "path parser after from remaining path parser passes the check." = + Versioned_parser.check_ok_and_print_urls_or_errors unparseable_parser; + [%expect + {| + Error with parser. + ┌───────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────┐ + │ Check name │ Error message │ + ├───────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┤ + │ Remaining path does not block other parsers check │ "Error! There cannot be path parsers inside of a [with_remaining_path] combinator! The r │ + │ │ eason for this is that there won't be any values after that parser!" │ + └───────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────┘ + |}] + ;; + + let%expect_test "path parser after from remaining path parser passes the check." = + let projection = + Versioned_parser.eval_for_uri ~encoding_behavior:Correct unparseable_parser + in + let original = Uri.make ~path:"e" () in + print_endline (Uri.to_string original); + [%expect {| e |}]; + Expect_test_helpers_core.require_does_raise [%here] (fun () -> + projection.parse_exn original); + [%expect + {| + ("Error while parsing record field:" + (error_message ( + "Did not recognize url during parsing! Expected path to match an expected [with_remaining_path] needed path! but\n the needed path was not recognized!" + (needed_path ()) + (components ((path (e)) (query ()))))) + (field_name foo) + (unparseable_components ((path (e)) (query ())))) + |}] + ;; + end) +;; + +let%test_module "path parser after with_remaining_path" = + (module struct + module T = struct + type t = + { foo : unit + ; bar : string + } + [@@deriving sexp_of, typed_fields] + + let parser_for_field : type a. a Typed_field.t -> a Parser.t = function + | Foo -> Parser.with_remaining_path [ "capybara" ] Parser.unit + | Bar -> Parser.from_path Value_parser.string + ;; + + module Path_order = Parser.Record.Path_order (Typed_field) + + let path_order = Path_order.T [ Foo; Bar ] + end + + let unparseable_parser = + let parser = Parser.Record.make (module T) in + Versioned_parser.first_parser parser + ;; + + let%expect_test "path parser after from remaining path parser passes the check." = + Versioned_parser.check_ok_and_print_urls_or_errors unparseable_parser; + [%expect + {| + Error with parser. + ┌───────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────┐ + │ Check name │ Error message │ + ├───────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┤ + │ Remaining path does not block other parsers check │ "Error! There cannot be path parsers inside of a [with_remaining_path] combinator! The r │ + │ │ eason for this is that there won't be any values after that parser!" │ + └───────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────┘ + |}] + ;; + + let%expect_test "path parser after from remaining path parser passes the check." = + let projection = + Versioned_parser.eval_for_uri ~encoding_behavior:Correct unparseable_parser + in + let original = Uri.make ~path:"capybara/foo" () in + print_endline (Uri.to_string original); + [%expect {| capybara/foo |}]; + Expect_test_helpers_core.require_does_raise [%here] (fun () -> + projection.parse_exn original); + [%expect + {| + ("Error while parsing record field:" + (error_message ( + "Did not recognize url during parsing! Expected path to match an expected [with_remaining_path] needed path! but\n the needed path was not recognized!" + (needed_path (capybara)) + (components ((path (capybara foo)) (query ()))))) + (field_name foo) + (unparseable_components ((path (capybara foo)) (query ())))) + |}] + ;; + end) +;; diff --git a/uri_parsing/test/test_parser_after_from_remaining_path.mli b/uri_parsing/test/test_parser_after_from_remaining_path.mli new file mode 100644 index 00000000..53e67be6 --- /dev/null +++ b/uri_parsing/test/test_parser_after_from_remaining_path.mli @@ -0,0 +1 @@ +(*_ Intentionally left empty. *) diff --git a/web/start.ml b/web/start.ml index eb072597..4f31daef 100644 --- a/web/start.ml +++ b/web/start.ml @@ -172,8 +172,15 @@ module Arrow_deprecated = struct ~bind_to_element_with_id ~(computation_for_instrumentation : result Bonsai.Private.Computation.t) ~fresh - ({ model; action; apply_action; input = _; run = _; reset = _; can_contain_path = _ } - as info : + ({ model + ; action + ; apply_action + ; input = _ + ; run = _ + ; reset = _ + ; may_contain_path = _ + ; may_contain_lifecycle = _ + } as info : (model, action, action_input, result, unit) Bonsai.Private.Computation.info) : (input, extra, incoming, outgoing) Handle.t = @@ -347,6 +354,7 @@ module Arrow_deprecated = struct ~bind_to_element_with_id ~component = + Util.For_bonsai_internal.set_stack_overflow_exception_check (); let fresh = Type_equal.Id.create ~name:"" sexp_of_opaque in let var = Bonsai.Private.Value.named App_input fresh |> Bonsai.Private.conceal_value diff --git a/web/to_incr_dom.ml b/web/to_incr_dom.ml index 56d5d647..71a82206 100644 --- a/web/to_incr_dom.ml +++ b/web/to_incr_dom.ml @@ -96,7 +96,16 @@ let convert_with_extra ?(optimize = false) component = let fresh = Type_equal.Id.create ~name:"" sexp_of_opaque in let var = Bonsai.Private.(Value.named App_input fresh |> conceal_value) in let maybe_optimize = if optimize then Bonsai.Private.pre_process else Fn.id in - let (T { model; input = _; action; apply_action; run; reset = _; can_contain_path = _ }) + let (T + { model + ; input = _ + ; action + ; apply_action + ; run + ; reset = _ + ; may_contain_path = _ + ; may_contain_lifecycle = _ + }) = component var |> Bonsai.Private.top_level_handle diff --git a/web/util.ml b/web/util.ml index 4d0ee933..445e29cf 100644 --- a/web/util.ml +++ b/web/util.ml @@ -35,3 +35,36 @@ let am_within_disabled_fieldset (event : #Dom_html.event Js.t) = | Some (tag_name, disabled) -> String.equal (Js.to_string tag_name) "FIELDSET" && Js.to_bool disabled) ;; + +module For_bonsai_internal = struct + let set_stack_overflow_exception_check () = + let get_test_truncated_trace = + match am_running_how with + | `Browser | `Node -> Fn.id + | `Node_test | `Node_benchmark | `Browser_benchmark -> + fun stack_trace -> + let first_line = + Core.String.split_lines stack_trace |> List.hd |> Option.value ~default:"" + in + sprintf + {|%s +|} + first_line + in + Bonsai.Private.set_perform_on_exception (fun exn -> + match exn with + | Stack_overflow -> + let stack_trace = + Js_of_ocaml.Js.Js_error.of_exn exn + |> Option.bind ~f:Js_of_ocaml.Js.Js_error.stack + |> Option.value_map + ~default:"" + ~f:get_test_truncated_trace + in + eprintf + {|Stack overflow inside of a bonsai computation is not supported! In a future release your app might crash. +%s|} + stack_trace + | _ -> ()) + ;; +end diff --git a/web/util.mli b/web/util.mli index 5dda4faf..ffe1ede8 100644 --- a/web/util.mli +++ b/web/util.mli @@ -26,3 +26,7 @@ val am_running_how [true], even if the component which performs this check is not. *) val am_within_disabled_fieldset : #Dom_html.event Js.t -> bool + +module For_bonsai_internal : sig + val set_stack_overflow_exception_check : unit -> unit +end diff --git a/web_test/bonsai_web_test.ml b/web_test/bonsai_web_test.ml index 1e8dba68..595b9cc8 100644 --- a/web_test/bonsai_web_test.ml +++ b/web_test/bonsai_web_test.ml @@ -1,7 +1,3 @@ -module Arrow = struct - module Driver = Bonsai_test.Arrow.Driver - module Helpers = Helpers - module Helpers_intf = Helpers_intf -end - include Proc + +let () = Bonsai_web.For_bonsai_internal.set_stack_overflow_exception_check () diff --git a/web_test/helpers.ml b/web_test/helpers.ml deleted file mode 100644 index 6e7ac564..00000000 --- a/web_test/helpers.ml +++ /dev/null @@ -1,176 +0,0 @@ -open! Core -open! Import -include Helpers_intf - -let sexp_to_string = Expect_test_helpers_core.sexp_to_string - -let make_generic - (type input extra action result s) - ~(driver : (input, s) Driver.t) - ~(string_of_result : result -> string) - ~(get_result : s -> result) - ~(get_extra : s -> extra) - ~(schedule_action : s -> action -> unit) - : (module S with type input = input and type action = action and type extra = extra) - = - (module struct - type nonrec input = input - type nonrec action = action - type nonrec extra = extra - - let show () = - driver |> Driver.result |> get_result |> string_of_result |> print_endline - ;; - - let show_model () = - (* Cleans up a sexp by - - removing empty lists (unit models are common in bonsai) - - flattening lists that contain a single element *) - let rec clean_up_model_sexp a = - match a with - | Sexp.List l -> - (match List.filter_map l ~f:clean_up_model_sexp with - | [] -> None - | [ l ] -> Some l - | l -> Some (Sexp.List l)) - | Sexp.Atom _ as a -> Some a - in - driver - |> Driver.sexp_of_model - |> clean_up_model_sexp - |> Option.value ~default:(Sexp.List []) - |> sexp_to_string - |> print_endline - ;; - - let get_extra () = driver |> Driver.result |> get_extra - - let set_input input = - Driver.set_input driver input; - Driver.flush driver; - show () - ;; - - let do_actions actions = - List.iter actions ~f:(schedule_action (Driver.result driver)); - Driver.flush driver; - show () - ;; - end) -;; - -let make_vdom_generic - (type input action extra s) - ~(driver : (input, s) Driver.t) - ~(vdom_of_result : s -> Vdom.Node.t) - ~(get_extra : s -> extra) - ~(inject_of_result : s -> action -> unit Vdom.Effect.t) - ?(vdom_to_string = - fun node -> - node - |> Virtual_dom_test_helpers.Node_helpers.unsafe_convert_exn - |> Virtual_dom_test_helpers.Node_helpers.to_string_html) - () - : (module S_vdom with type input = input and type action = action and type extra = extra) - = - let open Virtual_dom_test_helpers in - let (module H) = - make_generic - ~driver - ~string_of_result:vdom_to_string - ~get_result:vdom_of_result - ~get_extra - ~schedule_action:(fun s action -> - Driver.schedule_event driver ((inject_of_result s) action)) - in - (module struct - include H - - let get_element ~selector = - let node = - driver |> Driver.result |> vdom_of_result |> Node_helpers.unsafe_convert_exn - in - Node_helpers.select_first_exn node ~selector - ;; - - let click_on ~selector = - let element = get_element ~selector in - Node_helpers.User_actions.click_on element; - Driver.flush driver - ;; - - let input_text ~selector ~text = - let element = get_element ~selector in - Node_helpers.User_actions.input_text element ~text; - Driver.flush driver - ;; - end) -;; - -let[@warning "-16"] make_vdom_with_inject ?vdom_to_string ~driver = - make_vdom_generic - ?vdom_to_string - ~driver - ~get_extra:(Fn.const ()) - ~vdom_of_result:Tuple2.get1 - ~inject_of_result:Tuple2.get2 - () -;; - -let[@warning "-16"] make_vdom_with_extra ?vdom_to_string ~driver = - make_vdom_generic - ?vdom_to_string - ~driver - ~vdom_of_result:Tuple2.get1 - ~get_extra:Tuple2.get2 - ~inject_of_result:(Fn.const Nothing.unreachable_code) - () -;; - -let[@warning "-16"] make_vdom ?vdom_to_string ~driver = - make_vdom_generic - ?vdom_to_string - ~driver - ~get_extra:(Fn.const ()) - ~vdom_of_result:Fn.id - ~inject_of_result:(Fn.const Nothing.unreachable_code) - () -;; - -let make_string ~driver = - make_generic - ~driver - ~string_of_result:Fn.id - ~get_result:Fn.id - ~get_extra:(Fn.const ()) - ~schedule_action:(Fn.const Nothing.unreachable_code) -;; - -let make ~driver ~sexp_of_result = - make_generic - ~driver - ~string_of_result:(fun r -> r |> sexp_of_result |> sexp_to_string) - ~get_result:Fn.id - ~get_extra:(Fn.const ()) - ~schedule_action:(Fn.const Nothing.unreachable_code) -;; - -let make_string_with_inject ~driver = - make_generic - ~driver - ~string_of_result:Fn.id - ~get_result:fst - ~get_extra:(Fn.const ()) - ~schedule_action:(fun (_, inject) action -> - Driver.schedule_event driver (inject action)) -;; - -let make_with_inject ~driver ~sexp_of_result = - make_generic - ~driver - ~string_of_result:(fun r -> r |> sexp_of_result |> sexp_to_string) - ~get_result:fst - ~get_extra:(Fn.const ()) - ~schedule_action:(fun (_, inject) action -> - Driver.schedule_event driver (inject action)) -;; diff --git a/web_test/helpers.mli b/web_test/helpers.mli deleted file mode 100644 index 87d2c718..00000000 --- a/web_test/helpers.mli +++ /dev/null @@ -1 +0,0 @@ -include Helpers_intf.Helpers (** @inline *) diff --git a/web_test/helpers_intf.ml b/web_test/helpers_intf.ml deleted file mode 100644 index 76e9b393..00000000 --- a/web_test/helpers_intf.ml +++ /dev/null @@ -1,84 +0,0 @@ -open! Core -open! Import - -module type S = sig - type input - type action - type extra - - val show : unit -> unit - val show_model : unit -> unit - val set_input : input -> unit - val do_actions : action list -> unit - val get_extra : unit -> extra -end - -module type S_vdom = sig - include S - - val click_on : selector:string -> unit - val input_text : selector:string -> text:string -> unit -end - -module type Helpers = sig - module type S = S - - val make_generic - : driver:('input, 's) Driver.t - -> string_of_result:('result -> string) - -> get_result:('s -> 'result) - -> get_extra:('s -> 'extra) - -> schedule_action:('s -> 'action -> unit) - -> (module S - with type action = 'action - and type input = 'input - and type extra = 'extra) - - val make - : driver:('input, 'result) Driver.t - -> sexp_of_result:('result -> Sexp.t) - -> (module S - with type input = 'input - and type action = Nothing.t - and type extra = unit) - - val make_with_inject - : driver:('input, 'result * ('action -> unit Vdom.Effect.t)) Driver.t - -> sexp_of_result:('result -> Sexp.t) - -> (module S with type input = 'input and type action = 'action and type extra = unit) - - val make_string - : driver:('input, string) Driver.t - -> (module S - with type input = 'input - and type action = Nothing.t - and type extra = unit) - - val make_string_with_inject - : driver:('input, string * ('action -> unit Vdom.Effect.t)) Driver.t - -> (module S with type input = 'input and type action = 'action and type extra = unit) - - val make_vdom - : ?vdom_to_string:(Vdom.Node.t -> string) - -> driver:('input, Vdom.Node.t) Driver.t - -> (module S_vdom - with type input = 'input - and type action = Nothing.t - and type extra = unit) - - val make_vdom_with_extra - : ?vdom_to_string:(Vdom.Node.t -> string) - -> driver:('input, Vdom.Node.t * 'extra) Driver.t - -> (module S_vdom - with type input = 'input - and type action = Nothing.t - and type extra = 'extra) - - val make_vdom_with_inject - : ?vdom_to_string:(Vdom.Node.t -> string) - -> driver:('input, Vdom.Node.t * ('action -> unit Vdom.Effect.t)) Driver.t - -> (module S_vdom - with type input = 'input - and type action = 'action - and type extra = unit) -end diff --git a/web_test/of_bonsai_itself/overflow_tests.ml b/web_test/of_bonsai_itself/overflow_tests.ml new file mode 100644 index 00000000..b1f608bd --- /dev/null +++ b/web_test/of_bonsai_itself/overflow_tests.ml @@ -0,0 +1,44 @@ +open! Core +open Bonsai_web.Cont +open Bonsai.Let_syntax +open Bonsai_web_test +open Bonsai_test_of_bonsai_itself.Big_computation_regression_util + +let overflow_height = 1_000 + +let%expect_test "Bonsai prints an error message if there is a stack overflow in a \ + computation" + = + let app always_safe graph = + match%sub always_safe with + | `Safe -> Bonsai.return String.Set.empty + | `Overflow -> For_cont.basic ~height:overflow_height ~width:1 graph + in + let _handle = Handle.create lengths_result_spec (app (Bonsai.return `Safe)) in + (* Explicitly not calling Handle.show here *) + [%expect + {| + Stack overflow inside of a bonsai computation is not supported! In a future release your app might crash. + RangeError: Maximum call stack size exceeded + + |}] +;; + +let%expect_test "Bonsai actually stack overflows when an overflowed computation is active" + = + Expect_test_helpers_core.require_does_raise [%here] (fun () -> + let app always_overflow graph = + match%sub always_overflow with + | `Safe -> Bonsai.return String.Set.empty + | `Overflow -> For_cont.basic ~height:overflow_height ~width:1 graph + in + let _handle = Handle.create lengths_result_spec (app (Bonsai.return `Overflow)) in + (* Explicitly not calling Handle.show here *) + ()); + [%expect + {| + Stack overflow inside of a bonsai computation is not supported! In a future release your app might crash. + RangeError: Maximum call stack size exceeded + ("Stack overflow") + |}] +;; diff --git a/web_test/of_bonsai_itself/overflow_tests.mli b/web_test/of_bonsai_itself/overflow_tests.mli new file mode 100644 index 00000000..e082e6ee --- /dev/null +++ b/web_test/of_bonsai_itself/overflow_tests.mli @@ -0,0 +1 @@ +(*_ intentionally left blank. *) diff --git a/web_ui/README.md b/web_ui/README.md new file mode 100644 index 00000000..10d8549f --- /dev/null +++ b/web_ui/README.md @@ -0,0 +1,8 @@ +# Libraries for Bonsai Web UIs + +
+# Libraries for Bonsai Web UIs +
+ +A collection of libraries with reusable components, vdom helper functions, and other +useful tools for building web UIs with Bonsai. diff --git a/web_ui/auto_generated/README.md b/web_ui/auto_generated/README.md index 8347241c..2cec6d5e 100644 --- a/web_ui/auto_generated/README.md +++ b/web_ui/auto_generated/README.md @@ -1,4 +1,4 @@ -# Auto-generating Bonsai Web Components +# Views and Forms for Sexpable Types The `Bonsai_web_ui_auto_generated` library provides functions for creating web UIs and forms for types that derive `Sexp_grammar`. diff --git a/web_ui/dune b/web_ui/dune new file mode 100644 index 00000000..e69de29b diff --git a/web_ui/form/README.md b/web_ui/form/README.md index d12e9c90..8ca2bd5a 100644 --- a/web_ui/form/README.md +++ b/web_ui/form/README.md @@ -1,3 +1,3 @@ # Bonsai Form - See the Bonsai Guide [chapter on forms](../../docs/guide/04-forms.mdx). + See the Bonsai Guide [chapter on forms](../../docs/how_to/forms.mdx). diff --git a/web_ui/partial_render_table/bench/src/bonsai_web_ui_partial_render_table_bench.ml b/web_ui/partial_render_table/bench/src/bonsai_web_ui_partial_render_table_bench.ml index f4b36811..5c1537fd 100644 --- a/web_ui/partial_render_table/bench/src/bonsai_web_ui_partial_render_table_bench.ml +++ b/web_ui/partial_render_table/bench/src/bonsai_web_ui_partial_render_table_bench.ml @@ -76,7 +76,7 @@ let component_for_bench let key_range = Bonsai.Var.value key_range in let map = Bonsai.Var.value map in let on_change = Bonsai.Var.value on_change in - let%sub collate = + let%sub collate, key_rank = let collate = let%map filter = filter and order = order @@ -95,7 +95,7 @@ let component_for_bench Table.component ?preload_rows comparator - ~focus:(Table.Focus.By_cell { on_change; compute_presence = return }) + ~focus:(Table.Focus.By_cell { on_change; compute_presence = return; key_rank }) ~row_height:(Value.return (`Px 1)) ~columns collate diff --git a/web_ui/partial_render_table/src/bonsai_web_ui_partial_render_table.ml b/web_ui/partial_render_table/src/bonsai_web_ui_partial_render_table.ml index 851889d9..48474967 100644 --- a/web_ui/partial_render_table/src/bonsai_web_ui_partial_render_table.ml +++ b/web_ui/partial_render_table/src/bonsai_web_ui_partial_render_table.ml @@ -529,7 +529,12 @@ module Expert = struct ~order_to_compare data collate - |> Incr_map_collate.collated) + |> fun x -> + let key_rank = + let%map.Incremental key_rank = Incr_map_collate.key_rank x in + Effect.of_sync_fun key_rank + in + Incremental.both (Incr_map_collate.collated x) key_rank) ;; end @@ -606,29 +611,6 @@ module Basic = struct ~columns map -> let module Key_cmp = (val key_comparator) in - let focus : (focus, presence, key, column_id) Expert.Focus.Kind.t = - match focus with - | None -> None - | By_row { on_change } -> - let compute_presence focus = - let%arr focus = focus - and map = map in - match focus with - | None -> None - | Some focus -> if Map.mem map focus then Some focus else None - in - By_row { on_change; compute_presence } - | By_cell { on_change } -> - let compute_presence focus = - let%arr focus = focus - and map = map in - match focus with - | None -> None - | Some ((focused_key, _) as focus) -> - if Map.mem map focused_key then Some focus else None - in - By_cell { on_change; compute_presence } - in let filter = Value.transpose_opt filter in let%sub rank_range, set_rank_range = Bonsai.state @@ -681,7 +663,7 @@ module Basic = struct let key_range = Collate.Which_range.All_rows in { Collate.filter; order; key_range; rank_range } in - let%sub collated = + let%sub collated, key_rank = Expert.collate ~filter_equal:phys_equal ~filter_to_predicate:Fn.id @@ -690,6 +672,29 @@ module Basic = struct map collate in + let focus : (focus, presence, key, column_id) Expert.Focus.Kind.t = + match focus with + | None -> None + | By_row { on_change } -> + let compute_presence focus = + let%arr focus = focus + and map = map in + match focus with + | None -> None + | Some focus -> if Map.mem map focus then Some focus else None + in + By_row { on_change; compute_presence; key_rank } + | By_cell { on_change } -> + let compute_presence focus = + let%arr focus = focus + and map = map in + match focus with + | None -> None + | Some ((focused_key, _) as focus) -> + if Map.mem map focused_key then Some focus else None + in + By_cell { on_change; compute_presence; key_rank } + in let%sub num_filtered_rows = let%arr collated = collated in Collated.num_filtered_rows collated diff --git a/web_ui/partial_render_table/src/bonsai_web_ui_partial_render_table.mli b/web_ui/partial_render_table/src/bonsai_web_ui_partial_render_table.mli index 7fffcfc3..e95a86b0 100644 --- a/web_ui/partial_render_table/src/bonsai_web_ui_partial_render_table.mli +++ b/web_ui/partial_render_table/src/bonsai_web_ui_partial_render_table.mli @@ -218,11 +218,18 @@ module Expert : sig can be offscreen such that it isn't given to the table component. [compute_presence] forces the user to consider if a row is considered 'focused' or not. *) ; compute_presence : 'k option Value.t -> 'p Computation.t + (** A user might try to focus-by-key a row that has not been filtered out, + but is not inside the viewport. In that case, [key_rank] will be used as + a fallback to compute the desired index. + If the effect returns `None`, the key does not correspond to a row under the + current filter conditions, and the focus will be a no-op. *) + ; key_rank : ('k -> int option Effect.t) Value.t } -> (('k, 'p) Focus_by_row.t, 'p, 'k, 'c) t | By_cell : { on_change : (('k * 'c) option -> unit Effect.t) Value.t ; compute_presence : ('k * 'c) option Value.t -> 'presence Computation.t + ; key_rank : ('k -> int option Effect.t) Value.t } -> (('k, 'c, 'presence) By_cell.t, 'presence, 'k, 'c) t end @@ -326,7 +333,7 @@ module Expert : sig -> ('k, 'filter, 'order) Collate.t Value.t (** A [Collate.t] is a specification for how to perform collation: it's where the ['filter], ['order], and rank range are defined. *) - -> ('k, 'v) Collated.t Computation.t + -> (('k, 'v) Collated.t * ('k -> int option Effect.t)) Computation.t val component : ?theming:Table_view.Theming.t diff --git a/web_ui/partial_render_table/src/focus.ml b/web_ui/partial_render_table/src/focus.ml index 926d0a97..f81b2e47 100644 --- a/web_ui/partial_render_table/src/focus.ml +++ b/web_ui/partial_render_table/src/focus.ml @@ -3,6 +3,32 @@ open! Bonsai_web open! Bonsai.Let_syntax module Collated = Incr_map_collate.Collated +(* This global counter is shared across all PRTs, but this is fine because we only use it + for equality checking, so the bad case would be if: + + - A pending select is scheduled + - Exactly 2^63 focus calls to rows not in the current collated are sent out + - The counter overflows, and the original select has the same id as a new select + + This seems unlikely. + + That being said, [compare] should not be derived on this type, because the counter + _could_ overflow, although this is also very unlikely. *) +module Pending_select_id : sig + type t [@@deriving equal, sexp_of] + + val create : unit -> t +end = struct + include Int63 + + let global = ref Int63.zero + + let create () = + global := Int63.succ !global; + !global + ;; +end + module By_cell = struct module Action = struct type ('key, 'column_id) t = @@ -17,6 +43,7 @@ module By_cell = struct | Page_down | Select of ('key * 'column_id) | Select_index of (int * 'column_id) + | Select_index_from_pending of (int * 'column_id * Pending_select_id.t) | Switch_from_index_to_key of { key : 'key ; index : int @@ -115,11 +142,13 @@ module Kind = struct | By_row : { on_change : ('k option -> unit Effect.t) Value.t ; compute_presence : 'k option Value.t -> 'presence Computation.t + ; key_rank : ('k -> int option Effect.t) Value.t } -> (('k, 'presence) By_row.t, 'presence, 'k, 'column_id) t | By_cell : { on_change : (('k * 'column_id) option -> unit Effect.t) Value.t ; compute_presence : ('k * 'column_id) option Value.t -> 'presence Computation.t + ; key_rank : ('k -> int option Effect.t) Value.t } -> (('k, 'column_id, 'presence) By_cell.t, 'presence, 'k, 'column_id) t end @@ -189,6 +218,7 @@ module Cell_machine = struct (column_id : (column_id, column_id_cmp) Bonsai.comparator) ~(compute_presence : (key * column_id) option Value.t -> presence Computation.t) ~(on_change : ((key * column_id) option -> unit Effect.t) Value.t) + ~(key_rank : (key -> int option Effect.t) Value.t) ~(collated : (key, data) Incr_map_collate.Collated.t Value.t) ~(columns : column_id list Value.t) ~(range : (int * int) Value.t) @@ -230,11 +260,14 @@ module Cell_machine = struct type t = { locked : bool + ; pending_select_id : Pending_select_id.t option ; current_focus : current_focus } [@@deriving sexp_of, equal] - let empty = { locked = false; current_focus = No_focused_cell } + let empty = + { locked = false; current_focus = No_focused_cell; pending_select_id = None } + ;; end in let module Input = struct @@ -243,6 +276,7 @@ module Cell_machine = struct ; columns : column_id list ; range : int * int ; on_change : (key * column_id) option -> unit Ui_effect.t + ; key_rank : key -> int option Effect.t ; scroll_to_index : int -> unit Effect.t ; scroll_to_column : column_id -> unit Effect.t } @@ -253,9 +287,17 @@ module Cell_machine = struct and columns = columns and range = range and on_change = on_change + and key_rank = key_rank and scroll_to_index = scroll_to_index and scroll_to_column = scroll_to_column in - { Input.collated; columns; range; on_change; scroll_to_index; scroll_to_column } + { Input.collated + ; columns + ; range + ; on_change + ; key_rank + ; scroll_to_index + ; scroll_to_column + } in let apply_action context input (model : Model.t) action = match input with @@ -264,6 +306,7 @@ module Cell_machine = struct ; collated = _ ; range = _ ; on_change = _ + ; key_rank = _ ; scroll_to_index = _ ; scroll_to_column = _ } -> @@ -277,13 +320,15 @@ module Cell_machine = struct ; columns = first_column :: _ as columns ; range = range_start, range_end ; on_change + ; key_rank ; scroll_to_index ; scroll_to_column } -> (match model, (action : Action.t) with - | { locked = true; current_focus = _ }, Unlock -> { model with locked = false } - | { locked = true; current_focus = _ }, _ -> model - | { locked = false; current_focus }, _ -> + | { locked = true; current_focus = _; pending_select_id = _ }, Unlock -> + { model with locked = false } + | { locked = true; current_focus = _; pending_select_id = _ }, _ -> model + | { locked = false; current_focus; pending_select_id }, _ -> let scroll_to_index index = Bonsai.Apply_action_context.schedule_event context (scroll_to_index index) in @@ -330,6 +375,7 @@ module Cell_machine = struct | Select _ | Unfocus | Select_index _ + | Select_index_from_pending _ | Down | Up | Left @@ -339,101 +385,130 @@ module Cell_machine = struct (* Technically we already know [model.locked] is false in this branch, but this code feels closer to the semantics we're going for *) in - let new_focus = + let new_focus, pending_select = match (action : Action.t) with - | Lock | Unlock -> current_focus + | Lock | Unlock -> current_focus, `No_change | Switch_from_index_to_key { key; index } -> (* Before switching from index to key, we need to make sure that the focus is still at the that index. If it isn't, then we ignore the request to switch from index to key, since it is out of date. *) (match current_focus with - | No_focused_cell -> No_focused_cell + | No_focused_cell -> No_focused_cell, `No_change | Visible { row = At_index model_index; column } | Shadow { row = At_index model_index; column } -> if model_index = index then - Visible - { Currently_selected_cell.row = At_key { key; index }; column } - else Visible { row = At_index model_index; column } + ( Visible + { Currently_selected_cell.row = At_key { key; index }; column } + , `No_change ) + else Visible { row = At_index model_index; column }, `No_change | Visible ({ row = At_key _; column = _ } as current) - | Shadow ({ row = At_key _; column = _ } as current) -> Visible current) + | Shadow ({ row = At_key _; column = _ } as current) -> + Visible current, `No_change) | Select (key, column) -> (match find_by_key ~key ~key_equal:Key.equal collated with | Some ({ index; key = _ } as triple) -> scroll_to_index index; - Visible { row = At_key triple; column } - | None -> No_focused_cell) + Visible { row = At_key triple; column }, `Cancel + | None -> + let pending_select_id = Pending_select_id.create () in + Bonsai.Apply_action_context.schedule_event + context + (match%bind.Effect key_rank key with + | None -> Effect.Ignore + | Some index -> + Bonsai.Apply_action_context.inject + context + (By_cell.Action.Select_index_from_pending + (index, column, pending_select_id))); + No_focused_cell, `Schedule pending_select_id) | Unfocus -> (match current_focus with - | No_focused_cell -> No_focused_cell - | Visible triple | Shadow triple -> Shadow triple) + | No_focused_cell -> No_focused_cell, `Cancel + | Visible triple | Shadow triple -> Shadow triple, `Cancel) | Select_index (new_index, new_column) -> - Visible (update_focus ~f:(fun _original_index -> new_index, new_column)) + ( Visible (update_focus ~f:(fun _original_index -> new_index, new_column)) + , `Cancel ) + | Select_index_from_pending (new_index, new_column, result_select_id) -> + (match pending_select_id with + | Some state_pending_id + when Pending_select_id.equal state_pending_id result_select_id -> + ( Visible + (update_focus ~f:(fun _original_index -> new_index, new_column)) + , `Cancel ) + | _ -> current_focus, `No_change) | Down -> - Visible - (update_focus ~f:(function - | Some (original_index, column) -> original_index + 1, column - | None -> range_start, first_column)) + ( Visible + (update_focus ~f:(function + | Some (original_index, column) -> original_index + 1, column + | None -> range_start, first_column)) + , `Cancel ) | Up -> - Visible - (update_focus ~f:(function - | Some (original_index, column) -> original_index - 1, column - | None -> range_end, first_column)) + ( Visible + (update_focus ~f:(function + | Some (original_index, column) -> original_index - 1, column + | None -> range_end, first_column)) + , `Cancel ) | Left -> - Visible - (update_focus ~f:(function - | Some (original_index, original_column) -> - let column_index = - List.findi columns ~f:(fun _ column -> - Column_id.equal column original_column) - in - (match column_index with - | Some (i, _) -> - (* List.nth_exn will throw if and only if the length of the list is 0, - which the pattern match above guards against already.*) - original_index, List.nth_exn columns (Int.max (i - 1) 0) - | None -> original_index, first_column) - | None -> range_start, first_column)) + ( Visible + (update_focus ~f:(function + | Some (original_index, original_column) -> + let column_index = + List.findi columns ~f:(fun _ column -> + Column_id.equal column original_column) + in + (match column_index with + | Some (i, _) -> + (* List.nth_exn will throw if and only if the length of the list is 0, + which the pattern match above guards against already.*) + original_index, List.nth_exn columns (Int.max (i - 1) 0) + | None -> original_index, first_column) + | None -> range_start, first_column)) + , `Cancel ) | Right -> - Visible - (update_focus ~f:(function - | Some (original_index, original_column) -> - let column_index = - List.findi columns ~f:(fun _ column -> - Column_id.equal column original_column) - in - (match column_index with - | Some (i, _) -> - (* List.nth_exn will throw if and only if the length of the list is 0, - which the pattern match above guards against already.*) - ( original_index - , List.nth_exn columns (Int.min (i + 1) (List.length columns - 1)) - ) - | None -> original_index, first_column) - | None -> range_start, first_column)) + ( Visible + (update_focus ~f:(function + | Some (original_index, original_column) -> + let column_index = + List.findi columns ~f:(fun _ column -> + Column_id.equal column original_column) + in + (match column_index with + | Some (i, _) -> + (* List.nth_exn will throw if and only if the length of the list is 0, + which the pattern match above guards against already.*) + ( original_index + , List.nth_exn + columns + (Int.min (i + 1) (List.length columns - 1)) ) + | None -> original_index, first_column) + | None -> range_start, first_column)) + , `Cancel ) | Page_down -> - Visible - (update_focus ~f:(function - | Some (original_index, original_column) -> - let new_index = - if original_index < range_end - then range_end - else original_index + (range_end - range_start) - in - new_index, original_column - | None -> range_end, first_column)) + ( Visible + (update_focus ~f:(function + | Some (original_index, original_column) -> + let new_index = + if original_index < range_end + then range_end + else original_index + (range_end - range_start) + in + new_index, original_column + | None -> range_end, first_column)) + , `Cancel ) | Page_up -> - Visible - (update_focus ~f:(function - | Some (original_index, original_column) -> - let new_index = - if original_index > range_start - then range_start - else original_index - (range_end - range_start) - in - new_index, original_column - | None -> range_start, first_column)) + ( Visible + (update_focus ~f:(function + | Some (original_index, original_column) -> + let new_index = + if original_index > range_start + then range_start + else original_index - (range_end - range_start) + in + new_index, original_column + | None -> range_start, first_column)) + , `Cancel ) in let prev_key = match current_focus with @@ -447,9 +522,18 @@ module Cell_machine = struct None | Visible { row = At_key { key; _ }; column } -> Some (key, column) in + let new_pending_select_id = + match pending_select with + | `No_change -> pending_select_id + | `Cancel -> None + | `Schedule id -> Some id + in if not ([%equal: (Key.t * Column_id.t) option] prev_key next_key) then Bonsai.Apply_action_context.schedule_event context (on_change next_key); - { locked = new_locked; current_focus = new_focus }) + { locked = new_locked + ; current_focus = new_focus + ; pending_select_id = new_pending_select_id + }) in let%sub current, inject = Bonsai.state_machine1 @@ -478,13 +562,19 @@ module Cell_machine = struct let%arr current = current and collated = collated in match current with - | { current_focus = Visible { row = At_key { key; _ }; column }; locked = _ } -> - Some (key, column) - | { current_focus = Visible { row = At_index index; column }; locked = _ } -> + | { current_focus = Visible { row = At_key { key; _ }; column } + ; locked = _ + ; pending_select_id = _ + } -> Some (key, column) + | { current_focus = Visible { row = At_index index; column } + ; locked = _ + ; pending_select_id = _ + } -> (match find_by_index collated ~index with | Some { key; _ } -> Some (key, column) | None -> None) - | { current_focus = No_focused_cell | Shadow _; locked = _ } -> None + | { current_focus = No_focused_cell | Shadow _; locked = _; pending_select_id = _ } + -> None in let%sub () = Bonsai.Edge.on_change @@ -493,7 +583,8 @@ module Cell_machine = struct (Value.both current visually_focused) ~callback: (let%map inject = inject in - fun ({ Model.current_focus; locked = _ }, visually_focused) -> + fun ( { Model.current_focus; locked = _; pending_select_id = _ } + , visually_focused ) -> (* If we ever notice that the state machine is focused at an index for which there is an existing row, we can request that the state machine switch over to being focused on the key at that index. *) @@ -528,6 +619,7 @@ module Row_machine = struct (key : (key, cmp) Bonsai.comparator) ~(compute_presence : key option Value.t -> presence Computation.t) ~(on_change : (key option -> unit Effect.t) Value.t) + ~(key_rank : (key -> int option Effect.t) Value.t) ~(collated : (key, data) Incr_map_collate.Collated.t Value.t) ~(range : (int * int) Value.t) ~(scroll_to_index : (int -> unit Effect.t) Value.t) @@ -553,6 +645,7 @@ module Row_machine = struct (module Unit) ~on_change ~compute_presence + ~key_rank ~collated ~columns:(Value.return [ () ]) ~range @@ -588,16 +681,17 @@ let component | None -> fun _ _ ~collated:_ ~leaves:_ ~range:_ ~scroll_to_index:_ ~scroll_to_column:_ -> Bonsai.const { focus = (); visually_focused = Nothing_focused } - | By_row { on_change; compute_presence } -> + | By_row { on_change; compute_presence; key_rank } -> fun key _ ~collated ~leaves:_ ~range ~scroll_to_index ~scroll_to_column:_ -> Row_machine.component key ~on_change ~compute_presence + ~key_rank ~collated ~range ~scroll_to_index - | By_cell { on_change; compute_presence } -> + | By_cell { on_change; compute_presence; key_rank } -> fun key column ~collated ~leaves ~range ~scroll_to_index ~scroll_to_column -> let%sub columns = let%arr leaves = leaves in @@ -608,6 +702,7 @@ let component column ~on_change ~compute_presence + ~key_rank ~collated ~columns ~range diff --git a/web_ui/partial_render_table/src/focus.mli b/web_ui/partial_render_table/src/focus.mli index 068a8c5e..5404464b 100644 --- a/web_ui/partial_render_table/src/focus.mli +++ b/web_ui/partial_render_table/src/focus.mli @@ -63,11 +63,13 @@ module Kind : sig | By_row : { on_change : ('k option -> unit Effect.t) Value.t ; compute_presence : 'k option Value.t -> 'presence Computation.t + ; key_rank : ('k -> int option Effect.t) Value.t } -> (('k, 'presence) By_row.t, 'presence, 'k, 'col_id) t | By_cell : { on_change : (('k * 'col_id) option -> unit Effect.t) Value.t ; compute_presence : ('k * 'col_id) option Value.t -> 'presence Computation.t + ; key_rank : ('k -> int option Effect.t) Value.t } -> (('k, 'col_id, 'presence) By_cell.t, 'presence, 'k, 'col_id) t end diff --git a/web_ui/partial_render_table/test/ansi_table_tests.ml b/web_ui/partial_render_table/test/ansi_table_tests.ml index 910ce704..d341863a 100644 --- a/web_ui/partial_render_table/test/ansi_table_tests.ml +++ b/web_ui/partial_render_table/test/ansi_table_tests.ml @@ -2593,7 +2593,7 @@ let%expect_test "Pseudo-BUG: setting rank_range does not change the which rows t |> Value.return in let component = - let%sub collate = + let%sub collate, key_rank = let collate = let%map rank_range = Bonsai.Var.value rank_range in { Collate.filter = None @@ -2622,6 +2622,7 @@ let%expect_test "Pseudo-BUG: setting rank_range does not change the which rows t match focus with | None -> None | Some focus -> if Map.mem map focus then Some focus else None) + ; key_rank }) ~row_height:(Value.return (`Px 20)) ~columns: @@ -2765,7 +2766,11 @@ let%expect_test "focus down when presence says that all responses are None" = let test = Test.create ~stats:false - (Test.Component.expert_for_testing_compute_presence ~collate ~presence ()) + (Test.Component.expert_for_testing_compute_presence_and_key_rank + ~collate + ~presence + ~key_rank:(fun ~actual_key_rank -> Bonsai.read actual_key_rank) + ()) in Handle.show test.handle; [%expect @@ -2811,6 +2816,288 @@ let%expect_test "focus down when presence says that all responses are None" = |}] ;; +let%test_module "focus by key `key_rank` fallback" = + (module struct + let test () = + let presence ~focus:_ ~collation:_ = Bonsai.const None in + let collate = + Value.return + { Incr_map_collate.Collate.filter = () + ; order = () + ; key_range = To 4 + ; rank_range = All_rows + } + in + Test.create ~stats:true ~map:big_map ~should_set_bounds:false (fun input filter -> + let key_rank ~actual_key_rank:_ = + let%sub sleep = Bonsai.Clock.sleep in + let%arr sleep = sleep in + fun row_key -> + let%bind.Effect () = sleep (Time_ns.Span.of_ms 10.) in + Effect.return (Some ((row_key mod 2) + 2)) + in + Test.Component.expert_for_testing_compute_presence_and_key_rank + ~collate + ~presence + ~key_rank + () + input + filter) + ;; + + let%expect_test "focus by key should fall back to provided `key_rank` if the key \ + isn't in the current `Collated.t` range" + = + let test = test () in + Handle.recompute_view_until_stable test.handle; + Handle.show test.handle; + [%expect + {| + ((focused ()) (num_filtered_rows ())) + ┌────────────────┬───────┐ + │ metric │ value │ + ├────────────────┼───────┤ + │ rows-before │ 0 │ + │ rows-after │ 95 │ + │ num-filtered │ 99 │ + │ num-unfiltered │ 99 │ + └────────────────┴───────┘ + ┌───┬─────┬─────┐ + │ > │ # │ key │ + ├───┼─────┼─────┤ + │ │ 0 │ 1 │ + │ │ 100 │ 2 │ + │ │ 200 │ 3 │ + │ │ 300 │ 4 │ + └───┴─────┴─────┘ + |}]; + Handle.do_actions test.handle [ Focus_row 150 ]; + (* It doesn't focus immediately; we have to wait for the effect to complete.*) + Handle.show test.handle; + [%expect + {| + ((focused ()) (num_filtered_rows ())) + ┌────────────────┬───────┐ + │ metric │ value │ + ├────────────────┼───────┤ + │ rows-before │ 0 │ + │ rows-after │ 95 │ + │ num-filtered │ 99 │ + │ num-unfiltered │ 99 │ + └────────────────┴───────┘ + ┌───┬─────┬─────┐ + │ > │ # │ key │ + ├───┼─────┼─────┤ + │ │ 0 │ 1 │ + │ │ 100 │ 2 │ + │ │ 200 │ 3 │ + │ │ 300 │ 4 │ + └───┴─────┴─────┘ + |}]; + Handle.advance_clock_by test.handle (Time_ns.Span.of_int_ms 10); + Handle.show test.handle; + [%expect + {| + scrolling to index 2 at 30.0px + ((focused ()) (num_filtered_rows ())) + ┌────────────────┬───────┐ + │ metric │ value │ + ├────────────────┼───────┤ + │ rows-before │ 0 │ + │ rows-after │ 95 │ + │ num-filtered │ 99 │ + │ num-unfiltered │ 99 │ + └────────────────┴───────┘ + ┌───┬─────┬─────┐ + │ > │ # │ key │ + ├───┼─────┼─────┤ + │ │ 0 │ 1 │ + │ │ 100 │ 2 │ + │ * │ 200 │ 3 │ + │ │ 300 │ 4 │ + └───┴─────┴─────┘ + |}] + ;; + + let%expect_test "dispatching another focus action while `key_rank` is pending should \ + 'cancel' it." + = + let test = test () in + Handle.show test.handle; + [%expect + {| + ((focused ()) (num_filtered_rows ())) + ┌────────────────┬───────┐ + │ metric │ value │ + ├────────────────┼───────┤ + │ rows-before │ 0 │ + │ rows-after │ 95 │ + │ num-filtered │ 99 │ + │ num-unfiltered │ 99 │ + └────────────────┴───────┘ + ┌───┬─────┬─────┐ + │ > │ # │ key │ + ├───┼─────┼─────┤ + │ │ 0 │ 1 │ + │ │ 100 │ 2 │ + │ │ 200 │ 3 │ + │ │ 300 │ 4 │ + └───┴─────┴─────┘ + |}]; + Handle.do_actions test.handle [ Focus_row 150 ]; + Handle.show test.handle; + [%expect + {| + ((focused ()) (num_filtered_rows ())) + ┌────────────────┬───────┐ + │ metric │ value │ + ├────────────────┼───────┤ + │ rows-before │ 0 │ + │ rows-after │ 95 │ + │ num-filtered │ 99 │ + │ num-unfiltered │ 99 │ + └────────────────┴───────┘ + ┌───┬─────┬─────┐ + │ > │ # │ key │ + ├───┼─────┼─────┤ + │ │ 0 │ 1 │ + │ │ 100 │ 2 │ + │ │ 200 │ 3 │ + │ │ 300 │ 4 │ + └───┴─────┴─────┘ + |}]; + Handle.do_actions test.handle [ Focus_down ]; + Handle.show test.handle; + [%expect + {| + scrolling to index 0 at 0.0px + ((focused ()) (num_filtered_rows ())) + ┌────────────────┬───────┐ + │ metric │ value │ + ├────────────────┼───────┤ + │ rows-before │ 0 │ + │ rows-after │ 95 │ + │ num-filtered │ 99 │ + │ num-unfiltered │ 99 │ + └────────────────┴───────┘ + ┌───┬─────┬─────┐ + │ > │ # │ key │ + ├───┼─────┼─────┤ + │ * │ 0 │ 1 │ + │ │ 100 │ 2 │ + │ │ 200 │ 3 │ + │ │ 300 │ 4 │ + └───┴─────┴─────┘ + |}]; + Handle.advance_clock_by test.handle (Time_ns.Span.of_int_ms 10); + Handle.recompute_view_until_stable test.handle; + (* Focus should not have changed, because the pending `key_rank` effect should have + been "cancelled". *) + Handle.show test.handle; + [%expect + {| + ((focused ()) (num_filtered_rows ())) + ┌────────────────┬───────┐ + │ metric │ value │ + ├────────────────┼───────┤ + │ rows-before │ 0 │ + │ rows-after │ 95 │ + │ num-filtered │ 99 │ + │ num-unfiltered │ 99 │ + └────────────────┴───────┘ + ┌───┬─────┬─────┐ + │ > │ # │ key │ + ├───┼─────┼─────┤ + │ * │ 0 │ 1 │ + │ │ 100 │ 2 │ + │ │ 200 │ 3 │ + │ │ 300 │ 4 │ + └───┴─────┴─────┘ + |}] + ;; + + let%expect_test "If 2 `key_rank` calls are being processed in parallel, the last one \ + scheduled should win" + = + let test = test () in + Handle.show test.handle; + [%expect + {| + ((focused ()) (num_filtered_rows ())) + ┌────────────────┬───────┐ + │ metric │ value │ + ├────────────────┼───────┤ + │ rows-before │ 0 │ + │ rows-after │ 95 │ + │ num-filtered │ 99 │ + │ num-unfiltered │ 99 │ + └────────────────┴───────┘ + ┌───┬─────┬─────┐ + │ > │ # │ key │ + ├───┼─────┼─────┤ + │ │ 0 │ 1 │ + │ │ 100 │ 2 │ + │ │ 200 │ 3 │ + │ │ 300 │ 4 │ + └───┴─────┴─────┘ + |}]; + Handle.do_actions test.handle [ Focus_row 150 ]; + Handle.recompute_view_until_stable test.handle; + Handle.advance_clock_by test.handle (Time_ns.Span.of_int_ms 5); + Handle.do_actions test.handle [ Focus_row 151 ]; + Handle.recompute_view_until_stable test.handle; + Handle.advance_clock_by test.handle (Time_ns.Span.of_int_ms 6); + (* At this point, the first call should have finished, but it's been "cancelled". *) + Handle.show test.handle; + [%expect + {| + ((focused ()) (num_filtered_rows ())) + ┌────────────────┬───────┐ + │ metric │ value │ + ├────────────────┼───────┤ + │ rows-before │ 0 │ + │ rows-after │ 95 │ + │ num-filtered │ 99 │ + │ num-unfiltered │ 99 │ + └────────────────┴───────┘ + ┌───┬─────┬─────┐ + │ > │ # │ key │ + ├───┼─────┼─────┤ + │ │ 0 │ 1 │ + │ │ 100 │ 2 │ + │ │ 200 │ 3 │ + │ │ 300 │ 4 │ + └───┴─────┴─────┘ + |}]; + Handle.advance_clock_by test.handle (Time_ns.Span.of_int_ms 5); + Handle.recompute_view_until_stable test.handle; + (* And now, everything should be completed. *) + Handle.show test.handle; + [%expect + {| + scrolling to index 3 at 40.0px + ((focused ()) (num_filtered_rows ())) + ┌────────────────┬───────┐ + │ metric │ value │ + ├────────────────┼───────┤ + │ rows-before │ 0 │ + │ rows-after │ 95 │ + │ num-filtered │ 99 │ + │ num-unfiltered │ 99 │ + └────────────────┴───────┘ + ┌───┬─────┬─────┐ + │ > │ # │ key │ + ├───┼─────┼─────┤ + │ │ 0 │ 1 │ + │ │ 100 │ 2 │ + │ │ 200 │ 3 │ + │ * │ 300 │ 4 │ + └───┴─────┴─────┘ + |}] + ;; + end) +;; + let%expect_test "show that scrolling out of a basic table will keep the focus" = let test = Test.create @@ -2960,7 +3247,11 @@ let%expect_test "show that scrolling out of a custom table will execute the pres Test.create ~map:big_map ~stats:false - (Test.Component.expert_for_testing_compute_presence ~collate ~presence ()) + (Test.Component.expert_for_testing_compute_presence_and_key_rank + ~collate + ~presence + ~key_rank:(fun ~actual_key_rank -> Bonsai.read actual_key_rank) + ()) in Bonsai.Var.set rank (Between (0, 10)); Handle.show test.handle; @@ -3262,7 +3553,7 @@ let%test_module "dynamic columns with visibility" = |> Value.return in let component = - let%sub collate = + let%sub collate, _ = let collate = { Collate.filter = None ; order = Compare.Unchanged diff --git a/web_ui/partial_render_table/test/different_column_types_produce_same_vdom_tests.ml b/web_ui/partial_render_table/test/different_column_types_produce_same_vdom_tests.ml index e7ba0060..69d63074 100644 --- a/web_ui/partial_render_table/test/different_column_types_produce_same_vdom_tests.ml +++ b/web_ui/partial_render_table/test/different_column_types_produce_same_vdom_tests.ml @@ -9,7 +9,7 @@ module Vdom_tests (Columns : Shared_with_bench.S) = struct let component = let columns = Columns.all in let rows = Value.return (Shared_with_bench.Row.init_rows 5) in - let%sub collate = + let%sub collate, _ = let collate = Value.return { Incr_map_collate.Collate.filter = () diff --git a/web_ui/partial_render_table/test/shared.ml b/web_ui/partial_render_table/test/shared.ml index a84fb241..5c4efda8 100644 --- a/web_ui/partial_render_table/test/shared.ml +++ b/web_ui/partial_render_table/test/shared.ml @@ -440,16 +440,17 @@ module Test = struct } ;; - let expert_for_testing_compute_presence + let expert_for_testing_compute_presence_and_key_rank ?(theming = `Themed) ~collate ~presence + ~key_rank () input _filter = let component = - let%sub collation = + let%sub collation, actual_key_rank = Table_expert.collate ~filter_equal:[%compare.equal: unit] ~order_equal:[%compare.equal: unit] @@ -468,6 +469,7 @@ module Test = struct ] |> Table_expert.Columns.Dynamic_cells.lift in + let%sub key_rank = key_rank ~actual_key_rank in Table_expert.component (module Int) ~theming @@ -475,6 +477,7 @@ module Test = struct (By_row { on_change = Value.return (Fn.const Effect.Ignore) ; compute_presence = (fun focus -> presence ~focus ~collation) + ; key_rank }) ~row_height:(Value.return (`Px 10)) ~columns diff --git a/web_ui/partial_render_table/test/vdom_based_tests.ml b/web_ui/partial_render_table/test/vdom_based_tests.ml index 542d8be3..2c22126a 100644 --- a/web_ui/partial_render_table/test/vdom_based_tests.ml +++ b/web_ui/partial_render_table/test/vdom_based_tests.ml @@ -923,7 +923,7 @@ let%expect_test "table body is not recomputed more often than necessary" = let test = Test.create (fun input _filter_var -> let component = - let%sub collation = + let%sub collation, key_rank = Table_expert.collate ~filter_equal:[%compare.equal: unit] ~order_equal:[%compare.equal: unit] @@ -953,6 +953,7 @@ let%expect_test "table body is not recomputed more often than necessary" = (By_row { on_change = Value.return (Fn.const Effect.Ignore) ; compute_presence = (fun focus -> return focus) + ; key_rank }) ~row_height:(Value.return (`Px 10)) ~columns diff --git a/web_ui/partial_render_table/test/vdom_based_tests_legacy_unthemed.ml b/web_ui/partial_render_table/test/vdom_based_tests_legacy_unthemed.ml index 373f2844..d2e2bc97 100644 --- a/web_ui/partial_render_table/test/vdom_based_tests_legacy_unthemed.ml +++ b/web_ui/partial_render_table/test/vdom_based_tests_legacy_unthemed.ml @@ -876,7 +876,7 @@ let%expect_test "table body is not recomputed more often than necessary" = let test = Test.create (fun input _filter_var -> let component = - let%sub collation = + let%sub collation, key_rank = Table_expert.collate ~filter_equal:[%compare.equal: unit] ~order_equal:[%compare.equal: unit] @@ -907,6 +907,7 @@ let%expect_test "table body is not recomputed more often than necessary" = (By_row { on_change = Value.return (Fn.const Effect.Ignore) ; compute_presence = (fun focus -> return focus) + ; key_rank }) ~row_height:(Value.return (`Px 10)) ~columns