Updated dependencies

Updated version to 0.0.10

Updated ScalaTest to 3.2.2
Updated scala version to 2.13.4
Updated scala.js version to 1.1.0
Updated to outwatch version to commit 676f94a
Added dependency to outwatch-utils
Updated scala.js bundler to 0.20.0
Updated sbt-microsites to 1.1.2
Removed obsolete scalac arguments

Updated Router class itself
- Now generic over effect type instead of being hardcoded to
cats.effect.IO
- Uses colibri.Observable instead of monix.Observable
- Added history events listener to update dom on back/forward
 button press
This commit is contained in:
Rohan Sircar 2020-12-08 12:41:02 +05:30 committed by Rohan Sircar
parent 7c235c7706
commit 2c40f7b73f
11 changed files with 105 additions and 84 deletions

3
.gitignore vendored
View File

@ -142,3 +142,6 @@ GitHub.sublime-settings
/.idea/ /.idea/
/outwatch-router/yarn.lock /outwatch-router/yarn.lock
/.bsp
metals.sbt

View File

@ -1,3 +1,4 @@
version = "2.7.4"
style = default style = default
maxColumn = 100 maxColumn = 100

View File

@ -2,14 +2,9 @@ import xerial.sbt.Sonatype._
cancelable in Global := true cancelable in Global := true
val compilerPlugins = Seq(
addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.10.2"),
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full)
)
val versions = new { val versions = new {
val scalatest = "3.1.0-SNAP11" val scalatest = "3.2.2"
val outwatch = "676f94a" val outwatch = "61deece8"
} }
val commonSettings = Seq( val commonSettings = Seq(
@ -18,8 +13,8 @@ val commonSettings = Seq(
scalaVersion := Version.scalaVersion, scalaVersion := Version.scalaVersion,
scalacOptions ++= options.scalac, scalacOptions ++= options.scalac,
scalacOptions in (Compile, console) := options.scalacConsole, scalacOptions in (Compile, console) := options.scalacConsole,
updateOptions := updateOptions.value.withLatestSnapshots(false) updateOptions := updateOptions.value.withLatestSnapshots(false),
) ++ compilerPlugins )
lazy val publishSettings = Seq( lazy val publishSettings = Seq(
useGpg := true, useGpg := true,
@ -29,15 +24,14 @@ lazy val publishSettings = Seq(
homepage := Some(url("https://github.com/clovellytech/outwatch-router")), homepage := Some(url("https://github.com/clovellytech/outwatch-router")),
pomIncludeRepository := Function.const(false), pomIncludeRepository := Function.const(false),
sonatypeProfileName := "com.clovellytech", sonatypeProfileName := "com.clovellytech",
// License of your choice // License of your choice
licenses := Seq("MIT" -> url("http://opensource.org/licenses/MIT")), licenses := Seq("MIT" -> url("http://opensource.org/licenses/MIT")),
// Where is the source code hosted // Where is the source code hosted
sonatypeProjectHosting := Some(GitHubHosting("clovellytech", "outwatch-router", "pattersonzak@gmail.com")) sonatypeProjectHosting := Some(
GitHubHosting("clovellytech", "outwatch-router", "pattersonzak@gmail.com"),
),
) )
lazy val docs = project lazy val docs = project
.in(file("./router-docs")) .in(file("./router-docs"))
.settings(commonSettings) .settings(commonSettings)
@ -55,12 +49,12 @@ lazy val docs = project
micrositeCompilingDocsTool := WithMdoc, micrositeCompilingDocsTool := WithMdoc,
micrositeGithubOwner := "clovellytech", micrositeGithubOwner := "clovellytech",
micrositeGithubRepo := "outwatch-router", micrositeGithubRepo := "outwatch-router",
scalacOptions := options.scalacConsole scalacOptions := options.scalacConsole,
) )
.settings( .settings(
mdocVariables := Map( mdocVariables := Map(
"VERSION" -> version.value "VERSION" -> version.value,
) ),
) )
.dependsOn(router) .dependsOn(router)
@ -81,21 +75,29 @@ lazy val router = project
webpackConfigFile in fastOptJS := Some(baseDirectory.value / "webpack.config.dev.js"), webpackConfigFile in fastOptJS := Some(baseDirectory.value / "webpack.config.dev.js"),
// https://scalacenter.github.io/scalajs-bundler/cookbook.html#performance // https://scalacenter.github.io/scalajs-bundler/cookbook.html#performance
webpackBundlingMode in fastOptJS := BundlingMode.LibraryOnly(), webpackBundlingMode in fastOptJS := BundlingMode.LibraryOnly(),
resolvers += "jitpack" at "https://jitpack.io", resolvers += "jitpack".at("https://jitpack.io"),
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"io.github.outwatch" % "outwatch" % versions.outwatch, "com.github.outwatch.outwatch" %%% "outwatch" % versions.outwatch,
"com.github.outwatch.outwatch" %%% "outwatch-util" % versions.outwatch,
"org.scalatest" %%% "scalatest" % versions.scalatest % Test, "org.scalatest" %%% "scalatest" % versions.scalatest % Test,
), ),
copyFastOptJS := { copyFastOptJS := {
val inDir = (crossTarget in (Compile, fastOptJS)).value val inDir = (crossTarget in (Compile, fastOptJS)).value
val outDir = (crossTarget in (Compile, fastOptJS)).value / "dev" val outDir = (crossTarget in (Compile, fastOptJS)).value / "dev"
val files = Seq("outwatch-router-fastopt-loader.js", "outwatch-router-frontend-fastopt.js", "outwatch-router-frontend-fastopt.js.map") map { p => (inDir / p, outDir / p) } val files = Seq(
"outwatch-router-fastopt-loader.js",
"outwatch-router-frontend-fastopt.js",
"outwatch-router-frontend-fastopt.js.map",
).map(p => (inDir / p, outDir / p))
IO.copy(files, overwrite = true, preserveLastModified = true, preserveExecutable = true) IO.copy(files, overwrite = true, preserveLastModified = true, preserveExecutable = true)
}, },
// hot reloading configuration: // hot reloading configuration:
// https://github.com/scalacenter/scalajs-bundler/issues/180 // https://github.com/scalacenter/scalajs-bundler/issues/180
addCommandAlias("dev", "; compile; fastOptJS::startWebpackDevServer; devwatch; fastOptJS::stopWebpackDevServer"), addCommandAlias(
addCommandAlias("devwatch", "~; fastOptJS; copyFastOptJS") "dev",
"; compile; fastOptJS::startWebpackDevServer; devwatch; fastOptJS::stopWebpackDevServer",
),
addCommandAlias("devwatch", "~; fastOptJS; copyFastOptJS"),
) )
.settings(publishSettings) .settings(publishSettings)
@ -108,5 +110,3 @@ lazy val root = project
) )
.dependsOn(router) .dependsOn(router)
.aggregate(router) .aggregate(router)

View File

@ -1,60 +1,85 @@
package outwatch.router package outwatch.router
import cats.effect.IO import cats.effect.Sync
import monix.execution.Scheduler import colibri.Observable
import org.scalajs.dom.window import org.scalajs.dom.window
import outwatch.dom._ import outwatch._
import monix.reactive.Observable import outwatch.dsl._
import outwatch.util.Reducer
import outwatch.util.Store import outwatch.util.Store
sealed trait Action sealed trait Action
final case class Replace(path: Path) extends Action final case class Replace(path: Path) extends Action
final case class HistoryEvent(path: Path) extends Action
final case class RouterState[P](page: P) final case class RouterState[P](page: P)
/** /** An AppRouter handles parsing of URLs and mapping to pages of the given type.
* An AppRouter handles parsing of URLs and mapping to pages of the given type.
* @param siteRoot - The prefix part of a pathname, or the subpath at which your site is applied. * @param siteRoot - The prefix part of a pathname, or the subpath at which your site is applied.
* Usually this is just Root, but your site might need a prefix as in /my_site/[parsed pathname] * Usually this is just Root, but your site might need a prefix as in /my_site/[parsed pathname]
* @param parent - The parent path at which this router is mounted. You can have routers contained in subroots of your site. * @param parent - The parent path at which this router is mounted. You can have routers contained in subroots of your site.
* @param f - a mapping function from a Path to a page P. * @param f - a mapping function from a Path to a page P.
* @tparam F - the effect type
* @tparam P - Your page type, such as a sealed trait root type. * @tparam P - Your page type, such as a sealed trait root type.
*/ */
class AppRouter[P](siteRoot: Path, parent: Path, f: Path => P) { class AppRouter[F[_]: Sync, P](siteRoot: Path, parent: Path, f: Path => P) {
// Sync from the required page to the window.location // Sync from the required page to the window.location
def routerReducer(state: RouterState[P], action: Action): RouterState[P] = action match { def routerReducer(state: RouterState[P], action: Action): RouterState[P] =
action match {
case Replace(path) => case Replace(path) =>
window.history.pushState("", "", Path(siteRoot, Path(parent, path)).toUrlString) window.history.pushState("", "", Path(siteRoot, Path(parent, path)).toUrlString)
state.copy(page = f(path)) state.copy(page = f(path))
case HistoryEvent(path) =>
state.copy(page = f(path))
case _ => state case _ => state
} }
def store(implicit S : Scheduler): IO[RouterStore[P]] = { def store: F[RouterStore[P]] = {
val startingPath = Path(window.location.pathname) val startingPath = Path(window.location.pathname)
Store.create[Action, RouterState[P]]( Store.create[F, Action, RouterState[P]](
Replace(startingPath), Replace(startingPath),
RouterState(f(startingPath)), RouterState(f(startingPath)),
Store.Reducer.justState(routerReducer _) Reducer(routerReducer _),
) )
} }
def link(linkHref: String)(attrs: VDomModifier*)(implicit store: RouterStore[P]): BasicVNode =
a(href := linkHref)(
onClick.preventDefault.useLazy(Replace(Path(linkHref))) --> store.sink,
attrs,
)
def render(resolver: RouterResolve[P])(implicit store: RouterStore[P]): Observable[VDomModifier] =
store.map { case (_, RouterState(p)) => resolver(p) }
def watch()(implicit store: RouterStore[P]) =
emitter(outwatch.dsl.events.window.onPopState)
.useLazy(HistoryEvent(Path(org.scalajs.dom.window.location.pathname))) --> store.sink
} }
object AppRouter { object AppRouter {
def render[P](resolver: RouterResolve[P])(implicit store: RouterStore[P]): Observable[VDomModifier] = def render[P](resolver: RouterResolve[P])(implicit
store.map{ case (_, RouterState(p)) => resolver(p) } store: RouterStore[P],
): Observable[VDomModifier] = store.map { case (_, RouterState(p)) => resolver(p) }
def create[P](notFound: P)(f: PartialFunction[Path, P]): AppRouter[P] = def watch[P]()(implicit store: RouterStore[P]) = emitter(
create[P](Root, notFound)(f) outwatch.dsl.events.window.onPopState,
).useLazy(HistoryEvent(Path(org.scalajs.dom.window.location.pathname))) --> store.sink
def create[P](parent: Path, notFound: P)(f: PartialFunction[Path, P]): AppRouter[P] = def create[F[_]: Sync, P](notFound: P)(f: PartialFunction[Path, P]): AppRouter[F, P] =
new AppRouter[P](Root, parent, f.lift.andThen(_.getOrElse(notFound))) create[F, P](Root, notFound)(f)
def createParseSiteRoot[P](notFound: P)(f: PartialFunction[Path, P]): AppRouter[P] = def create[F[_]: Sync, P](parent: Path, notFound: P)(
createParseSiteRoot[P](Root, notFound)(f) f: PartialFunction[Path, P],
): AppRouter[F, P] = new AppRouter[F, P](Root, parent, f.lift.andThen(_.getOrElse(notFound)))
/** def createParseSiteRoot[F[_]: Sync, P](notFound: P)(
* Automatically determine what siteroot we're using, based on the current URL and expected parent. f: PartialFunction[Path, P],
): AppRouter[F, P] = createParseSiteRoot[F, P](Root, notFound)(f)
/** Automatically determine what siteroot we're using, based on the current URL and expected parent.
* For example, your site could be deployed at /example/directory/, your router root path could be /names, * For example, your site could be deployed at /example/directory/, your router root path could be /names,
* and the current url could be /example/directory/names/alice. So given the call: * and the current url could be /example/directory/names/alice. So given the call:
* createParseSubRoot[Page](Path("/names"), NotFound)(f), the router will work out that the window location * createParseSubRoot[Page](Path("/names"), NotFound)(f), the router will work out that the window location
@ -64,7 +89,9 @@ object AppRouter{
* @param f - a router function from Path to instances of your page type * @param f - a router function from Path to instances of your page type
* @tparam P - your page type * @tparam P - your page type
*/ */
def createParseSiteRoot[P](parent: Path, notFound: P)(f: PartialFunction[Path, P]): AppRouter[P] = { def createParseSiteRoot[F[_]: Sync, P](parent: Path, notFound: P)(
f: PartialFunction[Path, P],
): AppRouter[F, P] = {
val initUrl = window.location.pathname val initUrl = window.location.pathname
// url is of form /sra/srb/src/pa/pb/pc... // url is of form /sra/srb/src/pa/pb/pc...
// so just drop the parent part from the right of the url if it exists. // so just drop the parent part from the right of the url if it exists.
@ -75,6 +102,6 @@ object AppRouter{
} }
val routerFun: Path => P = f.lift.andThen(_.getOrElse(notFound)) val routerFun: Path => P = f.lift.andThen(_.getOrElse(notFound))
new AppRouter[P](siteRoot, parent, routerFun) new AppRouter[F, P](siteRoot, parent, routerFun)
} }
} }

View File

@ -1,13 +1,13 @@
package outwatch.router package outwatch.router
package dsl package dsl
import outwatch.dom.VDomModifier import outwatch._
import outwatch.dom.{dsl => O, _} import outwatch.dsl._
object C { object C {
def a[P](linkHref: String)(attrs: VDomModifier*)(implicit store: RouterStore[P]): BasicVNode = def link[P](linkHref: String)(attrs: VDomModifier*)(implicit store: RouterStore[P]): BasicVNode =
O.a( a(href := linkHref)(
O.href := linkHref, onClick.preventDefault.useLazy(Replace(Path(linkHref))) --> store.sink,
O.onClick.preventDefault.mapTo(Replace(Path(linkHref))) --> store attrs,
)(attrs) )
} }

View File

@ -1,9 +1,10 @@
package outwatch package outwatch
import outwatch.dom.VDomModifier import outwatch._
import colibri.ProSubject
package object router { package object router {
type RouterStore[P] = ProHandler[Action, (Action, RouterState[P])] type RouterStore[P] = ProSubject[Action, (Action, RouterState[P])]
type RouterResolve[P] = PartialFunction[P, VDomModifier] type RouterResolve[P] = PartialFunction[P, VDomModifier]
} }

View File

@ -1,8 +1,8 @@
package outwatch package outwatch
package router package router
import org.scalatest._
import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class PathTestSpec extends AnyFlatSpec with Matchers { class PathTestSpec extends AnyFlatSpec with Matchers {
class PathTest(url: String, path: Path) { class PathTest(url: String, path: Path) {
@ -12,4 +12,3 @@ class PathTestSpec extends AnyFlatSpec with Matchers {
"Root url" should "be /" in new PathTest("/", Root) "Root url" should "be /" in new PathTest("/", Root)
"A 1 part path" should "be correct" in new PathTest("/search", Root / "search") "A 1 part path" should "be correct" in new PathTest("/search", Root / "search")
} }

View File

@ -14,16 +14,13 @@ object options {
"-unchecked", // Enable additional warnings where generated code depends on assumptions. "-unchecked", // Enable additional warnings where generated code depends on assumptions.
"-Xcheckinit", // Wrap field accessors to throw an exception on uninitialized access. "-Xcheckinit", // Wrap field accessors to throw an exception on uninitialized access.
"-Xfatal-warnings", // Fail the compilation if there are any warnings. "-Xfatal-warnings", // Fail the compilation if there are any warnings.
"-Xfuture", // Turn on future language features.
"-Xlint:adapted-args", // Warn if an argument list is modified to match the receiver. "-Xlint:adapted-args", // Warn if an argument list is modified to match the receiver.
"-Xlint:by-name-right-associative", // By-name parameter of right associative operator.
"-Xlint:constant", // Evaluation of a constant arithmetic expression results in an error. "-Xlint:constant", // Evaluation of a constant arithmetic expression results in an error.
"-Xlint:delayedinit-select", // Selecting member of DelayedInit. "-Xlint:delayedinit-select", // Selecting member of DelayedInit.
"-Xlint:doc-detached", // A Scaladoc comment appears to be detached from its element. "-Xlint:doc-detached", // A Scaladoc comment appears to be detached from its element.
"-Xlint:inaccessible", // Warn about inaccessible types in method signatures. "-Xlint:inaccessible", // Warn about inaccessible types in method signatures.
"-Xlint:infer-any", // Warn when a type argument is inferred to be `Any`. "-Xlint:infer-any", // Warn when a type argument is inferred to be `Any`.
"-Xlint:missing-interpolator", // A string literal appears to be missing an interpolator id. "-Xlint:missing-interpolator", // A string literal appears to be missing an interpolator id.
"-Xlint:nullary-override", // Warn when non-nullary `def f()' overrides nullary `def f'.
"-Xlint:nullary-unit", // Warn when nullary methods return Unit. "-Xlint:nullary-unit", // Warn when nullary methods return Unit.
"-Xlint:option-implicit", // Option.apply used implicit view. "-Xlint:option-implicit", // Option.apply used implicit view.
"-Xlint:package-object-classes", // Class or object defined in package object. "-Xlint:package-object-classes", // Class or object defined in package object.
@ -31,15 +28,8 @@ object options {
"-Xlint:private-shadow", // A private field (or class parameter) shadows a superclass field. "-Xlint:private-shadow", // A private field (or class parameter) shadows a superclass field.
"-Xlint:stars-align", // Pattern sequence wildcard must align with sequence component. "-Xlint:stars-align", // Pattern sequence wildcard must align with sequence component.
"-Xlint:type-parameter-shadow", // A local type parameter shadows a type already in scope. "-Xlint:type-parameter-shadow", // A local type parameter shadows a type already in scope.
"-Xlint:unsound-match", // Pattern match may not be typesafe.
"-Yno-adapted-args", // Do not adapt an argument list (either by inserting () or creating a tuple) to match the receiver.
"-Ypartial-unification", // Enable partial unification in type constructor inference
"-Ywarn-dead-code", // Warn when dead code is identified. "-Ywarn-dead-code", // Warn when dead code is identified.
"-Ywarn-extra-implicit", // Warn when more than one implicit parameter section is defined. "-Ywarn-extra-implicit", // Warn when more than one implicit parameter section is defined.
"-Ywarn-inaccessible", // Warn about inaccessible types in method signatures.
"-Ywarn-infer-any", // Warn when a type argument is inferred to be `Any`.
"-Ywarn-nullary-unit", // Warn when nullary methods return Unit.
"-Ywarn-nullary-override", // Warn when non-nullary `def f()' overrides nullary `def f'.
"-Ywarn-numeric-widen", // Warn when numerics are widened. "-Ywarn-numeric-widen", // Warn when numerics are widened.
"-Ywarn-unused:implicits", // Warn if an implicit parameter is unused. "-Ywarn-unused:implicits", // Warn if an implicit parameter is unused.
"-Ywarn-unused:imports", // Warn if an import selector is not referenced. "-Ywarn-unused:imports", // Warn if an import selector is not referenced.

View File

@ -1,4 +1,4 @@
object Version { object Version {
val version = "0.0.9" val version = "0.0.10"
val scalaVersion = "2.12.8" val scalaVersion = "2.13.4"
} }

View File

@ -1 +1 @@
sbt.version=1.3.0-RC2 sbt.version=1.4.3

View File

@ -1,6 +1,6 @@
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.1.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.15.0-0.6") addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.20.0")
addSbtPlugin("org.scalameta" % "sbt-mdoc" % "1.2.8") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "1.2.8")
addSbtPlugin("com.47deg" % "sbt-microsites" % "0.8.0") addSbtPlugin("com.47deg" % "sbt-microsites" % "1.1.2")
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3") addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3")
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.2") addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.2")