Repository URL to install this package:
|
Version:
1.1.17 ▾
|
/* @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