Skip to content

Commit

Permalink
Fix #13: add attr.ref and attr.bindRef
Browse files Browse the repository at this point in the history
  • Loading branch information
Tarmil committed Dec 24, 2018
1 parent df1b6a2 commit 0246aa8
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 10 deletions.
8 changes: 8 additions & 0 deletions src/Bolero/Components.fs
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,11 @@ type ProgramComponent<'model, 'msg>() =
member this.Dispose() =
System.EventHandler<string> this.OnLocationChanged
|> this.UriHelper.OnLocationChanged.RemoveHandler

type ElementRefBinder() =

let mutable ref = Unchecked.defaultof<ElementRef>

member this.Ref = ref

member internal this.SetRef(r) = ref <- r
8 changes: 8 additions & 0 deletions src/Bolero/Html.fs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,14 @@ module attr =
let classes (classes: list<string>) : Attr =
"class" => String.concat " " classes

/// Bind an element reference.
let inline ref (f: ElementRef -> unit) =
Attr.Ref (Action<ElementRef>(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
Expand Down
4 changes: 4 additions & 0 deletions src/Bolero/Node.fs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ namespace Bolero

open System
#if !IS_DESIGNTIME
open Microsoft.AspNetCore.Blazor
open Microsoft.AspNetCore.Blazor.Components
#endif

/// HTML attribute or Blazor component parameter.
type Attr =
| Attr of string * obj
| Attrs of list<Attr>
#if !IS_DESIGNTIME
| Ref of Action<ElementRef>
#endif

/// HTML fragment.
type Node =
Expand Down
33 changes: 24 additions & 9 deletions src/Bolero/Render.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Type, _>) node =
let getMatchParams (ty: Type) =
Expand Down
9 changes: 8 additions & 1 deletion tests/Client/Main.fs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
module Bolero.Test.Client.Main

open Microsoft.AspNetCore.Blazor.Routing
open Microsoft.JSInterop
open Elmish
open Bolero
open Bolero.Html
Expand Down Expand Up @@ -108,12 +109,18 @@ type SecretPw = Template<"""<div>
<input type="number" bind="${Value}" />
</div>""">

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 "")]
Expand Down
15 changes: 15 additions & 0 deletions tests/Unit/Tests/Html.fs
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,18 @@ module Html =
elt.AssertEventually(fun () -> out.Text = "true")
inp.Click()
elt.AssertEventually(fun () -> out.Text = "false")

[<Test>]
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")
29 changes: 29 additions & 0 deletions tests/Unit/Web/App.Html.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ElementRef>
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"]
Expand All @@ -134,4 +162,5 @@ let Tests() =
] [text "NavLink content"]
comp<BoleroComponent> ["Ident" => "bolero-component"] []
comp<Binds> [] []
comp<BindElementRef> [] []
]
6 changes: 6 additions & 0 deletions tests/Unit/Web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
</head>
<body>
<div id="app"></div>
<script>
// Used by ElementBinder test:
function setContent(element, value) {
element.innerHTML = value;
}
</script>
<script src="_framework/blazor.server.js"></script>
</body>
</html>

0 comments on commit 0246aa8

Please sign in to comment.