You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

80 lines
3.4 KiB

  1. package outwatch.router
  2. import cats.effect.IO
  3. import monix.execution.Scheduler
  4. import org.scalajs.dom.window
  5. import outwatch.dom._
  6. import monix.reactive.Observable
  7. import outwatch.util.Store
  8. sealed trait Action
  9. final case class Replace(path: Path) extends Action
  10. final case class RouterState[P](page: P)
  11. /**
  12. * An AppRouter handles parsing of URLs and mapping to pages of the given type.
  13. * @param siteRoot - The prefix part of a pathname, or the subpath at which your site is applied.
  14. * Usually this is just Root, but your site might need a prefix as in /my_site/[parsed pathname]
  15. * @param parent - The parent path at which this router is mounted. You can have routers contained in subroots of your site.
  16. * @param f - a mapping function from a Path to a page P.
  17. * @tparam P - Your page type, such as a sealed trait root type.
  18. */
  19. class AppRouter[P](siteRoot: Path, parent: Path, f: Path => P) {
  20. // Sync from the required page to the window.location
  21. def routerReducer(state: RouterState[P], action: Action): RouterState[P] = action match {
  22. case Replace(path) =>
  23. window.history.pushState("", "", Path(siteRoot, Path(parent, path)).toUrlString)
  24. state.copy(page = f(path))
  25. case _ => state
  26. }
  27. def store(implicit S : Scheduler): IO[RouterStore[P]] = {
  28. val startingPath = Path(window.location.pathname)
  29. Store.create[Action, RouterState[P]](
  30. Replace(startingPath),
  31. RouterState(f(startingPath)),
  32. Store.Reducer.justState(routerReducer _)
  33. )
  34. }
  35. }
  36. object AppRouter{
  37. def render[P](resolver: RouterResolve[P])(implicit store: RouterStore[P]): Observable[VDomModifier] =
  38. store.map{ case (_, RouterState(p)) => resolver(p) }
  39. def create[P](notFound: P)(f: PartialFunction[Path, P]): AppRouter[P] =
  40. create[P](Root, notFound)(f)
  41. def create[P](parent: Path, notFound: P)(f: PartialFunction[Path, P]): AppRouter[P] =
  42. new AppRouter[P](Root, parent, f.lift.andThen(_.getOrElse(notFound)))
  43. def createParseSiteRoot[P](notFound: P)(f: PartialFunction[Path, P]): AppRouter[P] =
  44. createParseSiteRoot[P](Root, notFound)(f)
  45. /**
  46. * Automatically determine what siteroot we're using, based on the current URL and expected parent.
  47. * For example, your site could be deployed at /example/directory/, your router root path could be /names,
  48. * and the current url could be /example/directory/names/alice. So given the call:
  49. * createParseSubRoot[Page](Path("/names"), NotFound)(f), the router will work out that the window location
  50. * prefix must be /example/directory/names, and will handle actions such as Replace(Path("/names/bob"))
  51. * @param parent the parent for this router, another path perhaps managed by another router
  52. * @param notFound - the default case page assignment
  53. * @param f - a router function from Path to instances of your page type
  54. * @tparam P - your page type
  55. */
  56. def createParseSiteRoot[P](parent: Path, notFound: P)(f: PartialFunction[Path, P]): AppRouter[P] = {
  57. val initUrl = window.location.pathname
  58. // url is of form /sra/srb/src/pa/pb/pc...
  59. // so just drop the parent part from the right of the url if it exists.
  60. val siteRoot = initUrl.lastIndexOf(parent.toString) match {
  61. case x if x < 1 => Root
  62. case x => Path(initUrl.substring(0, x))
  63. }
  64. val routerFun: Path => P = f.lift.andThen(_.getOrElse(notFound))
  65. new AppRouter[P](siteRoot, parent, routerFun)
  66. }
  67. }