Skip to content

Commit

Permalink
feat: add Router.navigate
Browse files Browse the repository at this point in the history
  • Loading branch information
TylorS committed May 20, 2024
1 parent b0a0429 commit 4c433dc
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 49 deletions.
4 changes: 1 addition & 3 deletions examples/realworld/src/services/CurrentUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,11 @@ export const getCurrentJwtToken = CurrentUser.pipe(
}))
)

const isAuthenticated_ = CurrentUser.pipe(
export const isAuthenticated = CurrentUser.pipe(
Fx.dropWhile(AsyncData.isLoadingOrRefreshing),
Fx.map(AsyncData.isSuccess)
)

export const isAuthenticated = Object.assign(isAuthenticated_, Fx.first(isAuthenticated_))

export const isAuthenticatedGuard = <R extends Route.Route.Any>(route: R) =>
RouteGuard.flatMap(RouteGuard.fromRoute(route), (params) =>
Effect.flatMap(
Expand Down
5 changes: 2 additions & 3 deletions examples/realworld/src/ui/components/EditArticle.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { ParseError } from "@effect/schema/ParseResult"
import { EventHandler, Fx, html, many, RefSubject } from "@typed/core"
import { EventHandler, Fx, html, many, RefSubject, Router } from "@typed/core"
import type { NavigationError } from "@typed/navigation"
import { navigate } from "@typed/navigation"
import {
type Article,
ArticleBody,
Expand Down Expand Up @@ -40,7 +39,7 @@ export function useEditArticle<R, R2>(
Effect.flatMap(onSubmit),
Effect.catchTags({
Unprocessable: (error) => RefSubject.set(errors, error.errors),
Unauthorized: () => navigate(Routes.login.interpolate({})),
Unauthorized: () => Router.navigate(Routes.login),
ParseError: (issue) => RefSubject.set(errors, [issue.message])
})
)
Expand Down
5 changes: 2 additions & 3 deletions examples/realworld/src/ui/pages/article.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { AsyncData, Fx, Link, RefArray, RefSubject } from "@typed/core"
import { AsyncData, Fx, Link, RefArray, RefSubject, Router } from "@typed/core"
import type { EventWithTarget } from "@typed/dom/EventTarget"
import { navigate } from "@typed/navigation"
import type { ArticleSlug, Comment } from "@typed/realworld/model"
import { CommentBody, Image } from "@typed/realworld/model"
import { Articles, Comments, CurrentUser, isAuthenticated, Profiles } from "@typed/realworld/services"
Expand Down Expand Up @@ -88,7 +87,7 @@ export const main = (params: RefSubject.RefSubject<Params>) =>
const deleteArticle = Effect.gen(function*() {
const slug = yield* article.slug
yield* Articles.delete({ slug })
yield* navigate(Routes.home.interpolate({}))
yield* Router.navigate(Routes.home)
})

const currentUserActions = Fx.if(currentUserIsAuthor, {
Expand Down
5 changes: 2 additions & 3 deletions examples/realworld/src/ui/pages/editor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Fx } from "@typed/core"
import { Fx, Router } from "@typed/core"
import { RefSubject } from "@typed/fx"
import { navigate } from "@typed/navigation"
import { ArticleBody, ArticleDescription, ArticleTitle } from "@typed/realworld/model"
import { Articles } from "@typed/realworld/services"
import * as Routes from "@typed/realworld/ui/common/routes"
Expand All @@ -22,7 +21,7 @@ export const main = Fx.gen(function*() {
(input) =>
Effect.gen(function*(_) {
const article = yield* _(Articles.create(input))
yield* navigate(Routes.article.interpolate({ slug: article.slug }))
yield* Router.navigate(Routes.article, { relative: false, params: article })
})
)
})
15 changes: 4 additions & 11 deletions examples/realworld/src/ui/pages/login.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { ArrayFormatter } from "@effect/schema"
import { AsyncData, EventHandler, Fx, html, Navigation, RefAsyncData, RefSubject } from "@typed/core"
import { AsyncData, EventHandler, Fx, html, Link, RefAsyncData, RefSubject, Router } from "@typed/core"
import type { EventWithTarget } from "@typed/dom/EventTarget"
import { RedirectError } from "@typed/navigation"
import { parseFormData } from "@typed/realworld/lib/Schema"
import { CurrentUser, isAuthenticated, Users } from "@typed/realworld/services"
import { CurrentUser, Users } from "@typed/realworld/services"
import { Unprocessable } from "@typed/realworld/services/errors"
import { LoginInput } from "@typed/realworld/services/Login"
import * as Routes from "@typed/realworld/ui/common/routes"
Expand All @@ -20,19 +19,13 @@ export const main = Fx.gen(function*(_) {
Effect.zipRight(loginUser(ev), RefSubject.set(hasSubmitted, true))
)

if (yield* _(isAuthenticated)) {
return yield* _(
new RedirectError({ path: Routes.home.interpolate({}) })
)
}

return html`<div class="auth-page">
<div class="container page">
<div class="row">
<div class="col-md-6 col-xs-12 offset-md-3">
<h1 class="text-xs-center">Sign in</h1>
<p class="text-xs-center">
<a href="/register">Need an account?</a>
${Link({ to: Routes.register.interpolate({}) }, `Need an account?`)}
</p>
${Fx.if(hasSubmitted, { onFalse: Fx.null, onTrue: CurrentUserErrors })}
Expand Down Expand Up @@ -78,7 +71,7 @@ function loginUser(ev: SubmitEvent) {
const updated = yield* _(RefAsyncData.runAsyncData(CurrentUser, Users.login(input)))

if (AsyncData.isSuccess(updated)) {
yield* _(Navigation.navigate(Routes.home.interpolate({}), { history: "replace" }))
yield* _(Router.navigate(Routes.home, { relative: false, params: {} }))
}
}),
"ParseError",
Expand Down
5 changes: 2 additions & 3 deletions examples/realworld/src/ui/pages/settings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ArrayFormatter } from "@effect/schema"
import { AsyncData, Fx, RefAsyncData, RefSubject } from "@typed/core"
import { navigate } from "@typed/navigation"
import { AsyncData, Fx, RefAsyncData, RefSubject, Router } from "@typed/core"
import { parseFormData } from "@typed/realworld/lib/Schema"
import { CurrentUser, Users } from "@typed/realworld/services"
import { Unprocessable } from "@typed/realworld/services/errors"
Expand All @@ -26,7 +25,7 @@ export const main = Fx.gen(function*(_) {

const logoutCurrentUser = RefSubject.set(CurrentUser, AsyncData.noData()).pipe(
Effect.zipRight(Users.logout()),
Effect.zipRight(navigate(Routes.login.interpolate({})))
Effect.zipRight(Router.navigate(Routes.login))
)

return html`<div class="settings-page">
Expand Down
9 changes: 4 additions & 5 deletions examples/realworld/src/ui/router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as Navigation from "@typed/navigation"
import { Fx, Navigation, Router } from "@typed/core"
import { isAuthenticated } from "@typed/realworld/services"
import * as Router from "@typed/router"
import { Effect } from "effect"
import { layout } from "./layout"
import * as pages from "./pages"
Expand All @@ -15,9 +14,9 @@ export const router = Router
.match(pages.profile.route, pages.profile.main)
.match(pages.home.route, pages.home.main)

const onNotFound = Effect.if(isAuthenticated, {
onFalse: () => new Navigation.RedirectError(pages.login.route),
onTrue: () => new Navigation.RedirectError(pages.home.route)
const onNotFound = Effect.if(Fx.first(isAuthenticated), {
onFalse: () => new Navigation.RedirectError({ path: pages.login.route.interpolate({}) }),
onTrue: () => new Navigation.RedirectError({ path: pages.home.route.interpolate({}) })
})

export const main = layout(Router.notFoundWith(router, onNotFound))
74 changes: 56 additions & 18 deletions packages/router/src/CurrentRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import * as Context from "@typed/context"
import * as Document from "@typed/dom/Document"
import type * as Fx from "@typed/fx"
import * as RefSubject from "@typed/fx/RefSubject"
import type { Destination } from "@typed/navigation"
import { CurrentEntry, CurrentPath, getCurrentPathFromUrl, Navigation } from "@typed/navigation"
import * as Navigation from "@typed/navigation"
import * as Route from "@typed/route"
import * as Effect from "effect/Effect"
import { dual, pipe } from "effect/Function"
Expand Down Expand Up @@ -56,14 +55,14 @@ export function layer<R extends Route.Route.Any>(
export const CurrentParams: RefSubject.Filtered<
Readonly<Record<string, string | ReadonlyArray<string>>>,
never,
Navigation | CurrentRoute
Navigation.Navigation | CurrentRoute
> = RefSubject
.filteredFromTag(
Navigation,
Navigation.Navigation,
(nav) =>
RefSubject.filterMapEffect(
nav.currentEntry,
(e) => CurrentRoute.with(({ route }) => route.match(getCurrentPathFromUrl(e.url)))
(e) => CurrentRoute.with(({ route }) => route.match(Navigation.getCurrentPathFromUrl(e.url)))
)
)

Expand Down Expand Up @@ -115,9 +114,18 @@ const makeHref_ = (
export function makeHref<const R extends Route.Route.Any>(
route: R,
...[params]: Route.Route.ParamsList<R>
): RefSubject.Filtered<string, never, Navigation | CurrentRoute> {
): RefSubject.Filtered<string, never, Navigation.Navigation | CurrentRoute>
export function makeHref<const R extends Route.Route.Any>(
route: R,
params: Route.Route.Params<R>
): RefSubject.Filtered<string, never, Navigation.Navigation | CurrentRoute>

export function makeHref<const R extends Route.Route.Any>(
route: R,
...[params]: Route.Route.ParamsList<R>
): RefSubject.Filtered<string, never, Navigation.Navigation | CurrentRoute> {
return RefSubject.filterMapEffect(
CurrentPath,
Navigation.CurrentPath,
(currentPath) =>
Effect.map(
CurrentRoute,
Expand Down Expand Up @@ -156,17 +164,17 @@ const isActive_ = (
export function isActive<R extends Route.Route.Any>(
route: R,
...[params]: Route.Route.ParamsList<R>
): RefSubject.Computed<boolean, never, Navigation | CurrentRoute>
): RefSubject.Computed<boolean, never, Navigation.Navigation | CurrentRoute>
export function isActive<R extends Route.Route.Any>(
route: R,
params: Route.Route.Params<R>
): RefSubject.Computed<boolean, never, Navigation | CurrentRoute>
): RefSubject.Computed<boolean, never, Navigation.Navigation | CurrentRoute>
export function isActive<R extends Route.Route.Any>(
route: R,
...[params]: Route.Route.ParamsList<R>
): RefSubject.Computed<boolean, never, Navigation | CurrentRoute> {
): RefSubject.Computed<boolean, never, Navigation.Navigation | CurrentRoute> {
return RefSubject.mapEffect(
CurrentPath,
Navigation.CurrentPath,
(currentPath) =>
CurrentRoute.with((currentRoute): boolean => isActive_(currentPath, currentRoute.route, route, params))
)
Expand All @@ -180,16 +188,16 @@ export function decode<R extends Route.Route.Any>(
): Fx.RefSubject.Filtered<
Route.Route.Type<R>,
Route.RouteDecodeError<R>,
Navigation | CurrentRoute | Route.Route.Context<R>
Navigation.Navigation | CurrentRoute | Route.Route.Context<R>
> {
return RefSubject.filteredFromTag(
Navigation,
Navigation.Navigation,
(nav) =>
RefSubject.filterMapEffect(
nav.currentEntry,
(e) =>
Effect.flatMap(CurrentRoute, ({ route: parent }) =>
Effect.optionFromOptional(Route.decode(parent.concat(route) as R, getCurrentPathFromUrl(e.url))))
Effect.optionFromOptional(Route.decode(parent.concat(route) as R, Navigation.getCurrentPathFromUrl(e.url))))
)
)
}
Expand Down Expand Up @@ -225,18 +233,48 @@ function getBasePathname(base: string): string {
export const server = (base: string = "/"): Layer.Layer<CurrentRoute> =>
CurrentRoute.layer({ route: Route.parse(base), parent: Option.none() })

const getSearchParams = (destination: Destination) => destination.url.searchParams
const getSearchParams = (destination: Navigation.Destination) => destination.url.searchParams

/**
* @since 1.0.0
*/
export const CurrentSearchParams: RefSubject.Computed<URLSearchParams, never, Navigation> = RefSubject
.map(CurrentEntry, getSearchParams)
export const CurrentSearchParams: RefSubject.Computed<URLSearchParams, never, Navigation.Navigation> = RefSubject
.map(Navigation.CurrentEntry, getSearchParams)

/**
* @since 1.0.0
*/
export const CurrentState = RefSubject.computedFromTag(
Navigation,
Navigation.Navigation,
(n) => RefSubject.map(n.currentEntry, (e) => e.state)
)

/**
* @since 1.0.0
*/
export type NavigateOptions<R extends Route.Route.Any> =
& Navigation.NavigateOptions
& ([keyof Route.Route.Params<R>] extends [never] ? { readonly params?: Route.Route.Params<R> | undefined }
: { readonly params: Route.Route.Params<R> })
& {
readonly relative?: boolean
}

/**
* @since 1.0.0
*/
export const navigate = <R extends Route.Route.Any>(
route: R,
options: NavigateOptions<R> = {} as NavigateOptions<R>
): Effect.Effect<
Option.Option<Navigation.Destination>,
Navigation.NavigationError,
Navigation.Navigation | CurrentRoute
> =>
Effect.optionFromOptional(Effect.gen(function*(_) {
const params = options.params ?? {} as Route.Route.Params<R>
const path = options.relative === false
? route.interpolate(params)
: yield* _(makeHref<R>(route, params))
return yield* _(Navigation.navigate(path))
}))

0 comments on commit 4c433dc

Please sign in to comment.