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:
parent
7c235c7706
commit
2c40f7b73f
3
.gitignore
vendored
3
.gitignore
vendored
@ -142,3 +142,6 @@ GitHub.sublime-settings
|
||||
/.idea/
|
||||
|
||||
/outwatch-router/yarn.lock
|
||||
|
||||
/.bsp
|
||||
metals.sbt
|
@ -1,3 +1,4 @@
|
||||
version = "2.7.4"
|
||||
style = default
|
||||
|
||||
maxColumn = 100
|
||||
|
46
build.sbt
46
build.sbt
@ -2,14 +2,9 @@ import xerial.sbt.Sonatype._
|
||||
|
||||
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 scalatest = "3.1.0-SNAP11"
|
||||
val outwatch = "676f94a"
|
||||
val scalatest = "3.2.2"
|
||||
val outwatch = "61deece8"
|
||||
}
|
||||
|
||||
val commonSettings = Seq(
|
||||
@ -18,8 +13,8 @@ val commonSettings = Seq(
|
||||
scalaVersion := Version.scalaVersion,
|
||||
scalacOptions ++= options.scalac,
|
||||
scalacOptions in (Compile, console) := options.scalacConsole,
|
||||
updateOptions := updateOptions.value.withLatestSnapshots(false)
|
||||
) ++ compilerPlugins
|
||||
updateOptions := updateOptions.value.withLatestSnapshots(false),
|
||||
)
|
||||
|
||||
lazy val publishSettings = Seq(
|
||||
useGpg := true,
|
||||
@ -29,15 +24,14 @@ lazy val publishSettings = Seq(
|
||||
homepage := Some(url("https://github.com/clovellytech/outwatch-router")),
|
||||
pomIncludeRepository := Function.const(false),
|
||||
sonatypeProfileName := "com.clovellytech",
|
||||
|
||||
// License of your choice
|
||||
licenses := Seq("MIT" -> url("http://opensource.org/licenses/MIT")),
|
||||
|
||||
// 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
|
||||
.in(file("./router-docs"))
|
||||
.settings(commonSettings)
|
||||
@ -55,12 +49,12 @@ lazy val docs = project
|
||||
micrositeCompilingDocsTool := WithMdoc,
|
||||
micrositeGithubOwner := "clovellytech",
|
||||
micrositeGithubRepo := "outwatch-router",
|
||||
scalacOptions := options.scalacConsole
|
||||
scalacOptions := options.scalacConsole,
|
||||
)
|
||||
.settings(
|
||||
mdocVariables := Map(
|
||||
"VERSION" -> version.value
|
||||
)
|
||||
"VERSION" -> version.value,
|
||||
),
|
||||
)
|
||||
.dependsOn(router)
|
||||
|
||||
@ -81,21 +75,29 @@ lazy val router = project
|
||||
webpackConfigFile in fastOptJS := Some(baseDirectory.value / "webpack.config.dev.js"),
|
||||
// https://scalacenter.github.io/scalajs-bundler/cookbook.html#performance
|
||||
webpackBundlingMode in fastOptJS := BundlingMode.LibraryOnly(),
|
||||
resolvers += "jitpack" at "https://jitpack.io",
|
||||
resolvers += "jitpack".at("https://jitpack.io"),
|
||||
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,
|
||||
),
|
||||
copyFastOptJS := {
|
||||
val inDir = (crossTarget in (Compile, fastOptJS)).value
|
||||
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)
|
||||
},
|
||||
// hot reloading configuration:
|
||||
// https://github.com/scalacenter/scalajs-bundler/issues/180
|
||||
addCommandAlias("dev", "; compile; fastOptJS::startWebpackDevServer; devwatch; fastOptJS::stopWebpackDevServer"),
|
||||
addCommandAlias("devwatch", "~; fastOptJS; copyFastOptJS")
|
||||
addCommandAlias(
|
||||
"dev",
|
||||
"; compile; fastOptJS::startWebpackDevServer; devwatch; fastOptJS::stopWebpackDevServer",
|
||||
),
|
||||
addCommandAlias("devwatch", "~; fastOptJS; copyFastOptJS"),
|
||||
)
|
||||
.settings(publishSettings)
|
||||
|
||||
@ -108,5 +110,3 @@ lazy val root = project
|
||||
)
|
||||
.dependsOn(router)
|
||||
.aggregate(router)
|
||||
|
||||
|
||||
|
@ -1,60 +1,85 @@
|
||||
package outwatch.router
|
||||
|
||||
import cats.effect.IO
|
||||
import monix.execution.Scheduler
|
||||
import cats.effect.Sync
|
||||
import colibri.Observable
|
||||
import org.scalajs.dom.window
|
||||
import outwatch.dom._
|
||||
import monix.reactive.Observable
|
||||
import outwatch._
|
||||
import outwatch.dsl._
|
||||
import outwatch.util.Reducer
|
||||
import outwatch.util.Store
|
||||
|
||||
sealed trait Action
|
||||
final case class Replace(path: Path) extends Action
|
||||
final case class HistoryEvent(path: Path) extends Action
|
||||
|
||||
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.
|
||||
* 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 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.
|
||||
*/
|
||||
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
|
||||
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) =>
|
||||
window.history.pushState("", "", Path(siteRoot, Path(parent, path)).toUrlString)
|
||||
state.copy(page = f(path))
|
||||
case HistoryEvent(path) =>
|
||||
state.copy(page = f(path))
|
||||
case _ => state
|
||||
}
|
||||
|
||||
def store(implicit S : Scheduler): IO[RouterStore[P]] = {
|
||||
def store: F[RouterStore[P]] = {
|
||||
val startingPath = Path(window.location.pathname)
|
||||
|
||||
Store.create[Action, RouterState[P]](
|
||||
Store.create[F, Action, RouterState[P]](
|
||||
Replace(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{
|
||||
def render[P](resolver: RouterResolve[P])(implicit store: RouterStore[P]): Observable[VDomModifier] =
|
||||
store.map{ case (_, RouterState(p)) => resolver(p) }
|
||||
object AppRouter {
|
||||
def render[P](resolver: RouterResolve[P])(implicit
|
||||
store: RouterStore[P],
|
||||
): Observable[VDomModifier] = store.map { case (_, RouterState(p)) => resolver(p) }
|
||||
|
||||
def create[P](notFound: P)(f: PartialFunction[Path, P]): AppRouter[P] =
|
||||
create[P](Root, notFound)(f)
|
||||
def watch[P]()(implicit store: RouterStore[P]) = emitter(
|
||||
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] =
|
||||
new AppRouter[P](Root, parent, f.lift.andThen(_.getOrElse(notFound)))
|
||||
def create[F[_]: Sync, P](notFound: P)(f: PartialFunction[Path, P]): AppRouter[F, P] =
|
||||
create[F, P](Root, notFound)(f)
|
||||
|
||||
def createParseSiteRoot[P](notFound: P)(f: PartialFunction[Path, P]): AppRouter[P] =
|
||||
createParseSiteRoot[P](Root, notFound)(f)
|
||||
def create[F[_]: Sync, P](parent: Path, notFound: P)(
|
||||
f: PartialFunction[Path, P],
|
||||
): AppRouter[F, P] = new AppRouter[F, P](Root, parent, f.lift.andThen(_.getOrElse(notFound)))
|
||||
|
||||
/**
|
||||
* Automatically determine what siteroot we're using, based on the current URL and expected parent.
|
||||
def createParseSiteRoot[F[_]: Sync, P](notFound: P)(
|
||||
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,
|
||||
* 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
|
||||
@ -64,7 +89,9 @@ object AppRouter{
|
||||
* @param f - a router function from Path to instances of 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
|
||||
// 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.
|
||||
@ -75,6 +102,6 @@ object AppRouter{
|
||||
}
|
||||
|
||||
val routerFun: Path => P = f.lift.andThen(_.getOrElse(notFound))
|
||||
new AppRouter[P](siteRoot, parent, routerFun)
|
||||
new AppRouter[F, P](siteRoot, parent, routerFun)
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
package outwatch.router
|
||||
package dsl
|
||||
|
||||
import outwatch.dom.VDomModifier
|
||||
import outwatch.dom.{dsl => O, _}
|
||||
import outwatch._
|
||||
import outwatch.dsl._
|
||||
|
||||
object C {
|
||||
def a[P](linkHref: String)(attrs: VDomModifier*)(implicit store: RouterStore[P]): BasicVNode =
|
||||
O.a(
|
||||
O.href := linkHref,
|
||||
O.onClick.preventDefault.mapTo(Replace(Path(linkHref))) --> store
|
||||
)(attrs)
|
||||
def link[P](linkHref: String)(attrs: VDomModifier*)(implicit store: RouterStore[P]): BasicVNode =
|
||||
a(href := linkHref)(
|
||||
onClick.preventDefault.useLazy(Replace(Path(linkHref))) --> store.sink,
|
||||
attrs,
|
||||
)
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
package outwatch
|
||||
|
||||
import outwatch.dom.VDomModifier
|
||||
import outwatch._
|
||||
import colibri.ProSubject
|
||||
|
||||
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]
|
||||
}
|
||||
|
@ -1,15 +1,14 @@
|
||||
package outwatch
|
||||
package router
|
||||
|
||||
import org.scalatest._
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class PathTestSpec extends AnyFlatSpec with Matchers {
|
||||
class PathTest(url: String, path: Path){
|
||||
path.toUrlString should equal (url)
|
||||
class PathTest(url: String, path: Path) {
|
||||
path.toUrlString should equal(url)
|
||||
}
|
||||
|
||||
"Root url" should "be /" in new PathTest("/", Root)
|
||||
"A 1 part path" should "be correct" in new PathTest("/search", Root / "search")
|
||||
}
|
||||
|
||||
|
@ -14,16 +14,13 @@ object options {
|
||||
"-unchecked", // Enable additional warnings where generated code depends on assumptions.
|
||||
"-Xcheckinit", // Wrap field accessors to throw an exception on uninitialized access.
|
||||
"-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:by-name-right-associative", // By-name parameter of right associative operator.
|
||||
"-Xlint:constant", // Evaluation of a constant arithmetic expression results in an error.
|
||||
"-Xlint:delayedinit-select", // Selecting member of DelayedInit.
|
||||
"-Xlint:doc-detached", // A Scaladoc comment appears to be detached from its element.
|
||||
"-Xlint:inaccessible", // Warn about inaccessible types in method signatures.
|
||||
"-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:nullary-override", // Warn when non-nullary `def f()' overrides nullary `def f'.
|
||||
"-Xlint:nullary-unit", // Warn when nullary methods return Unit.
|
||||
"-Xlint:option-implicit", // Option.apply used implicit view.
|
||||
"-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: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: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-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-unused:implicits", // Warn if an implicit parameter is unused.
|
||||
"-Ywarn-unused:imports", // Warn if an import selector is not referenced.
|
||||
|
@ -1,4 +1,4 @@
|
||||
object Version{
|
||||
val version = "0.0.9"
|
||||
val scalaVersion = "2.12.8"
|
||||
object Version {
|
||||
val version = "0.0.10"
|
||||
val scalaVersion = "2.13.4"
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
sbt.version=1.3.0-RC2
|
||||
sbt.version=1.4.3
|
||||
|
@ -1,6 +1,6 @@
|
||||
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
|
||||
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.15.0-0.6")
|
||||
addSbtPlugin("org.scalameta" % "sbt-mdoc" % "1.2.8" )
|
||||
addSbtPlugin("com.47deg" % "sbt-microsites" % "0.8.0")
|
||||
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.1.0")
|
||||
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.20.0")
|
||||
addSbtPlugin("org.scalameta" % "sbt-mdoc" % "1.2.8")
|
||||
addSbtPlugin("com.47deg" % "sbt-microsites" % "1.1.2")
|
||||
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3")
|
||||
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.2")
|
||||
|
Loading…
Reference in New Issue
Block a user