core/router/lifecycle.js

import { findRoute } from './utils/paths'
import { setTransitionAttributes } from './utils/dom'
import baseTransition from './transition'
import request from './request'
import cache from './cache'
import historyManager from './history'
import eventBus from '@/core/modules/eventBus'
import * as Action from './actions'
import domify from 'domify'

/**
 * @typedef {Object} lifecycle
 * @property {function} addRoutes - add the routes to the lifecycle
 * @property {Array} addRoutes.routesObject - an array of anchors
 *
 * @property {function} setWrapper - function to set the wrapper element
 * @property {HTMLElement} setWrapper.node - the html element to set as a wrapper
 *
 * @property {function} onLoad - function to set the wrapper element
 * @property {String} onLoad.pathname - the pathname to load
 *
 * @property {function} transition
 * @property {String} transition.pathname - path name to transition to
 * @property {String} transition.action - the history action - POP|PUSH
 * @property {String} transition.transition - a custom transition object
 * @property {String} transition.dataAttrs - any data attributes on the link clicked
 *
 */

/**
 * Create a router
 * @memberof RouterUtils
 * @description lifecycle of pages entering/exiting the dom
 * @function lifecycle
 * @return {lifecycle}
 */

const lifecycle = (() => {
	/*
		setup to vars to share
	*/
	let matchRoute = () => {}
	let exitTransition = {}
	let enterTransition = {}
	let wrapper

	return {
		addRoutes(routesObject) {
			matchRoute = findRoute(routesObject)
			historyManager.set('from', matchRoute(window.location.pathname))
			return this
		},

		setWrapper(node) {
			// assign the node to the wrapper var
			wrapper = node

			return this
		},

		onLoad(pathname) {
			// get the new route object
			const newState = matchRoute(pathname)

			// combine the transition methods
			const fn = Object.assign({}, baseTransition, newState.view)

			// all the onLoad method
			fn.onLoad(newState)

			// emit this bad boy
			eventBus.emit(Action.ROUTE_TRANSITION_LOAD, newState)

			setTransitionAttributes.lifecycle('on-load')

			return this
		},

		transition({ pathname, action, transition: trans, dataAttrs }) {
			// get the new route object
			const newState = matchRoute(pathname)
			// have we been supplied with a transition object... no.. use the route one <--- transition prop needs testing
			const view = trans || newState.view

			// update the from history store.... <REWITE></REWITE>
			// historyManager.store.to = newState
			historyManager.set('to', newState)

			const from = historyManager.get('from')
			const to = historyManager.get('to')

			// combine the transition methods for exit... basic + route exits
			exitTransition = Object.assign({}, baseTransition, from.view)

			// combine the transition methods for exit... basic + route enters
			enterTransition = Object.assign({}, baseTransition, view)

			// setup the props to be passed to onExit and onExitComplete
			const exitProps = {
				to,
				from,
				wrapper,
				oldHtml: document.querySelector(exitTransition.el),
				action,
				dataAttrs
			}

			const promise = (method, transition, props = {}) =>
				new Promise((resolve, reject) => {
					transition[method]({
						next: resolve,
						onError: reject,
						...props
					})
				})

			// we have requested exit... emit
			eventBus.emit(Action.ROUTE_TRANSITION_EXIT, exitProps)

			// log(from, to)
			if (from.name) {
				setTransitionAttributes.from(from.name)
			}
			if (to.name) {
				setTransitionAttributes.to(to.name)
			}
			setTransitionAttributes.lifecycle('on-exit')

			// now... lets do the promise funk
			return Promise.all([
				promise('onExit', exitTransition, exitProps),
				request(pathname)
			])
				.then(([, resp]) => {
					// more event emiiting
					eventBus.emit(Action.ROUTE_TRANSITION_RESOLVED, exitProps)

					// do we have errors...
					if (resp.data && resp.data.ok === false) {
						// emit the error method
						exitTransition.onError({ ...exitProps, ...resp })
						// window.location.pathname = pathname
						return false
					}

					// get the data, assign to markup
					const { data: markup } = cache.get(pathname)
					// conver the response html into something we can work with
					const html = domify(markup)

					// get the title, and give it a clean
					const title = html.querySelector('title').textContent.trim()

					// query the new newHtml for the selector defined on
					// this object... default = '.page-child'
					const newHtml = html.querySelector(enterTransition.el)

					// props object passed to each after method
					const props = {
						oldHtml: document.querySelector(exitTransition.el),
						wrapper,
						newHtml,
						title,
						html,
						to,
						from,
						action,
						dataAttrs
					}

					// check... do we want to unmount the previous html
					const shouldUnmount = enterTransition.shouldUnmount(props)
					const shouldMount = enterTransition.shouldMount(props)

					// if we do... sure... unmount event
					if (shouldUnmount)
						eventBus.emit(Action.ROUTE_TRANSITION_BEFORE_DOM_UPDATE, props)

					// update the dom method
					enterTransition.updateDom(props)

					if (to.customBodyProp) {
						const prop = to.customBodyProp(newHtml)
						setTransitionAttributes.toggleCustomBodyProp(prop)
					} else {
						setTransitionAttributes.toggleCustomBodyProp(false)
					}

					// emit update event
					if (shouldMount)
						eventBus.emit(Action.ROUTE_TRANSITION_AFTER_DOM_UPDATE, props)

					// no proms here.. just call this method
					exitTransition.onAfterExit(props)
					setTransitionAttributes.lifecycle('on-after-exit')

					// return that props object we made earlier
					return props
				})
				.then(props => {
					// we have props...
					// it's possible we don't, we could have bailed early
					if (props) {
						// enter props
						const enterProps = {
							...props,
							action
						}

						// emit some more
						eventBus.emit(Action.ROUTE_TRANSITION_ENTER, enterProps)
						setTransitionAttributes.lifecycle('on-enter')

						// cycle through the enter methods
						promise('onEnter', enterTransition, enterProps).then(() => {
							enterTransition.onAfterEnter(enterProps)
							setTransitionAttributes.lifecycle('done')

							historyManager.set('from', newState)
							eventBus.emit(Action.ROUTE_TRANSITION_COMPLETE, enterProps)
						})
					}

					return props
				})
		}
	}
})()

export default lifecycle