Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handler/Sink refactor, added Pipe trait #91

Merged
merged 12 commits into from
Nov 21, 2017
22 changes: 22 additions & 0 deletions src/main/scala/outwatch/Handler.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package outwatch

import cats.effect.IO
import outwatch.dom.Observable

object Handler {
private[outwatch] def apply[T](sink: Sink[T], source: Observable[T]): Handler[T] = Pipe(sink, source)

/**
* This function also allows you to create initial values for your newly created Handler.
* This is equivalent to calling `startWithMany` with the given values.
*
* @param seeds a sequence of initial values that the Handler will emit.
* @tparam T the type parameter of the elements
* @return the newly created Handler.
*/
def create[T](seeds: T*): IO[Handler[T]] = Pipe.create[T](seeds: _*)

def create[T]: IO[Handler[T]] = Pipe.create[T]

}

33 changes: 33 additions & 0 deletions src/main/scala/outwatch/Pipe.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package outwatch

import cats.effect.IO
import outwatch.Sink.{ObservableSink, SubjectSink}
import rxscalajs.Observable

object Pipe {
private[outwatch] def apply[I, O](sink: Sink[I], source: Observable[O]): Pipe[I, O] =
new ObservableSink[I, O](sink, source)

/**
* This function also allows you to create initial values for your newly created Pipe.
* This is equivalent to calling `startWithMany` with the given values.
*
* @param seeds a sequence of initial values that the Pipe will emit.
* @tparam T the type parameter of the elements
* @return the newly created Pipe.
*/
def create[T](seeds: T*): IO[Pipe[T, T]] = create[T].map { pipe =>
if (seeds.nonEmpty) {
pipe.transformSource(_.startWithMany(seeds: _*))
}
else {
pipe
}
}

def create[T]: IO[Pipe[T, T]] = IO {
val subjectSink = SubjectSink[T]()
Pipe(subjectSink, subjectSink)
}

}
53 changes: 17 additions & 36 deletions src/main/scala/outwatch/Sink.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package outwatch

import cats.effect.IO
import outwatch.dom.Handler
import rxscalajs.facade.SubjectFacade
import rxscalajs.subscription.Subscription
import rxscalajs.{Observable, Observer, Subject}


import scala.scalajs.js

sealed trait Sink[-T] extends Any {
Expand Down Expand Up @@ -47,12 +47,16 @@ sealed trait Sink[-T] extends Any {
}

object Sink {
private final case class SubjectSink[T]() extends Subject[T](new SubjectFacade) with Sink[T] {
override private[outwatch] def observer = this

// these classes need to be in this file, because Sink is sealed
private[outwatch] case class ObservableSink[-I, +O](
sink: Sink[I], source: Observable[O]
) extends Observable[O](source.inner) with Sink[I]{
override private[outwatch] def observer = sink.observer
}
private final case class ObservableSink[T]
(oldSink: Sink[T], stream: Observable[T]) extends Observable[T](stream.inner) with Sink[T] {
override private[outwatch] def observer = oldSink.observer

private[outwatch] final case class SubjectSink[T]() extends Subject[T](new SubjectFacade) with Sink[T] {
override private[outwatch] def observer = this
}

/**
Expand All @@ -78,27 +82,6 @@ object Sink {
sink
}

/**
* Creates a Handler that is both Observable and Sink.
* An Observable with Sink is an Observable that can also receive Events, i.e. it’s both a Source and a Sink of events.
* If you’re familiar with Rx, they’re very similar to Subjects.
* This function also allows you to create initial values for your newly created Handler.
* This is equivalent to calling `startWithMany` with the given values.
* @param seeds a sequence of initial values that the Handler will emit.
* @tparam T the type parameter of the elements
* @return the newly created Handler.
*/
def createHandler[T](seeds: T*): IO[Handler[T]] = IO {
val handler = new SubjectSink[T]

if (seeds.nonEmpty) {
ObservableSink[T](handler, handler.startWithMany(seeds: _*))
}
else {
handler
}
}


private def completionObservable[T](sink: Sink[T]): Option[Observable[Unit]] = {
sink match {
Expand All @@ -122,7 +105,7 @@ object Sink {
* @return the resulting sink, that will forward the values
*/
def redirect[T,R](sink: Sink[T])(project: Observable[R] => Observable[T]): Sink[R] = {
val forward = Sink.createHandler[R]().unsafeRunSync()
val forward = SubjectSink[R]()

completionObservable(sink)
.fold(project(forward))(completed => project(forward).takeUntil(completed))
Expand All @@ -144,8 +127,8 @@ object Sink {
* @return the two resulting sinks, that will forward the values
*/
def redirect2[T,U,R](sink: Sink[T])(project: (Observable[R], Observable[U]) => Observable[T]): (Sink[R], Sink[U]) = {
val r = Sink.createHandler[R]().unsafeRunSync()
val u = Sink.createHandler[U]().unsafeRunSync()
val r = SubjectSink[R]()
val u = SubjectSink[U]()

completionObservable(sink)
.fold(project(r, u))(completed => project(r, u).takeUntil(completed))
Expand All @@ -170,9 +153,9 @@ object Sink {
def redirect3[T,U,V,R](sink: Sink[T])
(project: (Observable[R], Observable[U], Observable[V]) => Observable[T])
:(Sink[R], Sink[U], Sink[V]) = {
val r = Sink.createHandler[R]().unsafeRunSync()
val u = Sink.createHandler[U]().unsafeRunSync()
val v = Sink.createHandler[V]().unsafeRunSync()
val r = SubjectSink[R]()
val u = SubjectSink[U]()
val v = SubjectSink[V]()

completionObservable(sink)
.fold(project(r, u, v))(completed => project(r, u, v).takeUntil(completed))
Expand All @@ -197,6 +180,4 @@ object Sink {

}

final case class ObserverSink[-T](observer: Observer[T]) extends AnyVal with Sink[T]


final case class ObserverSink[-T](observer: Observer[T]) extends AnyVal with Sink[T]
43 changes: 43 additions & 0 deletions src/main/scala/outwatch/dom/HandlerFactories.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package outwatch.dom

import cats.effect.IO
import org.scalajs.dom.{ClipboardEvent, DragEvent, KeyboardEvent, MouseEvent}
import outwatch.dom.helpers.InputEvent

/**
* Trait containing event handlers, so they can be mixed in to other objects if needed.
*/
trait HandlerFactories {

@deprecated("Use Handler.inputEvents instead", "0.11.0")
def createInputHandler() = Handler.create[InputEvent]
@deprecated("Use Handler.mouseEvents instead", "0.11.0")
def createMouseHandler() = Handler.create[MouseEvent]
@deprecated("Use Handler.keyboardEvents instead", "0.11.0")
def createKeyboardHandler() = Handler.create[KeyboardEvent]
@deprecated("Use Handler.dragEvents instead", "0.11.0")
def createDragHandler() = Handler.create[DragEvent]
@deprecated("Use Handler.clipboardEvents instead", "0.11.0")
def createClipboardHandler() = Handler.create[ClipboardEvent]

@deprecated("Use Handler.create[String] instead", "0.11.0")
def createStringHandler(defaultValues: String*) = Handler.create[String](defaultValues: _*)
@deprecated("Use Handler.create[Boolean] instead", "0.11.0")
def createBoolHandler(defaultValues: Boolean*) = Handler.create[Boolean](defaultValues: _*)
@deprecated("Use Handler.create[Double] instead", "0.11.0")
def createNumberHandler(defaultValues: Double*) = Handler.create[Double](defaultValues: _*)

@deprecated("Use Handler.create[T] instead", "0.11.0")
def createHandler[T](defaultValues: T*): IO[Pipe[T, T]] = Handler.create[T](defaultValues: _*)


implicit class HandlerCreateHelpers(handler: Handler.type) {
lazy val inputEvents = Handler.create[InputEvent]
lazy val mouseEvents = Handler.create[MouseEvent]
lazy val keyboardEvents = Handler.create[KeyboardEvent]
lazy val dragEvents = Handler.create[DragEvent]
lazy val clipboardEvents = Handler.create[ClipboardEvent]
}

}

28 changes: 0 additions & 28 deletions src/main/scala/outwatch/dom/Handlers.scala

This file was deleted.

2 changes: 1 addition & 1 deletion src/main/scala/outwatch/dom/VDomModifier.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cats.effect.IO
import org.scalajs.dom._
import outwatch.dom.helpers.DomUtils
import scala.scalajs.js.|
import rxscalajs.{Observable, Observer}
import rxscalajs.Observer
import snabbdom.{VNodeProxy, h}

import scala.scalajs.js
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/outwatch/dom/helpers/DomUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package outwatch.dom.helpers

import cats.effect.IO
import org.scalajs.dom._
import org.scalajs.dom.raw.HTMLInputElement
import org.scalajs.dom.html
import outwatch.dom._
import rxscalajs.Observable
import rxscalajs.subscription.Subscription
Expand Down Expand Up @@ -76,7 +76,7 @@ object DomUtils {

private val valueSyncHook: (VNodeProxy, VNodeProxy) => Unit = (_, node) => {
node.elm.foreach { elm =>
val input = elm.asInstanceOf[HTMLInputElement]
val input = elm.asInstanceOf[html.Input]
if (input.value != input.getAttribute("value")) {
input.value = input.getAttribute("value")
}
Expand Down
5 changes: 3 additions & 2 deletions src/main/scala/outwatch/dom/helpers/InputEvent.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package outwatch.dom.helpers

import org.scalajs.dom.raw.{Event, HTMLInputElement}
import org.scalajs.dom.Event
import org.scalajs.dom.html

import scala.scalajs.js.annotation.ScalaJSDefined

@ScalaJSDefined
class InputEvent() extends Event {
override def target = {
super.target.asInstanceOf[HTMLInputElement]
super.target.asInstanceOf[html.Input]
}
}
14 changes: 13 additions & 1 deletion src/main/scala/outwatch/dom/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,23 @@ import cats.effect.IO
import scala.language.implicitConversions


package object dom extends Attributes with Tags with Handlers {
package object dom extends Attributes with Tags with HandlerFactories {

type VNode = IO[VNode_]
type VDomModifier = IO[VDomModifier_]

type Observable[+A] = rxscalajs.Observable[A]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the direction of outwatch becoming frp-backend agnostic. 👍

val Observable = rxscalajs.Observable

type Sink[-A] = outwatch.Sink[A]
val Sink = outwatch.Sink

type Pipe[-I, +O] = outwatch.Pipe[I, O]
val Pipe = outwatch.Pipe

type Handler[T] = outwatch.Handler[T]
val Handler = outwatch.Handler

implicit def stringNode(string: String): VDomModifier = IO.pure(StringNode(string))

implicit def optionIsEmptyModifier(opt: Option[VDomModifier]): VDomModifier = opt getOrElse IO.pure(EmptyVDomModifier)
Expand Down
54 changes: 54 additions & 0 deletions src/main/scala/outwatch/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import rxscalajs.Observable

package object outwatch {
type Pipe[-I, +O] = Observable[O] with Sink[I]
type Handler[T] = Pipe[T, T]

implicit class PipeOps[-I, +O](val self: Pipe[I, O]) extends AnyVal {

def mapSink[I2](f: I2 => I): Pipe[I2, O] = Pipe(self.redirectMap(f), self)

def mapSource[O2](f: O => O2): Pipe[I, O2] = Pipe(self, self.map(f))

def mapPipe[I2, O2](f: I2 => I)(g: O => O2): Pipe[I2, O2] = Pipe(self.redirectMap(f), self.map(g))


def collectSink[I2](f: PartialFunction[I2, I]): Pipe[I2, O] = Pipe(self.redirect(_.collect(f)), self)

def collectSource[O2](f: PartialFunction[O, O2]): Pipe[I, O2] = Pipe(self, self.collect(f))

def collectPipe[I2, O2](f: PartialFunction[I2, I])(g: PartialFunction[O, O2]): Pipe[I2, O2] = Pipe(
self.redirect(_.collect(f)), self.collect(g)
)


def filterSource(f: O => Boolean): Pipe[I, O] = Pipe(self, self.filter(f))


def transformSink[I2](f: Observable[I2] => Observable[I]): Pipe[I2, O] = Pipe(self.redirect(f), self)

def transformSource[O2](f: Observable[O] => Observable[O2]): Pipe[I, O2] = Pipe(self, f(self))

def transformPipe[I2, O2](f: Observable[I2] => Observable[I])(g: Observable[O] => Observable[O2]): Pipe[I2, O2] =
Pipe(self.redirect(f), g(self))
}

// This is not in PipeOps, because I is contravariant
implicit class FilterSink[I, +O](val self: Pipe[I, O]) extends AnyVal {
def filterSink(f: I => Boolean): Pipe[I, O] = Pipe(self.redirect(_.filter(f)), self)
}

implicit class HandlerOps[T](val self: Handler[T]) extends AnyVal {

def imap[S](read: T => S)(write: S => T): Handler[S] = self.mapPipe(write)(read)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have the sink/source transformation order consistent between Pipe and Handler. Either transform the sink first everywhere, or the source first everywhere. Intuitively for me was to have the sink first, because something sent into the sink gets emitted by the observable afterwards.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Yes this should be consistent. My motivation for this order was that you can easily rewrite a handler.map(f) to handler.imap(f)(g) by just appending the second function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting. In that case, let's leave them as they are.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. Merge?


def lens[S](seed: T)(read: T => S)(write: (T, S) => T): Handler[S] = {
val redirected = self.redirect[S](_.withLatestFrom(self.startWith(seed)).map { case (a, b) => write(b, a) })
Handler(redirected, self.map(read))
}

def transformHandler[S](read: Observable[T] => Observable[S])(write: Observable[S] => Observable[T]): Handler[S] =
self.transformPipe(write)(read)
}

}
12 changes: 6 additions & 6 deletions src/main/scala/outwatch/util/Store.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package outwatch.util

import cats.effect.IO
import outwatch.Sink
import outwatch.{Pipe, Sink}
import outwatch.dom._
import outwatch.dom.helpers.STRef
import rxscalajs.Observable
Expand All @@ -12,7 +12,7 @@ import scala.language.implicitConversions

final case class Store[State, Action](initialState: State,
reducer: (State, Action) => (State, Option[IO[Action]]),
handler: Observable[Action] with Sink[Action]) {
handler: Pipe[Action, Action]) {
val sink: Sink[Action] = handler
val source: Observable[State] = handler
.scan(initialState)(fold)
Expand All @@ -35,19 +35,19 @@ final case class Store[State, Action](initialState: State,
}

object Store {
implicit def toSink[Action](store: Store[_, Action]): Sink[Action] = store.sink
implicit def toSource[State](store: Store[State, _]): Observable[State] = store.source
implicit def toPipe[State, Action](store: Store[State, Action]): Pipe[Action, State] =
Pipe(store.sink, store.source)

private val storeRef = STRef.empty

def renderWithStore[S, A](initialState: S, reducer: (S, A) => (S, Option[IO[A]]), selector: String, root: VNode): IO[Unit] = for {
handler <- createHandler[A]()
handler <- Handler.create[A]
store <- IO(Store(initialState, reducer, handler))
_ <- storeRef.asInstanceOf[STRef[Store[S, A]]].put(store)
_ <- OutWatch.render(selector, root)
} yield ()

def getStore[S, A]: IO[Store[S, A]] =
def getStore[S, A]: IO[Store[S, A]] =
storeRef.asInstanceOf[STRef[Store[S, A]]].getOrThrow(NoStoreException)

private object NoStoreException extends
Expand Down
Loading