GithubHelp home page GithubHelp logo

typed-history's Introduction

@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
}

typed-history's People

Contributors

renovate[bot] avatar tylors avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.