raquo / Airstream

State propagation and event streams with mandatory ownership and no glitches
MIT License
246 stars 28 forks source link

[Question] Simulate DomEventStream click on page load #107

Closed mobilemindtec closed 1 year ago

mobilemindtec commented 1 year ago

Hello,

is there any way to simulate a "click" using a DomEventStream? I'm trying to reproduce and example in "The introduction to Reactive Programming you've been missing", and this piece is missing to finish.

I've had success calling element.click() directly, but I want to know if there's another way to start the flow when the app loads without someone clicking a button.

This is my code:


val githubUrl = "https://api.github.com/users"

case class User(id: Int,
                login: String,
                url: String,
                avatar_url: String
               ) derives NativeConverter

private val dynOwner = new DynamicOwner(() => ())

private val dynSub = DynamicSubscription.unsafe(
  dynOwner,
  activate = (owner: Owner) =>
    given o: Owner = owner
    appStart()
)

@main def main(args: String*) = dynOwner.activate()

def appStart(using owner: Owner) =
  val refreshButton = document.querySelector(".refresh").asInstanceOf[HTMLButtonElement]
  val refreshClickStream = DomEventStream[dom.MouseEvent](refreshButton, "click")

  val closeButton = document.querySelector(".close1").asInstanceOf[HTMLButtonElement]
  val closeClickStream = DomEventStream[dom.MouseEvent](closeButton, "click")

  val requestStream = refreshClickStream
    .map {
      _ =>
        println("get url")
        val randomOffset = Math.floor(Math.random()*500)
        s"$githubUrl?since=$randomOffset"
    }

  val responseStream = requestStream
    .flatMap {
      requestUrl =>
        println(s"fetch url $requestUrl")
        FetchStream
          .get(requestUrl)
          .map(s => JSON.parse(s))
          .map(r => NativeConverter[List[User]].fromNative(r))
          .recover {
            case e =>
              println(s"error ${e}")
              Some(List())
          }
    }

  val suggestionStream = closeClickStream
    .combineWith(responseStream).map {
      (_, listUsers) =>
        println(s"users= ${listUsers.length}")
        listUsers(Math.floor(Math.random() * listUsers.length).toInt)
    }

  val obs = suggestionStream.addObserver(Observer{
    suggestion => println(s"suggestion: $suggestion")
  })

  refreshButton.click()
  closeButton.click()

I'm already using Laminar in some small projects, and now I'm studying AirStream to better understand how to work with Stream. I still don't have a complete understanding of reactive programming.

Thanks for the great libraries!

yurique commented 1 year ago

If I understand your challenge correctly, here's what I'd do:

  val requestStream = EventStream.merge(
    EventStream.fromValue(()), // emit a Unit event initially
    refreshClickStream.mapToUnit // then emit all events from the refreshClickStream stream (mapped to Unit)
  ).map {
      _ =>
        println("get url")
        val randomOffset = Math.floor(Math.random()*500)
        s"$githubUrl?since=$randomOffset"
    }
raquo commented 1 year ago

Observables are (kinda) declarative, that is, you need to specify their behaviour when you're creating them.

When you create a DomEventStream, you tell it which element to look at, and what event type ("click") to listen to. You can call .click() on that element to simulate the click event and kinda "fool" the observable into thinking the user actually clicked it, but that is a special method provided by the browser.

So, you can't cause DomEventStream to emit events that aren't clicks, but you can create another stream that will merge your DomEventStream with some other stream. Yurique above showed how to merge your refreshClickStream with a stream that simply emits one event when it's started (EventStream.fromValue(())), however you could instead combine it with any other stream.

There are several ways to create a stream that lets you fire events imperatively, whenever you please – they're listed here among some other useful stream types.

For example, you could create a stream from scratch using val (manualStream, callback) = EventStream.withUnitCallback, and then call callback() to make manualStream fire an event. Then, you can merge this manualStream with refreshClickStream.mapToUnit, and you get a stream that emits whenever you call callback(), and also whenever the refresh button is called.

That way, instead of faking a click, you're properly declaring your dataflow, creating a stream that depends not only on clicks but also on your other logic that is needed to produce the desired results.

PS I see you're using Airstream without Laminar – this is certainly possible, but is more manual and more cumbersome because you need to manage ownership lifecycles yourself. If you get bogged down with ownership, just know that it's much smoother with Laminar.

mobilemindtec commented 1 year ago

Hi,

@raquo thanks, that worked!.

@raquo This isn't real work, it's just a study on FPS. I'm trying to reproduce this using AirStream https://gist.github.com/staltz/868e7e9bc2a7b8c1f754. For real projects I'm already using Laminar.

yurique commented 1 year ago

I wonder if we should convert this into a Discussion, for posterity :)

mobilemindtec commented 1 year ago

Of course, it would be great. I'll finish porting the example from RxJS to Scala/AirStream and post the complete code.

By the way, how do I make this code run only once? When I connect requestUrlStream with FetchStream, the event gets processed two time. I could use a .drop(1) to listen to just one. Is there another way?

  val refreshButton = document.querySelector(".refresh").asInstanceOf[HTMLButtonElement]
  val refreshClickStream = EventStream.merge(
    DomEventStream[dom.MouseEvent](refreshButton, "click").mapToUnit,
    EventStream.fromValue(())
  )

  val closeButton = document.querySelector(".close1").asInstanceOf[HTMLButtonElement]
  val closeClickStream = EventStream.merge(
    DomEventStream[dom.MouseEvent](closeButton, "click").mapToUnit,
    EventStream.fromValue(())
  )

  val requestUrlStream = refreshClickStream.map(_ => githubUrl)

  val responseStream = requestUrlStream.flatMap {
    url =>
      FetchStream
        .get(url)
        .map(_.length)
  }

  val combinedStream = closeClickStream.combineWithFn(responseStream) {
    (x, y) => s"combined result x=${x}, y=${y}"
  }

  combinedStream.addObserver(Observer{
    s => println(s)
  })

this code show combined result x=undefined, y=29925 two time.