Skip to content

Chapter 18—Routing the application

angel edited this page Jul 20, 2024 · 2 revisions
Note
You can check the code written for this chapter in the #194 pull request.
The application object expects an options object with an optional router (app.js)
  import { destroyDOM } from './destroy-dom'
  import { h } from './h'
  import { mountDOM } from './mount-dom
++import { NoopRouter } from './router'

--export function createApp(RootComponent, props = {}) {
++export function createApp(RootComponent, props = {}, options = {}) {
    let parentEl = null
    let isMounted = false
    let vdom = null

++  const context = {
++    router: options.router || new NoopRouter(),
++  }

    // -- snip -- //

    return {
      mount(_parentEl) {
        if (isMounted) {
          throw new Error('The application is already mounted')
        }

        parentEl = _parentEl
        vdom = h(RootComponent, props)
--      mountDOM(vdom, parentEl)
++      mountDOM(vdom, parentEl, null, { appContext: context })

++      context.router.init()

        isMounted = true
      },

      unmount() {
        if (!isMounted) {
          throw new Error('The application is not mounted')
        }

        destroyDOM(vdom)
++      context.router.destroy()
        reset()
      },
    }
  }
Saving the app context in the component (component.js)
  export function defineComponent({
    render,
    state,
    onMounted = emptyFn,
    onUnmounted = emptyFn,
    ...methods
  }) {
    class Component {
      #isMounted = false
      #vdom = null
      #hostEl = null
      #eventHandlers = null
      #parentComponent = null
      #dispatcher = new Dispatcher()
      #subscriptions = []
++    #appContext = null

      constructor(props = {}, eventHandlers = {}, parentComponent = null) {
        this.props = props
        this.state = state ? state(props) : {}
        this.#eventHandlers = eventHandlers
        this.#parentComponent = parentComponent
      }
      onMounted() {
        return Promise.resolve(onMounted.call(this))
      }
      onUnmounted() {
        return Promise.resolve(onUnmounted.call(this))
      }

++    setAppContext(appContext) {
++      this.#appContext = appContext
++    }

++    get appContext() {
++      return this.#appContext
++    }

      // -- snip -- //
    }
    // -- snip -- //
  }
Setting the application context to each newly created component (mount-dom.js)
  function createComponentNode(vdom, parentEl, index, hostComponent) {
    const { tag: Component, children } = vdom
    const { props, events } = extractPropsAndEvents(vdom)
    const component = new Component(props, events, hostComponent)
    component.setExternalContent(children)
++  component.setAppContext(hostComponent?.appContext ?? {})

    component.mount(parentEl, index)
    vdom.component = component
    vdom.el = component.firstElement
  }

Router Components

The router link (router-components.js)
import { defineComponent } from './component'
import { h, hSlot } from './h'

export const RouterLink = defineComponent({
  render() {
    const { to } = this.props

    return h(
      'a',
      {
        href: to,
        on: {
          click: (e) => {
            e.preventDefault()
            this.appContext.router.navigateTo(to)
          },
        },
      },
      [hSlot()]
    )
  },
})
The router outlet (router-components.js)
export const RouterOutlet = defineComponent({
  state() {
    return {
      matchedRoute: null,
      subscription: null,
    }
  },

  onMounted() {
    const subscription = this.appContext.router.subscribe(({ to }) => {
      this.handleRouteChange(to)
    })

    this.updateState({ subscription })
  },

  onUnmounted() {
    const { subscription } = this.state
    this.appContext.router.unsubscribe(subscription)
  },

  handleRouteChange(matchedRoute) {
    this.updateState({ matchedRoute })
  },

  render() {
    const { matchedRoute } = this.state

    return h('div', { id: 'router-outlet' }, [
      matchedRoute ? h(matchedRoute.component) : null,
    ])
  },
})

Exporting the routing components

Exporting the router components (index.js)
  export { createApp } from './app.js'
  export { defineComponent } from './component.js'
  export { DOM_TYPES, h, hFragment, hSlot, hString } from './h.js'
++export { RouterLink, RouterOutlet } from './router-components.js'
++export { HashRouter } from './router.js'
  export { nextTick } from './scheduler.js'