Skip to content

Commit 784774d

Browse files
committed
Add conversions between Callback and Future
Closes #216
1 parent a14cbb5 commit 784774d

File tree

4 files changed

+120
-3
lines changed

4 files changed

+120
-3
lines changed

core/src/main/scala/japgolly/scalajs/react/Callback.scala

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package japgolly.scalajs.react
22

33
import org.scalajs.dom.console
44
import scala.annotation.implicitNotFound
5-
import scala.concurrent.{Future, Promise}
5+
import scala.concurrent.{ExecutionContext, Future, Promise}
66
import scala.concurrent.duration.{FiniteDuration, MILLISECONDS}
77
import scala.scalajs.js
88
import js.{undefined, UndefOr, Function0 => JFn0, Function1 => JFn1}
@@ -52,6 +52,17 @@ object Callback {
5252
@inline def byName(f: => Callback): Callback =
5353
CallbackTo(f.runNow())
5454

55+
/**
56+
* Wraps a [[Future]] so that it is repeatable, and so that its inner callback is run when the future completes.
57+
*
58+
* The result is discarded. To retain it, use [[CallbackTo.future)]] instead.
59+
*
60+
* WARNING: Futures are scheduled to run as soon as they're created. Ensure that the argument you provide creates a
61+
* new [[Future]]; don't reference an existing one.
62+
*/
63+
def future[A](f: => Future[CallbackTo[A]])(implicit ec: ExecutionContext): Callback =
64+
CallbackTo.future(f).voidExplicit[Future[A]]
65+
5566
/**
5667
* Convenience for applying a condition to a callback, and returning `Callback.empty` when the condition isn't
5768
* satisfied.
@@ -143,6 +154,15 @@ object CallbackTo {
143154
@inline def byName[A](f: => CallbackTo[A]): CallbackTo[A] =
144155
CallbackTo(f.runNow())
145156

157+
/**
158+
* Wraps a [[Future]] so that it is repeatable, and so that its inner callback is run when the future completes.
159+
*
160+
* WARNING: Futures are scheduled to run as soon as they're created. Ensure that the argument you provide creates a
161+
* new [[Future]]; don't reference an existing one.
162+
*/
163+
def future[A](f: => Future[CallbackTo[A]])(implicit ec: ExecutionContext): CallbackTo[Future[A]] =
164+
CallbackTo(f.map(_.runNow()))
165+
146166
/**
147167
* Serves as a temporary placeholder for a callback until you supply a real implementation.
148168
*
@@ -164,6 +184,18 @@ object CallbackTo {
164184
@deprecated("", "not really deprecated")
165185
def TODO[A](result: => A, reason: => String): CallbackTo[A] =
166186
Callback.todoImpl(Some(() => reason)) >> CallbackTo(result)
187+
188+
final class ReactExt_CallbackToFuture[A](private val _c: () => Future[A]) extends AnyVal {
189+
@inline private def c = new CallbackTo(_c)
190+
191+
/**
192+
* Turns a `CallbackTo[Future[A]]` into a `Future[A]`.
193+
*
194+
* WARNING: This will trigger the execution of the [[Callback]].
195+
*/
196+
def toFlatFuture(implicit ec: ExecutionContext): Future[A] =
197+
c.toFuture.flatMap(identity)
198+
}
167199
}
168200

169201
// =====================================================================================================================
@@ -268,6 +300,14 @@ final class CallbackTo[A] private[react] (private[CallbackTo] val f: () => A) ex
268300
def void: Callback =
269301
ret(())
270302

303+
/**
304+
* Discard the value produced by this callback.
305+
*
306+
* This method allows you to be explicit about the type you're discarding (which may change in future).
307+
*/
308+
@inline def voidExplicit[B](implicit ev: A =:= B): Callback =
309+
void
310+
271311
def conditionally(cond: => Boolean): CallbackTo[Option[A]] =
272312
CallbackTo(if (cond) Some(f()) else None)
273313

@@ -394,6 +434,12 @@ final class CallbackTo[A] private[react] (private[CallbackTo] val f: () => A) ex
394434
p.future
395435
}
396436

437+
/**
438+
* Schedules an instance of this callback to run asynchronously.
439+
*/
440+
def toFuture(implicit ec: ExecutionContext): Future[A] =
441+
Future(runNow())
442+
397443
/**
398444
* Record the duration of this callback's execution.
399445
*/
@@ -459,7 +505,7 @@ final class CallbackTo[A] private[react] (private[CallbackTo] val f: () => A) ex
459505
bool2(b)(_() || _())
460506

461507
/**
462-
* Negates the callback result (so long as its boolean).
508+
* Negates the callback result (so long as it's boolean).
463509
*/
464510
def !(implicit ev: ThisIsBool): CallbackB =
465511
ev(this).map(!_)

core/src/main/scala/japgolly/scalajs/react/package.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package japgolly.scalajs
22

33
import org.scalajs.dom, dom.html
4+
import scala.concurrent.{ExecutionContext, Future}
45
import scala.scalajs.js
56
import js.{Dynamic, Object, Any => JAny, Function => JFn}
67

@@ -205,4 +206,8 @@ package object react extends ReactEventAliases {
205206
@inline def only: Option[ReactNode] =
206207
try { Some(React.Children.only(c))} catch { case t: Throwable => None}
207208
}
209+
210+
@inline implicit def ReactExt_CallbackToFuture[A](c: CallbackTo[Future[A]]) =
211+
new CallbackTo.ReactExt_CallbackToFuture(() => c.runNow())
212+
208213
}

doc/CHANGELOG-0.10.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,48 @@
11
# 0.10.1 (unreleased)
22

3-
* Upgrade Scala.JS to 0.6.5.
3+
* Added conversions between `Callback` and `Future`.
4+
5+
| Input | Method | Output |
6+
| -------------------------- | ---------------------- | ----------------------- |
7+
| `CallbackTo[A]` | `cb.toFuture` | `Future[A]` |
8+
| `CallbackTo[Future[A]]` | `cb.toFlatFuture` | `Future[A]` |
9+
| `=> Future[A]` | `CallbackTo(f)` | `CallbackTo[Future[A]]` |
10+
| `=> Future[CallbackTo[A]]` | `Callback.future(f)` | `Callback` |
11+
| `=> Future[CallbackTo[A]]` | `CallbackTo.future(f)` | `CallbackTo[Future[A]]` |
12+
13+
**NOTE:** It's important that when going from `Future` to `Callback`, you're aware of when the `Future` is instantiated.
14+
15+
```scala
16+
def queryServer: Future[Data] = ???
17+
18+
def updateComponent: Future[Callback] =
19+
queryServer.map($ setState _)
20+
21+
// This is GOOD because the callback wraps the updateComponent *function*, not an instance.
22+
Callback.future(updateComponent)
23+
24+
// This is BAD because the callback wraps a single instance of updateComponent.
25+
// 1) The server will be contacted immediately instead of when the callback executes.
26+
// 2) If the callback is execute more than once, the future and old result will be reused.
27+
val f = updateComponent
28+
Callback.future(f)
29+
30+
// This is BAD because the callback wraps a single instance of updateComponent.
31+
// 1) The server will be contacted immediately instead of when the callback executes.
32+
// 2) If the callback is execute more than once, the future and old result will be reused.
33+
34+
// This is GOOD too because the future is created inside the callback.
35+
Callback.future {
36+
val f = updateComponent
37+
f.onComplete(???)
38+
f
39+
}
40+
```
41+
442
* Add `React.Children.toArray`.
543

44+
* Upgrade Scala.JS to 0.6.5.
45+
646
---
747

848
# 0.10.0 ([commit log](https://github.com/japgolly/scalajs-react/compare/v0.9.2...v0.10.0))

test/src/test/scala/japgolly/scalajs/react/CallbackTest.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package japgolly.scalajs.react
22

3+
import scala.concurrent._
4+
import scala.concurrent.duration._
5+
import scala.scalajs.concurrent.JSExecutionContext.Implicits.runNow
36
import utest._
47

58
object CallbackTest extends TestSuite {
@@ -32,9 +35,32 @@ object CallbackTest extends TestSuite {
3235
"Callback(Callback)" - assertFails(compileError("Callback(cb)"))
3336
"Callback(CallbackTo)" - assertFails(compileError("Callback(cbI)"))
3437
}
38+
3539
'lazily -
3640
testEvalStrategy(Callback lazily _, 0, 1, 1)
41+
3742
'byName -
3843
testEvalStrategy(Callback byName _, 0, 1, 2)
44+
45+
'future {
46+
'repeatable {
47+
var runs = 0
48+
def modState = Callback(runs += 1)
49+
def f = Future(modState)
50+
val c = Callback.future(f)
51+
assert(runs == 0)
52+
c.runNow()
53+
c.runNow()
54+
assert(runs == 2)
55+
}
56+
57+
'toFlatFuture {
58+
val c = CallbackTo(Future(666))
59+
val f = c.toFlatFuture
60+
var i = 0
61+
f.map(i = _)
62+
assert(i == 666)
63+
}
64+
}
3965
}
4066
}

0 commit comments

Comments
 (0)