@typed/history -- 2.0.0
Functional History API for the browser and node
Get it
yarn add @typed/history
# or
npm install --save @typed/history
API Documentation
All functions are curried!
Env
Implementations of Location
and History
.
export type Env = Readonly<{ location: Location; history: History }>
Href
A type-alias to represent strings that are HREFs.
export type Href = string
ParsedHref
ParsedHref JSON data structure
export type ParsedHref = {
readonly href: string
readonly protocol: string
readonly host: string
readonly userInfo: string
readonly username: string
readonly password: string
readonly hostname: string
readonly port: string
readonly relative: string
readonly pathname: string
readonly directory: string
readonly file: string
readonly search: string
readonly hash: string
}
ServerHistory
An implementation of the History
interface.
See the code
export class ServerHistory implements History {
// Does not affect behavior
public scrollRestoration: ScrollRestoration = 'auto'
// ServerHistory specific
private _states: Array<{ state: any; url: string }>
private _index: number = 0
private location: Location
constructor(location: Location) {
this.location = location
this._states = [{ state: null, url: this.location.pathname }]
}
private set index(value: number) {
this._index = value
const { url } = this._states[this._index]
this.location.replace(url)
}
private get index(): number {
return this._index
}
get length(): number {
return this._states.length
}
get state(): any {
const { state } = this._states[this.index]
return state
}
public go(quanity: number = 0): void {
if (quanity === 0) return void 0
const minIndex = 0
const maxIndex = this.length - 1
this.index = Math.max(minIndex, Math.min(maxIndex, this.index + quanity))
}
public back(): void {
this.go(-1)
}
public forward(): void {
this.go(1)
}
public pushState(state: any, _: string | null, url: string) {
this._states = this._states.slice(0, this.index).concat({ state, url })
this.index = this._states.length - 1
}
public replaceState(state: any, _: string | null, url: string) {
this._states[this.index] = { state, url }
}
}
ServerLocation
An in-memory implementation of Location
.
See the code
export class ServerLocation implements Location {
private history: History
public href: string
constructor(href: string) {
this.replace(href)
}
get hash(): string {
return parseValue('hash', this)
}
set hash(value: string) {
const hash = value.startsWith('#') ? value : '#' + value
replace('hash', hash, this)
}
get host(): string {
return parseValue('host', this)
}
set host(value: string) {
replace('host', value, this)
}
get hostname(): string {
return parseValue('hostname', this)
}
set hostname(value: string) {
replace('hostname', value, this)
}
get pathname(): string {
return parseValue('pathname', this)
}
set pathname(value: string) {
replace('pathname', value, this)
}
get port(): string {
const { href } = this
const { port, protocol } = parseHref(href)
if (port) return port
return protocol === HTTPS_PROTOCOL ? HTTPS_DEFAULT_PORT : HTTP_DEFAULT_PORT
}
set port(value: string) {
replace('port', value, this)
}
get protocol(): string {
return parseValue('protocol', this) || 'http:'
}
set protocol(value: string) {
replace('protocol', value, this)
}
get search(): string {
return parseValue('search', this)
}
set search(value: string) {
const search = value.startsWith('?') ? value : '?' + value
replace('search', search, this)
}
get origin(): string {
return this.protocol + '//' + this.host
}
public assign(url: string): void {
this.replace(url)
if (this.history) this.history.pushState(null, null, this.href)
}
// Does not have defined behavior outside of browser
public reload(): void {}
public replace(url: string): void {
let { href, host, relative } = parseHref(url)
if (this.host && !host) href = this.host + href
const { protocol } = parseHref(href)
if (href !== relative && this.protocol && !protocol)
href = this.protocol + '//' + href
this.href = href
}
public toString(): string {
return this.href
}
// ServerLocation Specific
public setHistory(history: History) {
this.history = history
return this
}
}
function replace(
key: keyof ParsedHref,
value: string,
location: ServerLocation
) {
const { href } = location
const currentValue = parseHref(href)[key]
const updateHref = href.replace(currentValue, value)
location.replace(updateHref)
}
function parseValue(key: keyof ParsedHref, location: ServerLocation): string {
return parseHref(location.href)[key] as string
}
assign(url: string, location: Location): void
Loads the resource at the URL provided in parameter.
See the code
export const assign: Assign = invoker<Location, string, void>(1, 'assign')
back(history: History): void
Goes back to the previous location
See the code
export const back = invoker<History, void>(0, 'back')
createEnv(href: string = '/'): Env
Given an href returns a collection of History and Location implementations.
The given href is only used if it has been detected your are not in a browser
environment. If it's detected you are in a browser references to window.history
and window.location
are simply returned.
See an example
import { createEnv, href, pushHref } from '@typed/history'
const { history, location } = createEnv('https://my.example.com/')
console.log(href(location)) // logs => https://my.example.com/
pushHref('https://my.example.com/other', history)
console.log(href(location)) // logs => https://my.example.com/other
See the code
export function createEnv(href: Href = '/'): Env {
if (typeof location !== 'undefined' && typeof history !== 'undefined')
return { location, history }
const serverLocation = new ServerLocation(href)
const serverHistory = new ServerHistory(serverLocation)
serverLocation.setHistory(serverHistory)
return {
location: serverLocation,
history: serverHistory,
}
}
forward(history: History): void
Goes to the next location
See the code
export const forward = invoker<History, void>(0, 'forward')
go(quantity: number, history: History): void
Goes forward or back a specificed number of locations.
See the code
export const go: Go = invoker<History, number, void>(1, 'go')
hash(location: Location): string
Returns location.has
See the code
export const hash = prop<Location, 'hash'>('hash')
host(location: Location): string
Returns location.host
See the code
export const host = prop<Location, 'host'>('host')
hostname(location: Location): string
Returns location.hostname
See the code
export const hostname = prop<Location, 'hostname'>('hostname')
href(location: Location): string
Returns location.href
See the code
export const href = prop<Location, 'href'>('href')
origin(location: Location): string
Returns location.origin
See the code
export const origin = prop<Location, 'origin'>('origin')
parseHref(href: string): ParsedHref
Parses an href into JSON.
See the code
export function parseHref(href: string): ParsedHref {
const matches = HREF_REGEX.exec(href)
const parsedHref = {} as Record<keyof ParsedHref, string>
for (let i = 0; i < keyCount; ++i) {
const key = keys[i]
let value = matches[i] || ''
if (key === 'search' && value) value = '?' + value
if (key === 'protocol' && value && !value.endsWith(':')) value = value + ':'
if (key === 'hash') value = '#' + value
parsedHref[key] = value
}
return parsedHref
}
const keys: ReadonlyArray<keyof ParsedHref> = [
'href',
'protocol',
'host',
'userInfo',
'username',
'password',
'hostname',
'port',
'relative',
'pathname',
'directory',
'file',
'search',
'hash',
]
const keyCount = keys.length
parseQueries<Queries extends Record<string, string>>(location: Location): Queries
Parses a Location's query string into an object of key/value pairs.
See an example
import { createEnv, pushUrl, parseQueries } from '@typed/history'
const { history, location } = createEnv()
console.log(parseQueries(location)) // logs => {}
pushUrl('/?q=hello&lang=en', history)
console.log(parseQueries(location)) // logs => { q: 'hello', lang: 'en' }
See the code
export function parseQueries<Queries extends Record<string, string> = {}>(
location: Location
): Readonly<Queries> {
const { search } = location
const queries = {} as Queries
if (!search) return queries
search
.substring(1)
.replace(QUERYSTRING_REGEX, (_: string, name: string, value: string) => {
if (name) queries[name] = value
return value
})
return queries
}
pathname(location: Location): string
Returns location.pathname
See the code
export const pathname = prop<Location, 'pathname'>('pathname')
port(location: Location): string
Returns location.port
See the code
export const port = prop<Location, 'port'>('port')
protocol(location: Location): string
Returns location.protocol
See the code
export const protocol = prop<Location, 'protocol'>('protocol')
pushHref(href: Href, history: History): void
Pushes an HREF to the History statck
See the code
export const pushHref: StateArity2 = pushState({}, '')
pushState(state: any, title: string, href: string, history: History): void
Pushes a new location into the History stack
See the code
export const pushState: StateArity4 = invoker<History, any, string, Href, void>(
3,
'pushState'
)
reload(location: Location): void
Reloads the current resource. This has no behavior if it is detected you are not inside the browser.
See the code
export const reload = invoker<Location, void>(0, 'reload')
replace(url: string, location: Location): void
Replaces the current resource with the one at the provided URL. The difference from the assign() method is that after using replace() the current page will not be saved in session History, meaning the user won't be able to use the back button to navigate to it.
See the code
export const replace: Replace = invoker<Location, string, void>(1, 'replace')
// Interfaces
export type Assign = {
(url: string, location: Location): void
(url: string): (location: Location) => void
}
export type Replace = {
(url: string, location: Location): void
(url: string): (location: Location) => void
}
replaceState(state: any, title: string, href: Href, history: History): void
Replaces the current location into the History stack
See the code
export const replaceState: StateArity4 = invoker<
History,
any,
string,
Href,
void
>(3, 'replaceState')
search(location: Location): string
Returns location.search
See the code
export const search = prop<Location, 'search'>('search')
state(location: History): any
Returns History.state
See the code
export const state: <A extends Record<string, any> = {}>(
history: History
) => Readonly<A> = prop<History, 'state'>('state')
// Interfaces
export type Go = {
(quantity: number, history: History): void
(quantity: number): (history: History) => void
}
export type StateArity4 = {
(state: any, title: string | null, href: Href, history: History): void
(state: any, title: string | null, href: Href): StateArity1
(state: any, title: string | null): StateArity2
(state: any): StateArity3
}
export type StateArity3 = {
(title: string | null, href: Href, history: History): void
(title: string | null, href: Href): StateArity1
(title: string | null): StateArity2
}
export type StateArity2 = {
(href: Href, history: History): void
(href: Href): StateArity1
}
export type StateArity1 = {
(history: History): void
}