cellog/ion-router

View on GitHub
src/Link.tsx

Summary

Maintainability
B
5 hrs
Test Coverage
import React, {
  MouseEvent,
  useCallback,
  useContext,
  useState,
  useEffect,
} from 'react'
import RouteParser from 'route-parser'
import invariant from 'invariant'

import * as actions from './actions'
import Context, { RouterContext } from './Context'
import { Location } from 'history'

interface Props {
  route?: string
  children: React.ReactNode
  onClick?: (e: MouseEvent) => void
  to?: string
  replace?: string
  href?: string
}

function createRouteParser(route: string, routeInfo: RouterContext) {
  if (route && routeInfo.routes[route]) {
    return new RouteParser(routeInfo.routes[route].path)
  } else {
    return false
  }
}

function isLocation(s: any): s is Location {
  return s && s.pathname
}

type ValidHTMLAnchorProps =
  | 'download'
  | 'hreflang'
  | 'referrerPolicy'
  | 'rel'
  | 'target'
  | 'type'
  | 'id'
  | 'accessKey'
  | 'className'
  | 'contentEditable'
  | 'dir'
  | 'draggable'
  | 'hidden'
  | 'lang'
  | 'spellcheck'
  | 'style'
  | 'tabIndex'
  | 'title'

export type HTMLAnchor = {
  [P in ValidHTMLAnchorProps]: HTMLAnchorElement[P]
}

const validProps: ValidHTMLAnchorProps[] = [
  'download',
  'hreflang',
  'referrerPolicy',
  'rel',
  'target',
  'type',
  'id',
  'accessKey',
  'className',
  'contentEditable',
  'dir',
  'draggable',
  'hidden',
  'lang',
  'spellcheck',
  'style',
  'tabIndex',
  'title',
]

export function Link<ExtraProps extends { [key: string]: any }>(
  props: Props & Partial<HTMLAnchor> & ExtraProps
) {
  const { to, replace, onClick, href, children, route, ...extra } = props
  const routeInfo = useContext(Context)
  const [routeState, setRoute] = useState<false | RouteParser>(
    routeInfo ? createRouteParser(route!, routeInfo) : false
  )
  useEffect(() => {
    if (route && routeInfo && routeInfo.routes[route]) {
      setRoute(new RouteParser(routeInfo.routes[route].path))
    }
  }, [route, routeInfo])
  const click = useCallback(
    e => {
      e.preventDefault()
      let url: string | Location
      const action = replace ? 'replace' : 'push'
      if (route) {
        url = routeState
          ? routeState.reverse({ to, replace, href, ...extra }) || ''
          : ''
      } else if (replace) {
        url = replace
      } else {
        url = to || ''
      }
      routeInfo && routeInfo.dispatch(actions[action](url))
      if (onClick) {
        onClick(e)
      }
    },
    [replace, route, routeState, to, routeInfo, href, extra]
  )

  const aProps = Object.keys(props).reduce<
    {
      [P in keyof (Props & HTMLAnchorElement)]?: (Props &
        HTMLAnchorElement &
        ExtraProps)[P]
    }
  >((newProps, key: keyof (Props & HTMLAnchor & ExtraProps)) => {
    if (validProps.includes(key as ValidHTMLAnchorProps))
      (newProps as any)[key] = props[key]
    if ((key as string).slice(0, 5) === 'data-')
      (newProps as any)[key] = props[key]
    return newProps
  }, {})
  invariant(
    !href,
    'href should not be passed to Link, use "to," "replace" or "route" (passed "%s")',
    href
  )
  let landing: string | Location = replace || to || ''
  if (routeState) {
    landing = routeState.reverse(props) || ''
  } else if (isLocation(landing)) {
    landing = `${landing.pathname}${'' + landing.search}${'' + landing.hash}`
  }
  return (
    <a href={landing as string} onClick={click} {...aProps}>
      {children}
    </a>
  )
}

export default Link