Skip to content

Commit

Permalink
Convert Future to Async
Browse files Browse the repository at this point in the history
  • Loading branch information
JPonte committed Nov 20, 2024
1 parent 67bc50b commit fa9d60c
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 79 deletions.
44 changes: 35 additions & 9 deletions sandbox/src/main/scala/example/Sandbox.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,20 @@ package example

import cats.effect.IO
import org.scalajs.dom
import org.scalajs.dom.document
import tyrian.*
import tyrian.Html.*
import tyrian.SVG.*
import tyrian.cmds.Dom
import tyrian.cmds.File
import tyrian.cmds.FileReader
import tyrian.cmds.LocalStorage
import tyrian.cmds.Logger
import tyrian.cmds.*
import tyrian.http.*
import tyrian.syntax.*
import tyrian.websocket.*

import scala.concurrent.duration.*
import scala.scalajs.js
import scala.scalajs.js.annotation.*
import scala.util.Random

import scalajs.js

@JSExportTopLevel("TyrianApp")
object Sandbox extends TyrianIOApp[Msg, Model]:

Expand Down Expand Up @@ -379,6 +375,23 @@ object Sandbox extends TyrianIOApp[Msg, Model]:
case Msg.FileTextRead(data) =>
(model.copy(loadedText = Option(data)), Cmd.None)

case Msg.LoadImage =>
val cmd: Cmd[IO, Msg] = ImageLoader.load(model.imageLoadingUrl)(Msg.ImageLoaded(_))

(model, cmd)

case Msg.ImageLoaded(ImageLoader.Result.ImageLoadError(message, _)) =>
(model.copy(imageLoadingError = Some(message)), Cmd.None)
case Msg.ImageLoaded(ImageLoader.Result.Image(img)) =>
(
model.copy(imageLoadingError = None),
Cmd.SideEffect {
document.getElementById("loaded_images").append(img)
}
)
case Msg.UpdateImageUrl(url) =>
(model.copy(imageLoadingUrl = url), Cmd.None)

def view(model: Model): Html[Msg] =
val navItems =
Page.values.toList.map { pg =>
Expand Down Expand Up @@ -644,7 +657,13 @@ object Sandbox extends TyrianIOApp[Msg, Model]:
model.loadedBytes
.map(data => p(s"Read ${data.length} bytes in an IArray"))
.toList
)
),
div()(
input(placeholder := "Image url", onInput(Msg.UpdateImageUrl(_)), value := model.imageLoadingUrl),
text(model.imageLoadingError.getOrElse(""))
),
button(onClick(Msg.LoadImage))("Load an image from the internet"),
div(id := "loaded_images")()
)

div(
Expand Down Expand Up @@ -758,6 +777,9 @@ enum Msg:
case SelectBytesFile
case ReadBytesFile(file: dom.File)
case FileBytesRead(fileData: IArray[Byte])
case LoadImage
case UpdateImageUrl(url: String)
case ImageLoaded(result: ImageLoader.Result)
case NoOp

enum Status:
Expand Down Expand Up @@ -809,7 +831,9 @@ final case class Model(
fruitInput: String,
loadedImage: Option[String],
loadedText: Option[String],
loadedBytes: Option[IArray[Byte]]
loadedBytes: Option[IArray[Byte]],
imageLoadingUrl: String,
imageLoadingError: Option[String]
)

final case class Fruit(name: String, available: Boolean)
Expand Down Expand Up @@ -865,6 +889,8 @@ object Model:
"",
None,
None,
None,
"https://openmoji.org/php/download_asset.php?type=emoji&emoji_hexcode=1F9A6&emoji_variant=color",
None
)

Expand Down
44 changes: 22 additions & 22 deletions tyrian/src/main/scala/tyrian/cmds/File.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import org.scalajs.dom.document
import org.scalajs.dom.html
import tyrian.Cmd

import scala.concurrent.Promise

object File:

def select[F[_]: Async, Msg](fileTypes: List[String])(
Expand All @@ -34,25 +32,27 @@ object File:
private def selectFiles[F[_]: Async, Msg](fileTypes: List[String], multiple: Boolean)(
resultToMessage: List[dom.File] => Msg
): Cmd[F, Msg] =
val task = Async[F].delay {
val input = document.createElement("input").asInstanceOf[html.Input];
val p = Promise[List[dom.File]]()
input.setAttribute("type", "file")
input.setAttribute("accept", fileTypes.mkString(","))

if multiple then input.setAttribute("multiple", "multiple")

input.addEventListener(
"change",
(e: Event) =>
e.target match {
case elem: html.Input => if elem.files.length > 0 then p.success(elem.files.toList) else ()
case _ => ()
}
)

input.click();
p.future
val task = Async[F].async[List[dom.File]] { callback =>
Async[F].delay {
val input = document.createElement("input").asInstanceOf[html.Input]
input.setAttribute("type", "file")
input.setAttribute("accept", fileTypes.mkString(","))

if multiple then input.setAttribute("multiple", "multiple")

input.addEventListener(
"change",
(e: Event) =>
e.target match {
case elem: html.Input => if elem.files.length > 0 then callback(Right(elem.files.toList)) else ()
case _ => ()
}
)

input.click()

None
}
}

Cmd.Run(Async[F].fromFuture(task), resultToMessage)
Cmd.Run(task, resultToMessage)
65 changes: 33 additions & 32 deletions tyrian/src/main/scala/tyrian/cmds/FileReader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import org.scalajs.dom.document
import org.scalajs.dom.html
import tyrian.Cmd

import scala.concurrent.Future
import scala.concurrent.Promise
import scala.scalajs.js
import scala.scalajs.js.typedarray

Expand Down Expand Up @@ -69,40 +67,43 @@ object FileReader:
private def readFile[F[_]: Async, Msg](maybeGetFile: F[Option[dom.File]], readAsType: ReadType)(
resultToMessage: Result[js.Any] => Msg
): Cmd[F, Msg] =
val task = maybeGetFile.flatMap {
case None => Future.successful(Result.NoFile("No files on specified input")).pure[F]
val task: F[Result[js.Any]] = maybeGetFile.flatMap {
case None => Result.NoFile("No files on specified input").pure[F]
case Some(file) =>
Async[F].delay {
val p = Promise[Result[js.Any]]()
val fileReader = new dom.FileReader()
fileReader.addEventListener(
"load",
(e: Event) =>
p.success(
Result.File(
name = file.name,
path = readAsType match
case ReadType.AsDataUrl => e.target.asInstanceOf[js.Dynamic].result.asInstanceOf[String]
case _ => ""
,
data = fileReader.result
)
),
false
)
fileReader.onerror = _ => p.success(Result.Error(s"Error reading from file"))

readAsType match
case ReadType.AsText => fileReader.readAsText(file)
case ReadType.AsArrayBuffer =>
fileReader.readAsArrayBuffer(file)
case ReadType.AsDataUrl => fileReader.readAsDataURL(file)

p.future
Async[F].async { callback =>
Async[F].delay {
val fileReader = new dom.FileReader()
fileReader.addEventListener(
"load",
(e: Event) =>
callback(
Right(
Result.File(
name = file.name,
path = readAsType match
case ReadType.AsDataUrl => e.target.asInstanceOf[js.Dynamic].result.asInstanceOf[String]
case _ => ""
,
data = fileReader.result
)
)
),
false
)
fileReader.onerror = _ => callback(Right(Result.Error(s"Error reading from file")))

readAsType match
case ReadType.AsText => fileReader.readAsText(file)
case ReadType.AsArrayBuffer =>
fileReader.readAsArrayBuffer(file)
case ReadType.AsDataUrl => fileReader.readAsDataURL(file)

None
}
}
}

Cmd.Run(Async[F].fromFuture(task), resultToMessage)
Cmd.Run(task, resultToMessage)

private enum ReadType derives CanEqual:
case AsText
Expand Down
34 changes: 18 additions & 16 deletions tyrian/src/main/scala/tyrian/cmds/ImageLoader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,32 @@ import org.scalajs.dom.document
import org.scalajs.dom.html
import tyrian.Cmd

import scala.concurrent.Promise

/** Given a path, this cmd will load an image and return an `HTMLImageElement` for you to make use of.
*/
object ImageLoader:

/** Load an image from the given path and produce a message */
def load[F[_]: Async, Msg](path: String)(resultToMessage: Result => Msg): Cmd[F, Msg] =
val task =
Async[F].delay {
val p = Promise[Result]()
val image: html.Image = document.createElement("img").asInstanceOf[html.Image]
image.src = path
image.onload = { (_: Event) =>
p.success(Result.Image(image))
val task: F[Result] =
Async[F].async { callback =>
Async[F].delay {
println("Used ImageLoader!")

val image: html.Image = document.createElement("img").asInstanceOf[html.Image]
image.src = path
image.onload = { (_: Event) =>
callback(Right(Result.Image(image)))
}
image.addEventListener(
"error",
(_: Event) => callback(Right(Result.ImageLoadError(s"Image load error from path '$path'", path))),
false
)
None
}
image.addEventListener(
"error",
(_: Event) => p.success(Result.ImageLoadError(s"Image load error from path '$path'", path)),
false
)
p.future
}
Cmd.Run(Async[F].fromFuture(task), resultToMessage)

Cmd.Run(task, resultToMessage)

enum Result:
case Image(img: html.Image)
Expand Down

0 comments on commit fa9d60c

Please sign in to comment.