@typed/fp
Version:
Data Structures and Resources for fp-ts
446 lines (395 loc) • 9.27 kB
text/typescript
/**
* @typed/fp/dom is a collection of abstractions for working with the DOM
* @since 0.13.2
*/
import { A } from 'ts-toolbelt'
import { ParseSelector } from 'typed-query-selector/parser'
import * as E from './Env'
import * as EO from './EnvOption'
import * as Fail from './Fail'
import { ArgsOf, pipe } from './function'
import * as KV from './KV'
import * as O from './Option'
import * as RS from './ReaderStream'
import { Resume } from './Resume'
import { SchedulerEnv } from './Scheduler'
import * as S from './Stream'
import { useReaderStream } from './Use'
/**
* @category Environment
* @since 0.13.2
*/
export type HistoryEnv = { readonly history: History }
/**
* @category DOM
* @since 0.13.2
*/
export const getHistory = E.asks((e: HistoryEnv) => e.history)
/**
* @category Environment
* @since 0.13.2
*/
export type LocationEnv = { readonly location: Location }
/**
* @category DOM
* @since 0.13.2
*/
export const getLocation = E.asks((e: LocationEnv) => e.location)
/**
* @category Environment
* @since 0.13.2
*/
export type RafEnv = {
readonly raf: Resume<number>
}
/**
* @category Effect
* @since 0.13.2
*/
export const raf: E.Env<RafEnv, number> = (e: RafEnv) => e.raf
/**
* @category Environment
* @since 0.13.2
*/
export type WhenIdleEnv = {
readonly whenIdle: Resume<IdleDeadline>
}
/**
* @category Effect
* @since 0.13.2
*/
export const whenIdle: E.Env<WhenIdleEnv, IdleDeadline> = (e: WhenIdleEnv) => e.whenIdle
/**
* @category Environment
* @since 0.13.2
*/
export type WindowEnv = { readonly window: Window }
/**
* @category DOM
* @since 0.13.2
*/
export const getWindow = E.asks((e: WindowEnv) => e.window)
/**
* @category Environment
* @since 0.13.2
*/
export type DocumentEnv = { readonly document: Document }
/**
* @category DOM
* @since 0.13.2
*/
export const getDocument = E.asks((e: DocumentEnv) => e.document)
/**
* @category Environment
* @since 0.13.2
*/
export type RootElementEnv = { readonly rootElement: Element }
/**
* @category DOM
* @since 0.13.2
*/
export const getRootElement = E.asks((e: RootElementEnv) => e.rootElement)
/**
* @category DOM
* @since 0.13.2
*/
export const querySelector =
<S extends string>(selector: S) =>
<N extends ParentNode>(el: N) =>
O.fromNullable(el.querySelector<ParseSelector<S, A.Cast<N, Element>>>(selector))
/**
* @category DOM
* @since 0.13.2
*/
export const querySelectorAll =
<S extends string>(selector: S) =>
<N extends ParentNode>(el: N) =>
Array.from(el.querySelectorAll(selector)) as readonly ParseSelector<S, Element>[]
/**
* A Failure used to represent being unable to query for our RootElement
* @category Failure
* @since 0.13.4
*/
export const QueryRootElementFailure = Fail.named<{
readonly selector: string
readonly message: string
}>()('@typed/fp/dom/QueryRootElementError')
/**
* A Failure used to represent being unable to query for our RootElement
* @category Environment
* @since 0.13.4
*/
export type QueryRootElementFailure = Fail.EnvOf<typeof QueryRootElementFailure>
/**
* Provide the root element to your application by querying for an element in the document
* @category DOM
* @since 0.13.2
*/
export const queryRootElement = (selector: string) =>
pipe(
getDocument,
E.map(querySelector(selector)),
EO.map((rootElement): RootElementEnv => ({ rootElement })),
EO.getOrElseEW(() =>
QueryRootElementFailure.throw({
selector,
message: `Unable to find root element by selector ${selector}!`,
}),
),
)
/**
* Common setup for rendering an application into an element
* @category DOM
* @since 0.13.4
*/
export const patch =
<Patch extends (element: any, renderable: any) => any>(patch: Patch) =>
<E>(
stream: RS.ReaderStream<E, ArgsOf<Patch>[1]>,
): RS.ReaderStream<E & KV.Env & RootElementEnv & QueryRootElementFailure, ArgsOf<Patch>[0]> =>
pipe(
getRootElement,
RS.fromEnv,
RS.switchMapW((rootElement) => pipe(stream, RS.scan(patch, rootElement))),
)
/**
* Find the default EventMap for a given element
* @category Type-level
* @since 0.13.4
*/
export type GetDefaultEventMap<Target> = Target extends Window
? WindowEventMap
: Target extends Document
? DocumentEventMap
: Target extends HTMLBodyElement
? HTMLBodyElementEventMap
: Target extends HTMLVideoElement
? HTMLVideoElementEventMap
: Target extends HTMLMediaElement
? HTMLMediaElementEventMap
: Target extends HTMLFrameSetElement
? HTMLFrameSetElementEventMap
: Target extends HTMLElement
? HTMLElementEventMap
: Target extends SVGElement
? SVGElementEventMap
: Target extends Element
? ElementEventMap
: Readonly<Record<string, unknown>>
/**
* Append the proper CurrentTarget to an Event
* @category Type-level
* @since 0.13.4
*/
export type WithCurrentTarget<Ev, Target> = Ev & { readonly currentTarget: Target }
/**
* @category Use
* @since 0.13.4
*/
export const useEventListener = <E1, Target extends EventTarget, EventName extends string, E2, A>(
getEventListener: E.Env<E1, O.Option<Target>>,
eventName: EventName,
onEvent: (
event: WithCurrentTarget<GetDefaultEventMap<Target>[EventName], Target>,
) => E.Env<E2, A>,
): E.Env<E1 & E2 & KV.Env & SchedulerEnv, O.Option<A>> => {
type Ev = WithCurrentTarget<GetDefaultEventMap<Target>[EventName], Target>
const use = useReaderStream<Target>()
return pipe(
getEventListener,
EO.chainEnvK((target) =>
use(
pipe(
S.newStream<Ev>((sink, scheduler) => {
const listener = (ev: unknown) => sink.event(scheduler.currentTime(), ev as Ev)
target.addEventListener(eventName, listener)
return { dispose: () => target.removeEventListener(eventName, listener) }
}),
RS.fromStream,
RS.chainEnvK(onEvent),
),
target,
),
),
E.map(O.flatten),
)
}
/**
* @category Use
* @since 0.13.2
*/
export const usePopstate = () =>
pipe(
useEventListener(pipe(getWindow, EO.fromEnv), 'popstate', () => getState),
EO.getOrElseEW(() => getState),
)
/**
* @category Use
* @since 0.13.2
*/
export const useHashChange = () =>
pipe(
useEventListener(pipe(getWindow, EO.fromEnv), 'hashchange', () => getHash),
EO.getOrElseEW(() => getHash),
)
/**
* @category Use
* @since 0.13.2
*/
export const useWhenUrlChanges = <E, A>(env: E.Env<E, A>) =>
pipe(env, E.apFirstW(usePopstate()), E.apFirstW(useHashChange()))
/**
* @category Use
* @since 0.13.2
*/
export const useLocation = useWhenUrlChanges(getLocation)
/**
* @category Use
* @since 0.13.2
*/
export const useHistory = useWhenUrlChanges(getHistory)
/**
* @category History
* @since 0.13.2
*/
export const pushState = <A>(state: A, path: string) =>
pipe(
getHistory,
E.chainW((history) => E.fromIO(() => history.pushState(state, '', path))),
)
/**
* @category History
* @since 0.13.2
*/
export const navigateTo = (path: string) => pushState(null, path)
/**
* @category History
* @since 0.13.2
*/
export const replaceState = <A>(state: A, path: string) =>
pipe(
getHistory,
E.chainW((history) => E.fromIO(() => history.replaceState(state, '', path))),
)
/**
* @category History
* @since 0.13.2
*/
export const getState = pipe(
getHistory,
E.map(({ state }) => state as unknown),
)
/**
* @category History
* @since 0.13.3
*/
export const goBack = pipe(
getHistory,
E.chainW((history) => E.fromIO(() => history.back())),
)
/**
* @category History
* @since 0.13.3
*/
export const goForward = pipe(
getHistory,
E.chainW((history) => E.fromIO(() => history.forward())),
)
/**
* @category History
* @since 0.13.3
*/
export const goTo = (n: number) =>
pipe(
getHistory,
E.chainW((history) => E.fromIO(() => history.go(n))),
)
/**
* @category Location
* @since 0.13.2
*/
export const reload = pipe(
getLocation,
E.chainW((l) => E.fromIO(() => l.reload())),
)
/**
* @category Location
* @since 0.13.2
*/
export const assign = (url: string | URL) =>
pipe(
getLocation,
E.chainW((l) => E.fromIO(() => l.assign(url))),
)
/**
* @category Location
* @since 0.13.3
*/
export const getHash = pipe(
getLocation,
E.map((l) => l.hash),
)
/**
* @category Location
* @since 0.13.3
*/
export const getPathname = pipe(
getLocation,
E.map((l) => l.pathname),
)
/**
* @category Location
* @since 0.13.3
*/
export const getOrigin = pipe(
getLocation,
E.map((l) => l.origin),
)
/**
* @category Location
* @since 0.13.3
*/
export const getHref = pipe(
getLocation,
E.map((l) => l.href),
)
/**
* @category Location
* @since 0.13.3
*/
export const getHost = pipe(
getLocation,
E.map((l) => l.host),
)
/**
* @category Location
* @since 0.13.3
*/
export const getHostname = pipe(
getLocation,
E.map((l) => l.hostname),
)
/**
* @category Location
* @since 0.13.3
*/
export const getPort = pipe(
getLocation,
E.map((l) => l.port),
)
/**
* @category Location
* @since 0.13.3
*/
export const getProtocol = pipe(
getLocation,
E.map((l) => l.protocol),
)
/**
* @category Location
* @since 0.13.3
*/
export const getSearch = pipe(
getLocation,
E.map((l) => l.search),
)