Skip to content

Commit 4730ce5

Browse files
committed
feat: add onReceive modifier
1 parent d317ba8 commit 4730ce5

6 files changed

Lines changed: 177 additions & 0 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
namespace Fabulous.Tests.APISketchTests
2+
3+
open NUnit.Framework
4+
open Fabulous
5+
open Fabulous.Tests.APISketchTests.Platform
6+
open TestUI_Widgets
7+
open System
8+
open System.Timers
9+
10+
open type View
11+
open type Context
12+
13+
type IntArgs(delta: int) =
14+
inherit EventArgs()
15+
member _.Delta = delta
16+
17+
type NetEventSource() =
18+
let evt = new Event<EventHandler<IntArgs>, IntArgs>()
19+
20+
[<CLIEvent>]
21+
member _.Tick = evt.Publish
22+
23+
member _.Fire(delta: int) = evt.Trigger(null, IntArgs delta)
24+
25+
module ComponentOnReceiveTests =
26+
27+
[<Test>]
28+
let ``Component with onReceive increments label when event fires`` () =
29+
// Arrange (IObservable<'T> overload via IEvent upcast)
30+
use timer = new Timer(10.0)
31+
timer.AutoReset <- false // fire once per Start() to make the test deterministic
32+
33+
let view =
34+
Component("onReceive-basic") {
35+
let! count = State(0)
36+
37+
Stack().automationId("root") {
38+
Label(count.Current.ToString()).automationId("label").onReceive(timer.Elapsed, fun _ -> count.Set(count.Current + 1))
39+
}
40+
}
41+
42+
let tree = Run.startView view
43+
let label = find<TestLabel> tree "label" :> IText
44+
45+
// Assert initial
46+
Assert.AreEqual("0", label.Text)
47+
48+
// Fire timer twice and assert updates
49+
timer.Start()
50+
System.Threading.Thread.Sleep(30)
51+
Assert.AreEqual("1", label.Text)
52+
53+
timer.Start()
54+
System.Threading.Thread.Sleep(30)
55+
Assert.AreEqual("2", label.Text)
56+
57+
[<Test>]
58+
let ``Component onReceive with .NET EventHandler<'T> increments label`` () =
59+
// Arrange (IEvent<EventHandler<'T>,'T> overload)
60+
61+
let src = NetEventSource()
62+
63+
let view =
64+
Component("onReceive-netevent") {
65+
let! count = State(0)
66+
67+
Stack().automationId("root") {
68+
Label(count.Current.ToString()).automationId("label").onReceive(src.Tick, fun args -> count.Set(count.Current + args.Delta))
69+
}
70+
}
71+
72+
let tree = Run.startView view
73+
let label = find<TestLabel> tree "label" :> IText
74+
75+
Assert.AreEqual("0", label.Text)
76+
src.Fire 1
77+
Assert.AreEqual("1", label.Text)
78+
src.Fire 2
79+
Assert.AreEqual("3", label.Text)
80+
81+
[<Test>]
82+
let ``Component onReceive with F# Event<'T> increments label`` () =
83+
// Arrange (Event<'T> overload)
84+
let tick = new Event<int>()
85+
86+
let view =
87+
Component("onReceive-fsharp-event") {
88+
let! count = State(0)
89+
90+
Stack().automationId("root") {
91+
Label(count.Current.ToString()).automationId("label").onReceive(tick, fun delta -> count.Set(count.Current + delta))
92+
}
93+
}
94+
95+
let tree = Run.startView view
96+
let label = find<TestLabel> tree "label" :> IText
97+
98+
Assert.AreEqual("0", label.Text)
99+
tick.Trigger 1
100+
Assert.AreEqual("1", label.Text)
101+
tick.Trigger 2
102+
Assert.AreEqual("3", label.Text)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Fabulous.Tests.APISketchTests
2+
3+
open Fabulous
4+
5+
[<AutoOpen>]
6+
module TestUI_ComponentBuilders =
7+
type TestUI_Widgets.View with
8+
static member Component<'msg, 'marker when 'msg: equality>(key: string) = ComponentBuilder<'msg, 'marker>(key)

src/Fabulous.Tests/APISketchTests/TestUI.Widgets.fs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,30 @@ module TestUI_Widgets =
234234

235235
view :?> TestViewElement
236236

237+
let startView (view: WidgetBuilder<'msg, 'marker>) : TestViewElement =
238+
let logger =
239+
{ Log = fun _ -> ()
240+
MinLogLevel = LogLevel.Fatal }
241+
242+
let envContext = new EnvironmentContext(logger)
243+
244+
let treeContext: ViewTreeContext =
245+
{ CanReuseView = ViewHelpers.canReuseView
246+
GetViewNode = ViewNode.getViewNode
247+
Logger = logger
248+
Dispatch = fun _ -> ()
249+
GetComponent = Component.getComponent
250+
SetComponent = Component.setComponent
251+
SyncAction = fun fn -> fn() }
252+
253+
let widget = view.Compile()
254+
let widgetDef = WidgetDefinitionStore.get widget.Key
255+
256+
let struct (_node, rootView) =
257+
widgetDef.CreateView(widget, envContext, treeContext, ValueNone)
258+
259+
rootView :?> TestViewElement
260+
237261
//module View =
238262
// let inline map (fn: 'oldMsg -> 'newMsg) (this: WidgetBuilder<'oldMsg, 'marker>) : WidgetBuilder<'newMsg, 'marker> =
239263
// this.MapMsg fn

src/Fabulous.Tests/Fabulous.Tests.fsproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
<Compile Include="APISketchTests\TestUI.ViewUpdaters.fs" />
1212
<Compile Include="APISketchTests\TestUI.Attributes.fs" />
1313
<Compile Include="APISketchTests\TestUI.Widgets.fs" />
14+
<Compile Include="APISketchTests\TestUI.ComponentBuilders.fs" />
1415
<Compile Include="APISketchTests\APISketchTests.fs" />
16+
<Compile Include="APISketchTests\ComponentOnReceiveTests.fs" />
1517
<Compile Include="Generators.fs" />
1618
<Compile Include="AttributesTests.fs" />
1719
<Compile Include="ViewTests.fs" />
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
namespace Fabulous
2+
3+
open System
4+
open System.Runtime.CompilerServices
5+
open Fabulous.ScalarAttributeDefinitions
6+
7+
module OnReceiveAttribute =
8+
let OnReceive: SimpleScalarAttributeDefinition<IViewNode -> IDisposable> =
9+
{ Key =
10+
SimpleScalarAttributeDefinition.CreateAttributeData(
11+
ScalarAttributeComparers.noCompare,
12+
(fun _ (newValueOpt: (IViewNode -> IDisposable) voption) (node: IViewNode) ->
13+
match node.TryGetHandler("onReceive") with
14+
| ValueSome d -> d.Dispose()
15+
| ValueNone -> ()
16+
17+
match newValueOpt with
18+
| ValueNone -> node.RemoveHandler("onReceive")
19+
| ValueSome subscribe ->
20+
let d = subscribe node
21+
node.SetHandler("onReceive", d))
22+
)
23+
|> AttributeDefinitionStore.registerScalar
24+
Name = "OnReceive" }
25+
26+
type OnReceiveExtensions =
27+
[<Extension>]
28+
static member onReceive(this: WidgetBuilder<'msg, 'marker>, source: IObservable<'T>, action: 'T -> unit) =
29+
let subscribe (node: IViewNode) : IDisposable =
30+
source.Subscribe(fun value -> node.TreeContext.SyncAction(fun () -> action value))
31+
32+
this.AddScalar(OnReceiveAttribute.OnReceive.WithValue(subscribe))
33+
34+
[<Extension>]
35+
static member onReceive(this: WidgetBuilder<'msg, 'marker>, source: IEvent<EventHandler<'T>, 'T>, action: 'T -> unit) =
36+
OnReceiveExtensions.onReceive(this, (source :> IObservable<'T>), action)
37+
38+
[<Extension>]
39+
static member onReceive(this: WidgetBuilder<'msg, 'marker>, source: Event<'T>, action: 'T -> unit) =
40+
OnReceiveExtensions.onReceive(this, (source.Publish :> IObservable<'T>), action)

src/Fabulous/Fabulous.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
<Compile Include="Components\Environment.fs" />
5454
<Compile Include="Components\EnvironmentObject.fs" />
5555
<Compile Include="Components\Mvu.fs" />
56+
<Compile Include="Components\OnReceive.fs" />
5657
<Compile Include="View.fs" />
5758
</ItemGroup>
5859
<ItemGroup>

0 commit comments

Comments
 (0)