Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
@skava/modules / ___dist / router / src / OneRouterToRuleThemAll.js
Size: Mime:
/* @todo @lint - split up a bit - max-lines */
/* eslint-disable complexity */
/* @lint this is fine, decision tree */
/* eslint-disable max-statements */
import React from 'react'
import { object } from 'prop-types'
import qs from 'query-string'
// @todo @james may want to use extend shallow
// extendObservable, observe, runInAction
import { observable } from 'xmobx/mobx'
// import { inject, observer } from 'xmobx/mobx-react'
import { stringify, merge, defineFinal } from 'chain'
// @todo react-router-config?
import matchPath from 'react-router-dom/matchPath'
import {
  isNil,
  isObj,
  isString,
  isFunction,
  isArray,
  isValidJSON,
  EMPTY_STRING,
  autofixSafe,
  fromPairsToObj,
} from 'exotic'
// !!!
// @todo
// import { application } from 'state/application'
// !!!
import makeHistory from './makeHistory'
import {
  // matchRouteParams,
  // matchProtocol,
  // routePathsList,
  // magic
  // routePathList,
  // routePathsByName,
  // pathParams,
  //
  // unused
  hasProtocol,
  isHash,
  routeToAttributes,
  // checks
  isRelativeWebAddress,
  isFullyQualifiedWebAddress,
  isStringifiedParams,
  isURIEncoded,
  // frozen
  routePathCurrent,
  // utils
  parseJSON,
  stripNonAlphaNumeric,
  goodLookingStringify,
} from './deps'
import { config } from './config'

const history = makeHistory()
const IS_BROWSER = typeof window === 'object'


// --- above all routers ---

// @todo optimize
// @decorate({
//   // urlFromExpress
//   gotoNext: action,
//   full: computed,
//   origin: computed,
//   pathname: computed,
//   searchParams: computed,
//   history: observable.ref,
//   getSearchParams: computed,
//   parse: computed,
//   update: action,
//   delete: action,
//   replace: action,
//   clearHistory: action,
//   set: action,
//   toObj: computed(requiresReaction: false),
//   entries: computed(requiresReaction: true),
//   get: computed(requiresReaction: true),
//   has:  computed(requiresReaction: true),
// })

/**
 * @todo subscribe, observe, unsubscribe
 *
 * @note modules/ does not hot reload
 * extends Container
 */
class OneRouterToRuleThemAll {
  // History | HistoryType
  // hard to import the typing that itself imports this file...
  // history: History
  // observersList: Array<Function>

  constructor() {
    // this.observersList = new WeakMap()
    this.observersList = []
    // super()
    // @todo can use this to avoid making  all  history and router observable
    this.store = observable.map()

    // for @overriding parent entries
    this.entries = this.toObj
    // allow using  .get but  on  our  .entries
    this.get = key => {
      const entries = this.entries()
      return entries[key]
    }
    this.set = (key, value) => {
      this.store.set(key, value)
      return this
    }
    this.from = obj => {
      const keys = Object.keys(obj)
      let didChange = false
      // const original = stringify(this.entries())
      // const changed = stringify(obj)
      // if (original === changed) {
      //   return
      // }

      // // go through keys
      keys.forEach(key => {
        const value = obj[key]
        // update if it changed
        if (this.store.get(key) !== value) {
          this.store.set(key, value)
          didChange = true
        }
      })

      // // could also  set the .diff
      // if (didChange === true) {
      //   this.observersList.forEach(subscriber => subscriber(this))
      // }
    }

    // for backend routing
    // this.urlFromExpress = EMPTY_STRING

    /**
     * @note I'm not actually sure if `extendObservable`
     *       is any different than just  doing this.property = observable({})
     *       but I think it makes the instance have a $mobx property too
     *       to help tracks
     * @todo @james ask Michel from mobx
     */

    // setup observables
    // this.history = observable(history)
    // this.router = observable({})
    // const types = {
    //   history,
    //   router: {},
    // }
    // extendObservable(this, types)
    // this.history = history
    this.router = {
      location: {
        //
      },
      history: {
        location: {

        },
      },
    }
  }

  get urlFromExpress() {
    return isObj(this.router) && isObj(this.router.location) ? this.router.location.full || '' : ''
  }
  set urlFromExpress(oneUrl) {
    // this.oneUrl = oneUrl
    this.oneUrl = oneUrl
    this.router.location = oneUrl || {}
    this.router.history.location = oneUrl || this.router.location
  }


  toString(pretty?: Boolean = true): String {
    const entries = this.entries()
    return pretty === true ? stringify(entries, null, 2) : stringify(entries)
  }
  has(data: Object | String): Boolean {
    return this.toString(false).includes(data)
  }

  /**
   * @access private
   * @param {*} msg
   * @param {*} data
   */
  log(msg, data) {
    // if (this.getDebug()) {
    //   if (data === undefined && typeof msg === 'string') {
    //     console.log(msg)
    //   } else {
    //     console.log(stringify({ [msg]: data }, null, 2))
    //   }
    // }
  }

  /**
   * @see prevnextcontainer
   * @see history/goBack
   * @description common naming
   * @alias goBack
   * @alias gotoBack
   */
  gotoPrev() {
    /**
     * @todo - should unify this history usage stuff & simplify v3
     */
    this.router.history.goBack()
  }
  gotoNext() {
    this.router.history.goForward()
  }

  /**
   * @access public
   * @example https://uxui.com/route?search=eh#hash
   *
   * @alias fullyQualifiedWebAddress
   * @alias absolute
   * @alias absoluteWebAddress
   * @name full
   */
  get full(): string {
    return IS_BROWSER
      ? window.location.href
      : this.router.history.location.href ||
      this.router.history.location.origin + '/' + this.router.history.location.pathname
  }

  // also available in entries
  get origin(): string {
    return this.router.history.location.origin
  }

  /**
   * @access private
   * (pathname.match(/\w+/gim) || []).join('')
   */
  get pathname() {
    if (IS_BROWSER) {
      return window.location.pathname || '@@empty-browser'
    }
    if (this.oneUrl !== undefined) {
      return this.oneUrl.pathname
    }
    if (typeof global === 'object' && global.oneUrl !== undefined) {
      return global.oneUrl.pathname
    }
    if (this.urlFromExpress !== EMPTY_STRING) {
      return this.urlFromExpress
    }
    if (isObj(this.router) &&
      isObj(this.router.history) &&
      // @todo - this was the issue
      isObj(this.router.history.location) &&
      isString(this.router.history.location.pathname) &&
      this.router.history.location.pathname !== '/') {
      return this.router.history.location
      // return this.history.pathname
    }
    if (this.history.pathname) {
      return this.history.pathname
    }
    if (this.history.location.pathname) {
      return this.history.location.pathname
    }

    const unknown = new Error('could not find pathname')
    console.error(unknown)
    console.log(this)
    return '@@unknown'
  }

  /**
   * @private
   * @example
   *    localhost/eh/10/
   *    /eh/:categoryId
   *    match.params.categoryId
   *    => 10
   *
   * @type {computed}
   * @return {string | number | boolean | object | undefined}
   */
  get matched(): Object {
    const matched = {}
    const pathname = this.pathname
    const routePathsList = config.get('routePathsList')

    // go through our routes
    // use the sealed object for perf
    // match params (which has a cache)
    // merge with matched object
    // return matched
    for (let index = 0; index < routePathsList.length; index++) {
      const matchablePath = routePathsList[index]

      routePathCurrent.path = matchablePath
      const matchedFromPath = matchPath(pathname, routePathCurrent)

      // !!!!!!!!! IMPORTANT
      // console.log('____MATCHED___', index, pathname, matchedFromPath, matchablePath)

      if (isObj(matchedFromPath)) {
        // this.log(pathname, { routePathCurrent, matchedFromPath, pathname })
        // const params = { ...matchedFromPath, ...matchedFromPath.params }
        const params = { ...matchedFromPath.params }
        Object.assign(matched, params)

        // access to which route had the data
        // Object.defineProperty(matched, '@@matches', {
        //   writable: false,
        //   configurable: false,
        //   enumerable: false,
        //   value: matchedFromPath,
        // })
      }
    }

    // react router always counts this as root
    // if (this.props && isObj(this.props.match)) {
    //   // matched = {
    //   //   ...matched,
    //   //   ...this.props.match,
    //   //   ...this.props.match.params,
    //   // }
    //   Object.assign(matched, this.props.match.params)
    // }
    // else {
    //   matched.match = matched
    // }

    return matched
  }
  /**
   * @private
   * @see this.getSearchParams
   * @return {String}
   */
  get searchParams(): String {
    return this.getSearchParams()
  }

  /**
   * @public
   * @description this.router this.router.history history
   *
   * @todo regression when doing babel
   *
   * @type {mobx.Computed}
   * @return {ReactRouter.history | History}
   */
  get history() {
    return (isObj(this.router) && isObj(this.router.history)
      ? this.router.history
      : history) || {}
  }

  /**
   * @see http://unixpapa.com/js/querystring.html
   * @api https://github.com/sindresorhus/query-string#nesting
   *
   * @todo these entries could be observable...
   *
   * @param {string} [fallback='']
   * @return {object | string | array}
   * @type {computed}
   */
  getSearchParams(fallback = ''): Object | '' {
    const parsed = this.history.location.search
      ? qs.parse(this.history.location.search)
      : fallback

    if (!parsed) {
      return parsed
    }

    // could optimize this
    const searchParams = {}
    Object.assign(searchParams, parsed)

    /**
     * it does not support doing things like
     * @example &eh=1&eh=10
     */
    if (IS_BROWSER) {
      const params = new URLSearchParams(window.location.search)
      // issue exporting fromIteratorToArray
      const entries = Array.from(params)
      const obj = fromPairsToObj(entries)

      // searchParams = { ...parsed, ...obj }
      Object.assign(searchParams, obj)
    }

    const final = this.parse(searchParams)
    return final
  }

  /**
   * @param {Object<string, JSON>} parsed
   * @return {Object<string, Object>} destringified
   */
  parse(parsed) {
    if (isObj(parsed)) {
      Object.keys(parsed).forEach(key => {
        // remove undefined
        if (isNil(parsed[key]) === true) {
          delete parsed[key]
        }
        if (isString(parsed[key]) === false) {
          return
        }

        if (isURIEncoded(parsed[key]) === true) {
          parsed[key] = decodeURIComponent(parsed[key])
        }

        if (isValidJSON(parsed[key]) === true) {
          parsed[key] = parseJSON(parsed[key])
          if (isObj(parsed[key])) {
            const toJSON = () => stringify(parsed[key])
            defineFinal(parsed[key], 'toString', toJSON)
            defineFinal(parsed[key], Symbol.toPrimitive, toJSON)
          }
        }
      })
    }

    return parsed
  }

  /**
   * @todo return better intelisense here
   * @alias entries
   * @return {object}
   */
  toObj() {
    const pathname = this.pathname
    // const pathstring = stripNonAlphaNumeric(pathname)
    const browserLocation = IS_BROWSER ? window.location : {}
    const params = this.getSearchParams({})
    const matched = this.matched

    const flattenedActions = {
      // no need for this
      // [pathstring]: pathname,
      ...browserLocation,
      ...this.history,
      ...this.history.location,
      ...this.router,
    }

    // autofixSafe
    const entries = ({
      pathname,
      ...params,
      ...matched,
    })

    // @todo @james
    // runInAction(() => {
    //   const { hash, key, search, state } = this.history.location
    //   const length = flattenedActions.length

    //   this.from({
    //     pathname,
    //     params,
    //     length,
    //     search,
    //     state,
    //     key,
    //     hash,
    //     matched,
    //     // entries,
    //   })
    //   // this
    //   //   .set('obj')
    //   //   .set('pathname', pathname)
    //   //   .set('params', params)
    //   //   .set('matched', matched)
    //   //   .set('entries', entries)
    //   //   .set('key', key)
    //   //   .set('state', state)
    //   //   .set('hash', hash)
    //   //   .set('search', search)
    //   //   .set('length', length)
    // })

    const mergedEntries = {
      ...flattenedActions,
      ...entries,
    }

    // this.log('___entries___', entries)

    return (mergedEntries)
  }

  /**
   * @tutorial https://github.com/ReactTraining/history/blob/master/modules/LocationUtils.js
   * @alias push
   * @alias merge
   * @alias updateQueryParams
   *
   * @fires observable update
   * @description any kind of url update can be done here
   *
   * @param {Object | String | Array} to
   * @return {OneRouterToRuleThemAll} @chainable
   *
   * @todo should chain it :(
   *
   * @example
   *   [state, pathname, hash]
   *   toUrlOrPathNameOrParams, optionalParamAsDataOrPath
   *   let to = toUrlOrPathNameOrParams
   *   let state = optionalParamAsDataOrPath
   *   let hash
   *   if (arguments.length === 2)
   */
  update(to, options = { shouldStringify: true, shouldMerge: true, shouldUseNative: false }) {
    this.log('oneRouterUpdate', { to })
    /**
     * @static @todo @demo @fixme
     */
    // application.showLoadingGauge(true, 1000)

    if (isObj(to)) {
      this.log('oneRouterUpdate_isObj')

      // if (to.search) {
      //   to = to.search
      // }
      // if (to.hash) {
      //   return this.update('#' + to.hash)
      // }
      if (to.pathname) {
        throw new Error(
          `avoid stringifying history-like properties: ` + to
        )
      }
      // Commented the below line for filter to work in plp
      const searchParams = {}
      // const searchParams = this.getSearchParams(false)
      this.log({ to })

      if (searchParams && options.shouldMerge !== false) {
        to = merge(searchParams, to)
      }
      this.log({ to })

      // const params = options.shouldStringify ? qs.stringify(to) : to
      const params = goodLookingStringify(to)


      if (options.shouldUseNative === true) {
        // to, document.title, url
        window.history.pushState(to, '', this.pathname + '?' + params)
        return this
      } else {
        this.history.push({ pathname: this.pathname, search: params })
      }
      return this
    }
    // @note this works
    // but  we don't  want to use this how it's been used in domain  currently
    // if (isHash(to)) {
    //   this.history.push({ pathname: this.pathname, hash: to })
    //   return this
    // }
    if (isStringifiedParams(to)) {
      this.log('isStringifiedParams')
      return this.update(qs.parse(to))
      // this.history.push({ search: to })
      // return this
    }
    if (isFullyQualifiedWebAddress(to)) {
      this.log('isFullyQualifiedWebAddress')
      // check our local routes
      // otherwise goto

      // routePathsList.includes(to)
      // return this.router.goto(to)

      this.history.push({ pathname: to })
      return this
    }
    if (isRelativeWebAddress(to)) {
      this.log('isFullyQualifiedWebAddress')
      // check our local routes
      // return this.router.replace()
      this.history.replace(to)
      return this
    }

    return this
  }

  /**
   * @param {Object | String} param query param to delete
   * @return {OneRouter} @chainable
   */
  delete(param) {
    if (isString(param)) {
      const searchParamsRaw = this.getSearchParams(false)
      const searchParams = this.parse(searchParamsRaw)
      if (searchParams === false) {
        return this
      } else {
        delete searchParams[param]
        return this.update(searchParams)
      }
    }
    return this
  }

  /**
   * @alias goto
   * @inheritdoc
   * @see this.update
   * @description same as update, but goto this url instead of merging
   * @type {Action}
   *
   * @param {String} path
   * @return {OneRouter} @chainable
   */
  replace(path) {
    this.router.history.replace(path)
    return this
  }

  /**
   * @type {Action}
   * @description reset url, goto root
   * @return {OneRouter} @chainable
   */
  clearHistory() {
    this.history.replace({
      pathname: this.location.pathname,
      search: ``,
    })
    return this
  }

  /**
   * @description
   * @fires onChange
   * @event onChange
   *
   * @todo add  these aliases
   * @alias observe
   * @alias subscribe
   *
   * @param {Function} subscriber called when observable changes
   * @return {OneRouter} @chainable
   */
  onChange(subscriber) {
    /**
     * @api https://mobx.js.org/refguide/observe
     */
    // observe(this, subscriber)
    // observe(this.store, subscriber)
    // if (this.observersList.includes(subscriber) === false) {
    // this.observersList.set(subscriber, true)
    // this.observersList.push(subscriber)
    // const subscribeToRouteChange = this.get('subscribe')
    const subscribeToRouteChange = this.router && this.router.history && this.router.history.listen
    if (isFunction(subscribeToRouteChange)) {
      subscribeToRouteChange(subscriber)
    } else {
      console.assert(typeof subscribeToRouteChange === 'function', 'ON_CHANGE_ONE_ROUTER_NOT_FUNCTION')
    }
    // }

    /**
     * @deprecated
     *
     * @api https://mobx.js.org/refguide/reaction.html
     * @todo could use ^ probably better perf, easy to swap  out  later
     *
     * // this was not clear, did not work
     * @api  https://mobx.js.org/refguide/autorun.html
     * Just like the @observer decorator/function,
     * autorun will only observe data
     * that is used during the execution of the provided function.
     *
     * @note we could used boxed values but the docs say
     * > since MobX tracks changes to boxes automatically,
     *   in most cases it is better to use a reaction
     *   like mobx.autorun instead.
     */
    // autorun(subscriber, this)
    return this
  }

  /**
   * window.location.reload()
   */
  reload(): void {
    window.location.reload()
  }

  get isRestricted(): boolean {
    const restrictedRoutes = this.get('restrictedRoutes') || []
    // @todo use chain-able/matcher
    // ^ regexp, function, or string, or string wildcard/route-params

    // isMatch(restrictedRoutes, this.pathname)
    const isRestricted = restrictedRoutes.includes(this.pathname)

    // return as bool
    if (isRestricted) {
      return true
    } else {
      return false
    }
  }
}

const oneRouter = new OneRouterToRuleThemAll()

if (IS_BROWSER) {
  window.oneRouter = oneRouter
}


// ========= @todo split =========
// ^^^^^^^^  if variable declaration was another file, single responsibility, would work ^^^^^

// provide it all the way down through context
// @withRouter
class OneRouter extends React.Component {
  static childContextTypes = {
    router: object.isRequired,
    oneRouter: object,
  }
  getChildContext() {
    return {
      oneRouter,
      router: {
        ...this.context.router,
        history: this.props.history,
        route: {
          location: this.props.history.location,
          match: this.state.match,
        },
      },
    }
  }
}
const provideOneRouter = _props => {
  // PureComponent
  // @withRouter
  class OneRouterWrap extends React.Component {
    static contextTypes = {
      router: object,
    }

    // can trigger oneRouter here too
    // @michael @bhargavi
    // // @todo !!!
    // oneRouter.onChange(change => {
    //   // import {sessionContainer} from 'state/container'
    //   // const { isRegisteredUser, isGuestUser } = sessionContainer
    //   // () => isRegisteredUser
    //   // const restrictedRoutes = ['/']
    //   // oneRouter.set('restrictedRoutes', restrictedRoutes)
    //   // if (oneRouter.isRestricted === true) {
    //   //   oneRouter.update('/login')
    //   // }
    // })
    componentWillMount() {
      const props = this.props
      // console.log('PROPS_WILLMOUNT__', JSON.stringify(props, null, 2))
      oneRouter.props = props
      oneRouter.router = this.context.router || props.router
      // oneRouter.history = oneRouter.history.router || oneRouter.history

      // const { history, isSSR } = this.props
      // if (!isSSR) this.unsubscribeFromHistory = history.listen(this.handleLocationChange)
      // this.handleLocationChange(history.location)
    }
    // componentWillUnmount() {
    //   if (this.unsubscribeFromHistory) {
    //     this.unsubscribeFromHistory()
    //   }
    // }
    // handleLocationChange = location => {
    //   this.store.notifyRouteChange(location)
    // }
    // ===
    // componentWillReceiveProps(props) {
    //   // console.log('___PROPS___', JSON.stringify(props, null, 2))
    //   props = props || this.props
    //   oneRouter.props = clone(props)
    //   oneRouter.router = this.context.router || props.router
    //   oneRouter.history = oneRouter.history.router || oneRouter.history
    //   oneRouter.entries()
    // }
    render() {
      return null
    }
  }
  return React.createElement(OneRouterWrap, _props)
}
// not used?
const withOneRouter = Target => {
  return class OneRouterWrapper extends React.Component {
    render() {
      const attributes = {
        ...oneRouter,
        ...this.props,
      }
      return <Target {...attributes} />
    }
  }
}

oneRouter.OneRouter = OneRouter
oneRouter.connectToRouter = withOneRouter
oneRouter.withOneRouter = withOneRouter
// oneRouter.history = history
oneRouter.provide = provideOneRouter
const OneRouterContainer = provideOneRouter

export {
  // oneServerSideRender,
  OneRouterContainer,
  provideOneRouter,
  withOneRouter,
  OneRouter,
  oneRouter,
  history,
  qs,
  // routes,
  makeHistory,
}
export default oneRouter