From 0246aa859cb4b5bdb4b5e0523a1115a94f583da0 Mon Sep 17 00:00:00 2001 From: Loic Denuziere Date: Mon, 24 Dec 2018 11:11:48 +0100 Subject: [PATCH] Fix #13: add attr.ref and attr.bindRef --- src/Bolero/Components.fs | 8 ++++++++ src/Bolero/Html.fs | 8 ++++++++ src/Bolero/Node.fs | 4 ++++ src/Bolero/Render.fs | 33 ++++++++++++++++++++++++--------- tests/Client/Main.fs | 9 ++++++++- tests/Unit/Tests/Html.fs | 15 +++++++++++++++ tests/Unit/Web/App.Html.fs | 29 +++++++++++++++++++++++++++++ tests/Unit/Web/index.html | 6 ++++++ 8 files changed, 102 insertions(+), 10 deletions(-) diff --git a/src/Bolero/Components.fs b/src/Bolero/Components.fs index 76218ce5..f181bd42 100644 --- a/src/Bolero/Components.fs +++ b/src/Bolero/Components.fs @@ -158,3 +158,11 @@ type ProgramComponent<'model, 'msg>() = member this.Dispose() = System.EventHandler this.OnLocationChanged |> this.UriHelper.OnLocationChanged.RemoveHandler + +type ElementRefBinder() = + + let mutable ref = Unchecked.defaultof + + member this.Ref = ref + + member internal this.SetRef(r) = ref <- r diff --git a/src/Bolero/Html.fs b/src/Bolero/Html.fs index 79d3b158..86264bd2 100644 --- a/src/Bolero/Html.fs +++ b/src/Bolero/Html.fs @@ -599,6 +599,14 @@ module attr = let classes (classes: list) : Attr = "class" => String.concat " " classes + /// Bind an element reference. + let inline ref (f: ElementRef -> unit) = + Attr.Ref (Action(f)) + + /// Bind an element reference. + let bindRef (refBinder: ElementRefBinder) = + ref refBinder.SetRef + // BEGIN ATTRS /// Create an HTML `accept` attribute. let accept (v: obj) : Attr = "accept" => v diff --git a/src/Bolero/Node.fs b/src/Bolero/Node.fs index 2b7a3971..f56b5d16 100644 --- a/src/Bolero/Node.fs +++ b/src/Bolero/Node.fs @@ -22,6 +22,7 @@ namespace Bolero open System #if !IS_DESIGNTIME +open Microsoft.AspNetCore.Blazor open Microsoft.AspNetCore.Blazor.Components #endif @@ -29,6 +30,9 @@ open Microsoft.AspNetCore.Blazor.Components type Attr = | Attr of string * obj | Attrs of list +#if !IS_DESIGNTIME + | Ref of Action +#endif /// HTML fragment. type Node = diff --git a/src/Bolero/Render.fs b/src/Bolero/Render.fs index d51c69cf..7e668fae 100644 --- a/src/Bolero/Render.fs +++ b/src/Bolero/Render.fs @@ -129,31 +129,46 @@ let rec renderNode (builder: RenderTreeBuilder) (matchCache: Type -> int * (obj | Elt (name, attrs, children) -> builder.OpenElement(sequence, name) let sequence = sequence + 1 - let sequence = Seq.fold (renderAttr builder) sequence attrs + let sequence = renderAttrs builder sequence attrs let sequence = List.fold (renderNode builder matchCache) sequence children builder.CloseElement() sequence | Component (comp, attrs, children) -> builder.OpenComponent(sequence, comp) let sequence = sequence + 1 - let sequence = Seq.fold (renderAttr builder) sequence attrs + let sequence = renderAttrs builder sequence attrs let hasChildren = not (List.isEmpty children) if hasChildren then let frag = RenderFragment(fun builder -> builder.AddContent(sequence + 1, RenderFragment(fun builder -> - Seq.fold (renderNode builder matchCache) 0 children + List.fold (renderNode builder matchCache) 0 children |> ignore))) builder.AddAttribute(sequence, "ChildContent", frag) builder.CloseComponent() sequence + (if hasChildren then 2 else 0) -/// Render an attribute with `name` and `value` into `builder` at `sequence` number. -and renderAttr builder sequence = function - | Attr (name, value) -> - builder.AddAttribute(sequence, name, value) +/// Render a list of attributes into `builder` at `sequence` number. +and renderAttrs builder sequence attrs = + // AddAttribute calls want to be just after the OpenElement/OpenComponent call, + // so we make sure that AddElementReferenceCapture is called last. + let rec run attrs = + ((sequence, None), attrs) + ||> List.fold (fun (sequence, ref) attr -> + match attr with + | Attr (name, value) -> + builder.AddAttribute(sequence, name, value) + (sequence + 1, ref) + | Attrs attrs -> + run attrs + | Ref ref -> + (sequence, Some ref) + ) + match run attrs with + | sequence, Some r -> + builder.AddElementReferenceCapture(sequence, r) sequence + 1 - | Attrs attrs -> - List.fold (renderAttr builder) sequence attrs + | sequence, None -> + sequence let RenderNode builder (matchCache: Dictionary) node = let getMatchParams (ty: Type) = diff --git a/tests/Client/Main.fs b/tests/Client/Main.fs index 39bf671b..435b2554 100644 --- a/tests/Client/Main.fs +++ b/tests/Client/Main.fs @@ -21,6 +21,7 @@ module Bolero.Test.Client.Main open Microsoft.AspNetCore.Blazor.Routing +open Microsoft.JSInterop open Elmish open Bolero open Bolero.Html @@ -108,12 +109,18 @@ type SecretPw = Template<"""
"""> +let btnRef = ElementRefBinder() + let viewForm model dispatch = div [] [ input [attr.value model.input; on.change (fun e -> dispatch (SetInput (unbox e.Value)))] input [ + attr.bindRef btnRef attr.``type`` "submit" - on.click (fun _ -> dispatch Submit) + on.click (fun _ -> + JSRuntime.Current.InvokeAsync("console.log", btnRef.Ref) |> ignore + dispatch Submit + ) attr.style (if model.input = "" then "color:gray;" else null) ] div [] [text (defaultArg model.submitted "")] diff --git a/tests/Unit/Tests/Html.fs b/tests/Unit/Tests/Html.fs index f58a6365..6453df76 100644 --- a/tests/Unit/Tests/Html.fs +++ b/tests/Unit/Tests/Html.fs @@ -160,3 +160,18 @@ module Html = elt.AssertEventually(fun () -> out.Text = "true") inp.Click() elt.AssertEventually(fun () -> out.Text = "false") + + [] + let ElementRefBinder() = + let btn = elt.ByClass("element-ref") + Assert.IsNotNull(btn) + btn.Click() + elt.AssertEventually( + (fun () -> btn.Text = "ElementRef 1 is bound"), + "attr.ref") + let btn = elt.ByClass("element-ref-binder") + Assert.IsNotNull(btn) + btn.Click() + elt.AssertEventually( + (fun () -> btn.Text = "ElementRef 2 is bound"), + "attr.bindRef") diff --git a/tests/Unit/Web/App.Html.fs b/tests/Unit/Web/App.Html.fs index 85e0283a..9a189a7c 100644 --- a/tests/Unit/Web/App.Html.fs +++ b/tests/Unit/Web/App.Html.fs @@ -22,8 +22,10 @@ module Bolero.Tests.Web.App.Html open Bolero open Bolero.Html +open Microsoft.AspNetCore.Blazor open Microsoft.AspNetCore.Blazor.Routing open Microsoft.AspNetCore.Blazor.Components +open Microsoft.JSInterop type SomeUnion = | Empty @@ -116,6 +118,32 @@ type Binds() = span [attr.``class`` "bind-checked-out"] [textf "%b" checkedState.Value] ] +type BindElementRef() = + inherit Component() + + let mutable elt1 = Unchecked.defaultof + let elt2 = ElementRefBinder() + + override this.Render() = + concat [ + button [ + attr.``class`` "element-ref" + attr.ref (fun r -> elt1 <- r) + on.click (fun _ -> + JSRuntime.Current.InvokeAsync("setContent", elt1, "ElementRef 1 is bound") + |> ignore + ) + ] [text "Click me"] + button [ + attr.``class`` "element-ref-binder" + attr.bindRef elt2 + on.click (fun _ -> + JSRuntime.Current.InvokeAsync("setContent", elt2.Ref, "ElementRef 2 is bound") + |> ignore + ) + ] [text "Click me"] + ] + let Tests() = div [attr.id "test-fixture-html"] [ p [attr.id "element-with-id"] [text "Contents of element with id"] @@ -134,4 +162,5 @@ let Tests() = ] [text "NavLink content"] comp ["Ident" => "bolero-component"] [] comp [] [] + comp [] [] ] diff --git a/tests/Unit/Web/index.html b/tests/Unit/Web/index.html index d48ad749..f4aab4cc 100644 --- a/tests/Unit/Web/index.html +++ b/tests/Unit/Web/index.html @@ -6,6 +6,12 @@
+