diff --git a/src/mobx/react/Route.js b/src/mobx/react/Route.js index dd32e4f..fad4eab 100644 --- a/src/mobx/react/Route.js +++ b/src/mobx/react/Route.js @@ -4,11 +4,15 @@ import {inject, observer} from 'mobx-react' function Route (props) { const {router, path, Component, ...otherProps} = props - return router.route.startsWith(path) ? : null + if (!router.route.startsWith(path)) { + return null + } + if (router.vmPlugin && router.vmPlugin.vmTree[path]) { + otherProps.vm = router.vmPlugin.vmTree[path] + } + return } -export default inject('router')(observer(Route)) - Route.propTypes = { Component: PropTypes.func.isRequired, router: PropTypes.shape({ @@ -17,3 +21,5 @@ Route.propTypes = { }), path: PropTypes.string.isRequired, } + +export default inject('router')(observer(Route)) diff --git a/src/plugins/vmPlugin.js b/src/plugins/vmPlugin.js new file mode 100644 index 0000000..776a1a4 --- /dev/null +++ b/src/plugins/vmPlugin.js @@ -0,0 +1,52 @@ +import {diffPaths, splitPath} from '../Router' + +/** + * @param {{router: {vmPlugin: {vmTree: *}},incomingRequest: {route: string}, currentState: {route: string}}} - beforeNav event + */ +export function vmPluginInstantiateVM ({router, incomingRequest, currentState}) { + const nodes = diffPaths(splitPath(currentState.route), splitPath(incomingRequest.route)) + nodes.forEach((node) => { + const routeConfig = router.routes[node] + if (routeConfig.vmPlugin) { + const {vmClass} = routeConfig.vmPlugin + router.vmPlugin.vmTree[node] = new vmClass(router.app.rootStore) // eslint-disable-line + } + }) +} + +/** + * @param {{router: {vmPlugin: {vmTree: *}},incomingRequest: {route: string}, currentState: {route: string}}} - afterNav event + */ +export function vmPluginCleanupVM ({router, incomingRequest, currentState}) { + const nodes = diffPaths(splitPath(incomingRequest.route), splitPath(currentState.route)) + const {vmTree} = router.vmPlugin + nodes.forEach((node) => { + if (vmTree[node] && typeof vmTree[node].destroyVM === 'function') { + vmTree[node].destroyVM() + } + }) +} + +/** + * @param {*} router + */ +function register (router) { + router.on('beforeNav', vmPluginInstantiateVM) + router.on('afterNav', vmPluginCleanupVM) + + const unregister = () => { + router.off('beforeNav', vmPluginInstantiateVM) + router.off('afterNav', vmPluginCleanupVM) + delete router.vmPlugin + } + + router.vmPlugin = { + vmTree: {}, + } + + return unregister +} + +export default { + register, +} diff --git a/src/plugins/vmPlugin.spec.js b/src/plugins/vmPlugin.spec.js new file mode 100644 index 0000000..0f1fce4 --- /dev/null +++ b/src/plugins/vmPlugin.spec.js @@ -0,0 +1,110 @@ +import vmPlugin, {vmPluginInstantiateVM, vmPluginCleanupVM} from './vmPlugin' + +class SomePageMockVM { + destroyVM = jest.fn() +} + +class OtherPageMockVM { +} + +const routes = { + '/': { + }, + '/some-page': { + vmPlugin: { + vmClass: SomePageMockVM, + }, + }, + '/some-other-page': { + vmPlugin: { + vmClass: OtherPageMockVM, + }, + }, +} + +class RouterMock { + route = '/' + params = {} + goTo = jest.fn().mockImplementation((request) => { + this.route = request.route + this.params = request.params + }) + on = jest.fn() + off = jest.fn() + routes = routes + app = { + rootStore: {} + } +} + +describe('vmPlugin', () => { + let routerMock + let unregister + + beforeEach(() => { + routerMock = new RouterMock(routes) + unregister = vmPlugin.register(routerMock) + }) + + describe('register', () => { + it('should register vmPluginInstantiateVM / vmPluginCleanupVM event listeners', () => { + // Before nav + const spyArgsBeforeNav = routerMock.on.mock.calls[0] + expect(spyArgsBeforeNav[0]).toBe('beforeNav') + expect(spyArgsBeforeNav[1]).toBe(vmPluginInstantiateVM) + // After nav + const spyArgsAfterNav = routerMock.on.mock.calls[1] + expect(spyArgsAfterNav[0]).toBe('afterNav') + expect(spyArgsAfterNav[1]).toBe(vmPluginCleanupVM) + }) + + it('should return a function to unregister the plugin', () => { + expect(unregister).toBeInstanceOf(Function) + }) + + it('should decorate the router with the vmTree', () => { + const routerMock = new RouterMock() + expect(routerMock.vmPlugin).toBeUndefined() + vmPlugin.register(routerMock) + expect(routerMock.vmPlugin.vmTree).toBeDefined() + }) + }) + + describe('unregister', () => { + it('should remove the event handlers & vmTree', () => { + expect(routerMock.vmPlugin.vmTree).toBeDefined() + unregister() + expect(routerMock.vmPlugin).toBeUndefined() + + // Before nav + const spyArgsBeforeNav = routerMock.off.mock.calls[0] + expect(spyArgsBeforeNav[0]).toBe('beforeNav') + expect(spyArgsBeforeNav[1]).toBe(vmPluginInstantiateVM) + // After nav + const spyArgsAfterNav = routerMock.off.mock.calls[1] + expect(spyArgsAfterNav[0]).toBe('afterNav') + expect(spyArgsAfterNav[1]).toBe(vmPluginCleanupVM) + }) + }) + + describe('vmPluginInstantiateVM', () => { + it('should instantiate the corresponding VMs', () => { + vmPluginInstantiateVM({ + router: routerMock, incomingRequest: {route: '/some-page'}, currentState: {route: '/'} + }) + expect(routerMock.vmPlugin.vmTree['/some-page']).toBeInstanceOf(routes['/some-page'].vmPlugin.vmClass) + }) + }) + + describe('vmPluginCleanupVM', () => { + it('should cleanup VMs', () => { + vmPluginInstantiateVM({ + router: routerMock, incomingRequest: {route: '/some-page'}, currentState: {route: '/'} + }) + vmPluginCleanupVM({ + router: routerMock, incomingRequest: {route: '/some-other-page'}, currentState: {route: '/some-page'} + }) + expect(routerMock.vmPlugin.vmTree['/some-page'].destroyVM).toHaveBeenCalled() + }) + }) +})