From 536fe8e50579c2e8689a781a68da6625c4b8e53b Mon Sep 17 00:00:00 2001 From: Robert-Frampton Date: Tue, 5 Dec 2017 14:48:59 -0800 Subject: [PATCH] Adds support for nested Router components. Closes #14 --- src/Router.js | 39 ++++++++++++- test/Router.js | 148 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 1 deletion(-) diff --git a/src/Router.js b/src/Router.js index 996d611..6784f75 100644 --- a/src/Router.js +++ b/src/Router.js @@ -1,6 +1,6 @@ 'use strict'; -import {core, getFunctionName, object} from 'metal'; +import {core, getFunctionName, isString, object} from 'metal'; import {App, RequestScreen, Route} from 'senna'; import CancellablePromise from 'metal-promise'; import {Component, ComponentRegistry} from 'metal-component'; @@ -26,6 +26,43 @@ class Router extends Component { // anything. It will be set back in `attached`. this.firstRenderElement = this.element; this.element = null; + + this.createChildRouters_(); + } + + /** + * Joins parent path and child path to create nested Router. + * @param {!Object} child + * @protected + */ + createChildRouter_(child) { + if (child.tag !== Router) { + throw new TypeError( + 'Router can only receive additional Routers as children.' + ); + } + + const {config} = child; + + if (!isString(this.path) || !isString(config.path)) { + throw new TypeError( + 'When nesting Routers, both parent and child path values must be strings.' + ); + } + + config.path = Uri.joinPaths(this.path, config.path); + + new child.tag(config); // eslint-disable-line + } + + /** + * Loops through children Routers and invokes them. + * @protected + */ + createChildRouters_() { + if (this.children.length) { + this.children.forEach(this.createChildRouter_.bind(this)); + } } /** diff --git a/test/Router.js b/test/Router.js index e101655..bda77af 100644 --- a/test/Router.js +++ b/test/Router.js @@ -13,6 +13,7 @@ import RouterSoy from '../src/Router'; const defaultScreen = Router.defaultScreen; describe('Router', function() { + let comp; let router; let router2; @@ -27,6 +28,9 @@ describe('Router', function() { } Router.activeRouter = null; Router.defaultScreen = defaultScreen; + if (comp) { + comp.dispose(); + } if (router) { router.dispose(); } @@ -993,6 +997,150 @@ describe('Router', function() { done(); }); }); + + it('should create nested routes from IncrementalDOM calls', function() { + class FirstComponent {} + class SecondComponent {} + class ThirdComponent {} + + class ParentComponent extends Component { + render() { + IncrementalDOM.elementOpen( + Router, + null, + null, + 'component', + FirstComponent, + 'path', + '/path' + ); + IncrementalDOM.elementOpen( + Router, + null, + null, + 'component', + SecondComponent, + 'path', + '/first' + ); + IncrementalDOM.elementVoid( + Router, + null, + null, + 'component', + ThirdComponent, + 'path', + '/second' + ); + IncrementalDOM.elementClose(Router); + IncrementalDOM.elementClose(Router); + } + } + ParentComponent.RENDERER = IncrementalDomRenderer; + + comp = new ParentComponent(); + + const {routes} = Router.router(); + + assert.equal(routes.length, 3); + assert.equal(routes[0].path, '/path'); + assert.equal(routes[1].path, '/path/first'); + assert.equal(routes[2].path, '/path/first/second'); + assert.deepEqual(routes[0].router.component, FirstComponent); + assert.deepEqual(routes[1].router.component, SecondComponent); + assert.deepEqual(routes[2].router.component, ThirdComponent); + }); + + it('should throw error if nested Router does not pass path that is a string', function() { + class FirstComponent {} + + class ParentComponent extends Component { + render() { + IncrementalDOM.elementOpen( + Router, + null, + null, + 'component', + FirstComponent, + 'path', + '/path' + ); + IncrementalDOM.elementVoid( + Router, + null, + null, + 'component', + FirstComponent, + 'path', + /\/first/ + ); + IncrementalDOM.elementClose(Router); + } + } + ParentComponent.RENDERER = IncrementalDomRenderer; + + assert.throws(() => { + comp = new ParentComponent(); + }, 'When nesting Routers, both parent and child path values must be strings.'); + }); + + it('should throw error if parent Router does not have path that is a string', function() { + class FirstComponent {} + + class ParentComponent extends Component { + render() { + IncrementalDOM.elementOpen( + Router, + null, + null, + 'component', + FirstComponent, + 'path', + /\/first/ + ); + IncrementalDOM.elementVoid( + Router, + null, + null, + 'component', + FirstComponent, + 'path', + '/first' + ); + IncrementalDOM.elementClose(Router); + } + } + ParentComponent.RENDERER = IncrementalDomRenderer; + + assert.throws(() => { + comp = new ParentComponent(); + }, 'When nesting Routers, both parent and child path values must be strings.'); + }); + + it('should throw error if nested component is not an instance of Router', function() { + class FirstComponent {} + + class ParentComponent extends Component { + render() { + IncrementalDOM.elementOpen( + Router, + null, + null, + 'component', + FirstComponent, + 'path', + '/path' + ); + IncrementalDOM.elementVoid(FirstComponent); + IncrementalDOM.elementClose(Router); + } + } + ParentComponent.RENDERER = IncrementalDomRenderer; + + assert.throws(() => { + comp = new ParentComponent(); + }, 'Router can only receive additional Routers as children.'); + }); }); describe('RouterSoy', function() {