diff --git a/README.md b/README.md
index 248e98d0..531c253f 100644
--- a/README.md
+++ b/README.md
@@ -42,24 +42,25 @@ Find more examples [here](https://github.com/Travix-International/frint/tree/mas
The framework is a collection of these packages, which can be composed together on demand:
-| Package | Status | Description |
-|-----------------------------|------------------------------------------------------------------------|-------------|
-| [frint] | [![frint-status]][frint-package] | Base for creating Apps |
-| [frint-store] | [![frint-store-status]][frint-store-package] | State management with reactive stores |
-| [frint-data] | [![frint-data-status]][frint-data-package] | Reactive data modelling |
-| [frint-react] | [![frint-react-status]][frint-react-package] | React.js integration |
-| [frint-react-server] | [![frint-react-server-status]][frint-react-server-package] | Server-side rendering of Apps |
-| [frint-router] | [![frint-router-status]][frint-router-package] | Router services for building Single Page Applications |
-| [frint-router-react] | [![frint-router-react-status]][frint-router-react-package] | React components for building SPAs |
-| [frint-cli] | [![frint-cli-status]][frint-cli-package] | CLI runner |
-| [frint-compat] | [![frint-compat-status]][frint-compat-package] | Backwards compatibility for older versions |
-| [frint-model] | [![frint-model-status]][frint-model-package] | Use `frint-data` instead |
-| **For library developers:** | | |
-| [frint-component-utils] | [![frint-component-utils-status]][frint-component-utils-package] | Utils for reactive Components |
-| [frint-component-handlers] | [![frint-component-handlers-status]][frint-component-handlers-package] | Handlers for integrating with other rendering libraries |
-| **Internally used:** | | |
-| [frint-test-utils] | [![frint-test-utils-status]][frint-test-utils-package] | Internally used test utilities |
-| [frint-config] | [![frint-config-status]][frint-config-package] | Common config for your Apps |
+| Package | Status | Description |
+|------------------------------------|--------------------------------------------------------------------------------------|-------------|
+| [frint] | [![frint-status]][frint-package] | Base for creating Apps |
+| [frint-store] | [![frint-store-status]][frint-store-package] | State management with reactive stores |
+| [frint-data] | [![frint-data-status]][frint-data-package] | Reactive data modelling |
+| [frint-react] | [![frint-react-status]][frint-react-package] | React.js integration |
+| [frint-react-server] | [![frint-react-server-status]][frint-react-server-package] | Server-side rendering of Apps |
+| [frint-router] | [![frint-router-status]][frint-router-package] | Router services for building Single Page Applications |
+| [frint-router-react] | [![frint-router-react-status]][frint-router-react-package] | React components for building SPAs |
+| [frint-cli] | [![frint-cli-status]][frint-cli-package] | CLI runner |
+| [frint-compat] | [![frint-compat-status]][frint-compat-package] | Backwards compatibility for older versions |
+| [frint-model] | [![frint-model-status]][frint-model-package] | Use `frint-data` instead |
+| **For library developers:** | | |
+| [frint-component-utils] | [![frint-component-utils-status]][frint-component-utils-package] | Utils for reactive Components |
+| [frint-component-handlers] | [![frint-component-handlers-status]][frint-component-handlers-package] | Handlers for integrating with other rendering libraries |
+| [frint-router-component-handlers] | [![frint-router-component-handlers-status]][frint-router-component-handlers-package] | Handlers for integrating `frint-router` with other rendering libraries |
+| **Internally used:** | | |
+| [frint-test-utils] | [![frint-test-utils-status]][frint-test-utils-package] | Internally used test utilities |
+| [frint-config] | [![frint-config-status]][frint-config-package] | Common config for your Apps |
[frint]: https://frint.js.org/docs/packages/frint
[frint-store]: https://frint.js.org/docs/packages/frint-store
@@ -73,6 +74,7 @@ The framework is a collection of these packages, which can be composed together
[frint-compat]: https://frint.js.org/docs/packages/frint-compat
[frint-component-utils]: https://frint.js.org/docs/packages/frint-component-utils
[frint-component-handlers]: https://frint.js.org/docs/packages/frint-component-handlers
+[frint-router-component-handlers]: https://frint.js.org/docs/packages/frint-router-component-handlers
[frint-test-utils]: https://frint.js.org/docs/packages/frint-test-utils
[frint-config]: https://frint.js.org/docs/packages/frint-config
@@ -88,6 +90,7 @@ The framework is a collection of these packages, which can be composed together
[frint-compat-status]: https://img.shields.io/npm/v/frint-compat.svg
[frint-component-utils-status]: https://img.shields.io/npm/v/frint-component-utils.svg
[frint-component-handlers-status]: https://img.shields.io/npm/v/frint-component-handlers.svg
+[frint-router-component-handlers-status]: https://img.shields.io/npm/v/frint-router-component-handlers.svg
[frint-test-utils-status]: https://img.shields.io/npm/v/frint-test-utils.svg
[frint-config-status]: https://img.shields.io/npm/v/frint-config.svg
@@ -103,6 +106,7 @@ The framework is a collection of these packages, which can be composed together
[frint-compat-package]: https://npmjs.com/package/frint-compat
[frint-component-utils-package]: https://npmjs.com/package/frint-component-utils
[frint-component-handlers-package]: https://npmjs.com/package/frint-component-handlers
+[frint-router-component-handlers-package]: https://npmjs.com/package/frint-router-component-handlers
[frint-test-utils-package]: https://npmjs.com/package/frint-test-utils
[frint-config-package]: https://npmjs.com/package/frint-config
diff --git a/packages/frint-router-component-handlers/.gitignore b/packages/frint-router-component-handlers/.gitignore
new file mode 100644
index 00000000..5f985010
--- /dev/null
+++ b/packages/frint-router-component-handlers/.gitignore
@@ -0,0 +1 @@
+dist/*.js
diff --git a/packages/frint-router-component-handlers/.npmignore b/packages/frint-router-component-handlers/.npmignore
new file mode 100644
index 00000000..a2cfb9e4
--- /dev/null
+++ b/packages/frint-router-component-handlers/.npmignore
@@ -0,0 +1,5 @@
+src
+test
+coverage
+node_modules
+.nyc_output
diff --git a/packages/frint-router-component-handlers/README.md b/packages/frint-router-component-handlers/README.md
new file mode 100644
index 00000000..122692cb
--- /dev/null
+++ b/packages/frint-router-component-handlers/README.md
@@ -0,0 +1,73 @@
+# frint-router-component-handlers
+
+[![npm](https://img.shields.io/npm/v/frint-router-component-handlers.svg)](https://www.npmjs.com/package/frint-router-component-handlers)
+
+> Router component handlers package of Frint
+
+
+
+- [Guide](#guide)
+ - [Installation](#installation)
+- [API](#api)
+ - [createLinkHandler](#createlinkhandler)
+ - [createRouteHandler](#createroutehandler)
+ - [createSwitchHandler](#createswitchhandler)
+
+
+
+---
+
+# Guide
+
+This package is aimed at enabling other reactive rendering/templating libraries integrate with FrintJS router, and not to be used directly by developers in their applications.
+
+For example, take a look at `frint-router-react` for its implementation using this package internally.
+
+## Installation
+
+With [npm](https://www.npmjs.com/):
+
+```
+$ npm install --save frint-router-component-handlers
+```
+
+# API
+
+
+## createLinkHandler
+
+> createLinkHandler(ComponentHandler, app, component)
+
+Method for creating handler for `Link` component.
+
+#### Arguments
+
+1. `ComponentHandler` (`Object`): framework specific component handler implementing common API to work with `component`
+2. `app` (`Object`): app instance
+3. `component` (`Object`): `Link` component instance
+
+
+## createRouteHandler
+
+> createRouteHandler(ComponentHandler, app, component)
+
+Method for creating handler for `Route` component.
+
+#### Arguments
+
+1. `ComponentHandler` (`Object`): framework specific component handler implementing common API to work with `component`
+2. `app` (`Object`): app instance
+3. `component` (`Object`): `Route` component instance
+
+
+## createSwitchHandler
+
+> createSwitchHandler(ComponentHandler, app, component)
+
+Method for creating handler for `Switch` component.
+
+#### Arguments
+
+1. `ComponentHandler` (`Object`): framework specific component handler implementing common API to work with `component`
+2. `app` (`Object`): app instance
+3. `component` (`Object`): `Switch` component instance
diff --git a/packages/frint-router-component-handlers/package.json b/packages/frint-router-component-handlers/package.json
new file mode 100644
index 00000000..9b289db8
--- /dev/null
+++ b/packages/frint-router-component-handlers/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "frint-router-component-handlers",
+ "version": "3.0.1",
+ "description": "Router component handlers package for Frint",
+ "main": "lib/index.js",
+ "homepage": "https://github.com/Travix-International/frint/tree/master/packages/frint-router-component-handlers",
+ "scripts": {
+ "lint": "cross-env ../../node_modules/.bin/eslint --color '{src,test}/**/*.js'",
+ "transpile": "cross-env ../../node_modules/.bin/babel src --out-dir lib",
+ "test": "cross-env ../../node_modules/.bin/mocha --colors --compilers js:babel-register --recursive ./src/**/*.spec.js",
+ "cover:run": "cross-env ../../node_modules/.bin/nyc --reporter=json --require babel-register ../../node_modules/.bin/mocha --colors --compilers js:babel-register --recursive ./src/**/*.spec.js",
+ "cover:report": "cross-env ../../node_modules/.bin/nyc report",
+ "cover": "npm run cover:run && npm run cover:report",
+ "dist:lib": "cross-env ../../node_modules/.bin/webpack --config ./webpack.config.js",
+ "dist:min": "cross-env DIST_MIN=1 ../../node_modules/.bin/webpack --config ./webpack.config.js",
+ "dist": "npm run dist:lib && npm run dist:min",
+ "prepublish": "npm run transpile"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Travix-International/frint.git"
+ },
+ "author": {
+ "name": "Travix International B.V.",
+ "url": "https://travix.com"
+ },
+ "keywords": [
+ "frint"
+ ],
+ "dependencies": {
+ "frint-component-utils": "^3.0.1"
+ },
+ "devDependencies": {
+ "cross-env": "^5.0.5",
+ "frint-router": "^3.0.1",
+ "frint": "^3.0.1"
+ },
+ "bugs": {
+ "url": "https://github.com/Travix-International/frint/issues"
+ },
+ "license": "MIT"
+}
diff --git a/packages/frint-router-component-handlers/src/createLinkHandler.js b/packages/frint-router-component-handlers/src/createLinkHandler.js
new file mode 100644
index 00000000..ed3d687a
--- /dev/null
+++ b/packages/frint-router-component-handlers/src/createLinkHandler.js
@@ -0,0 +1,74 @@
+import { composeHandlers } from 'frint-component-utils';
+
+const LinkHandler = {
+ _routerSubscription: null,
+
+ getInitialData() {
+ return {
+ active: false,
+ };
+ },
+
+ beforeMount() {
+ this._resubscribeToRouter(this.getProps());
+ },
+
+ propsChange(nextProps, toChanged, exactChanged) {
+ this._resubscribeToRouter(nextProps, toChanged, exactChanged);
+ },
+
+ beforeDestroy() {
+ this._unsubscribeFromRouter();
+ },
+
+ handleClick() {
+ const router = this._getRouter();
+ const to = this.getProp('to');
+
+ if (router.getMatch(to, router.getHistory(), { exact: true }) === null) {
+ router.push(to);
+ }
+ },
+
+ _getRouter() {
+ return this.app.get('router');
+ },
+
+ _resubscribeToRouter(nextProps, toChanged = false, exactChanged = false) {
+ const { activeClassName, to, exact } = nextProps;
+
+ this._unsubscribeFromRouter();
+
+ if (typeof activeClassName === 'string') {
+ if (!this._routerSubscription || toChanged || exactChanged) {
+ this._routerSubscription = this._getRouter()
+ .getMatch$(to, { exact })
+ .subscribe((matched) => {
+ if (!matched) {
+ return this.setData('active', false);
+ }
+
+ return this.setData('active', true);
+ });
+ }
+ }
+ },
+
+ _unsubscribeFromRouter() {
+ if (this._routerSubscription) {
+ this._routerSubscription.unsubscribe();
+ this._routerSubscription = null;
+ }
+ },
+};
+
+export default function createLinkHandler(ComponentHandler, app, component) {
+ return composeHandlers(
+ LinkHandler,
+ ComponentHandler,
+ {
+ app,
+ component
+ },
+ );
+}
diff --git a/packages/frint-router-component-handlers/src/createLinkHandler.spec.js b/packages/frint-router-component-handlers/src/createLinkHandler.spec.js
new file mode 100644
index 00000000..481e1a38
--- /dev/null
+++ b/packages/frint-router-component-handlers/src/createLinkHandler.spec.js
@@ -0,0 +1,74 @@
+/* eslint-disable import/no-extraneous-dependencies, func-names, no-unused-expressions */
+/* global describe, it */
+import { expect } from 'chai';
+
+import { MemoryRouterService } from 'frint-router';
+import createLinkHandler from './createLinkHandler';
+import { ComponentHandler, createTestAppInstance, createComponent } from './testHelpers';
+
+describe('frint-router-component-handlers › createLinkHandler', function () {
+ it('creates a handler with getInitialData, beforeMount, propsChange, handleClick, beforeDestroy methods', function () {
+ const handler = createLinkHandler(
+ ComponentHandler,
+ createTestAppInstance(new MemoryRouterService()),
+ createComponent()
+ );
+
+ expect(handler).to.include.all.keys('getInitialData', 'beforeMount', 'propsChange', 'handleClick', 'beforeDestroy');
+ });
+
+ describe('LinkHandler functions properly throughout lifecycle', function () {
+ const router = new MemoryRouterService();
+
+ const component = createComponent();
+ component.props.to = '/about';
+ component.props.exact = false;
+ component.props.activeClassName = 'active-link';
+
+ const handler = createLinkHandler(
+ ComponentHandler,
+ createTestAppInstance(router),
+ component
+ );
+
+ it('when getInitialData() called, it returns object with active key', function () {
+ component.data = handler.getInitialData();
+ expect(component.data).to.have.all.keys({ active: false });
+ });
+
+ it('when beforeMount() called, subscribes to router and changes active data accordingly', function () {
+ handler.beforeMount();
+ expect(component.data.active).to.be.false;
+
+ router.push('/about');
+
+ expect(component.data.active).to.be.true;
+ });
+
+ it('when props change and propsChange() called, it resubscribes to router and changes active data accordingly', function () {
+ component.props.to = '/contact';
+ handler.propsChange(component.props, true, false);
+
+ expect(component.data.active).to.be.false;
+
+ router.push('/contact/us');
+ expect(component.data.active).to.be.true;
+ });
+
+ it('when handleClick() is called, it changes router url', function () {
+ expect(router.getLocation().pathname).to.equal('/contact/us');
+
+ handler.handleClick();
+
+ expect(router.getLocation().pathname).to.equal('/contact');
+ });
+
+ it('when beforeDestroy() is called, unsubscribes from router and doesn\'t change active data anymore', function () {
+ expect(component.data.active).to.be.true;
+
+ handler.beforeDestroy();
+ router.push('/random');
+ expect(component.data.active).to.be.true;
+ });
+ });
+});
diff --git a/packages/frint-router-component-handlers/src/createRouteHandler.js b/packages/frint-router-component-handlers/src/createRouteHandler.js
new file mode 100644
index 00000000..0b190a73
--- /dev/null
+++ b/packages/frint-router-component-handlers/src/createRouteHandler.js
@@ -0,0 +1,102 @@
+import { composeHandlers } from 'frint-component-utils';
+
+const RouteHandler = {
+ _routerSubscription: null,
+ _appInstance: null,
+
+ getInitialData() {
+ return {
+ component: null,
+ matched: null,
+ };
+ },
+
+ beforeMount() {
+ const props = this.getProps();
+ this._calculateMatchedState(props);
+ this._calculateComponentState(props);
+ },
+
+ propsChange(nextProps, pathChanged, exactChanged, appChanged) {
+ this._calculateMatchedState(nextProps, pathChanged, exactChanged);
+ this._calculateComponentState(nextProps, appChanged);
+ },
+
+ beforeDestroy() {
+ this._unsubscribeFromRouter();
+ this._destroyRouteApp();
+ },
+
+ _getRouter() {
+ return this.app.get('router');
+ },
+
+ _calculateMatchedState(nextProps, pathChanged = false, exactChanged = false) {
+ if (nextProps.computedMatch) {
+ // in case it was subscribed before
+ this._unsubscribeFromRouter();
+ } else if (nextProps.path) {
+ if (!this._routerSubscription || pathChanged || exactChanged) {
+ this._unsubscribeFromRouter();
+
+ this._routerSubscription = this._getRouter()
+ .getMatch$(nextProps.path, {
+ exact: nextProps.exact,
+ })
+ .subscribe((matched) => {
+ this.setData('matched', matched);
+ });
+ }
+ }
+ },
+
+ _calculateComponentState(nextProps, appChanged = false) {
+ if (nextProps.component) {
+ // component
+ this._destroyRouteApp();
+
+ this.setData('component', nextProps.component);
+ } else if (nextProps.app && (this._appInstance === null || appChanged)) {
+ // app
+ this._destroyRouteApp();
+
+ const RouteApp = nextProps.app;
+
+ this._appInstance = new RouteApp({
+ parentApp: this.app,
+ });
+
+ this.setData('component', this.getMountableComponent(this._appInstance));
+ }
+ },
+
+ _unsubscribeFromRouter() {
+ if (this._routerSubscription) {
+ this._routerSubscription.unsubscribe();
+ this._routerSubscription = null;
+ }
+ },
+
+ _destroyRouteApp() {
+ if (this._appInstance) {
+ this._appInstance.beforeDestroy();
+ this._appInstance = null;
+ }
+ }
+};
+
+export default function createRouteHandler(ComponentHandler, app, component) {
+ if (typeof ComponentHandler.getMountableComponent !== 'function') {
+ throw new Error('ComponentHandler must provide getMountableComponent() method');
+ }
+
+ return composeHandlers(
+ RouteHandler,
+ ComponentHandler,
+ {
+ app,
+ component
+ },
+ );
+}
+
diff --git a/packages/frint-router-component-handlers/src/createRouteHandler.spec.js b/packages/frint-router-component-handlers/src/createRouteHandler.spec.js
new file mode 100644
index 00000000..5eb47b86
--- /dev/null
+++ b/packages/frint-router-component-handlers/src/createRouteHandler.spec.js
@@ -0,0 +1,142 @@
+/* eslint-disable import/no-extraneous-dependencies, func-names, no-unused-expressions */
+/* global describe, it */
+import { expect } from 'chai';
+
+import { MemoryRouterService } from 'frint-router';
+import createRouteHandler from './createRouteHandler';
+import { ComponentHandler, createTestAppInstance, createTestApp, createComponent } from './testHelpers';
+
+describe('frint-router-component-handlers › createRouteHandler', function () {
+ it('creates a handler with getInitialData, beforeMount, propsChange, beforeDestroy methods', function () {
+ const handler = createRouteHandler(
+ ComponentHandler,
+ createTestAppInstance(new MemoryRouterService()),
+ createComponent()
+ );
+
+ expect(handler).to.include.all.keys('getInitialData', 'beforeMount', 'propsChange', 'beforeDestroy');
+ });
+
+ it('throws exception when ComponentHandler doesn\'t have getMountableComponent() method', function () {
+ const invalidCreation = () => {
+ createRouteHandler(
+ {},
+ createTestAppInstance(new MemoryRouterService()),
+ createComponent()
+ );
+ };
+
+ expect(invalidCreation).to.throw(Error, 'ComponentHandler must provide getMountableComponent() method');
+ });
+
+ describe('RouteHandler functions properly throughout lifecycle', function () {
+ const router = new MemoryRouterService();
+
+ const component = createComponent();
+ const componentToRender = createComponent();
+ component.props = {
+ path: '/products',
+ exact: true,
+ component: componentToRender
+ };
+
+ const handler = createRouteHandler(
+ ComponentHandler,
+ createTestAppInstance(router),
+ component
+ );
+
+ it('initially component and matched data are set to null', function () {
+ component.data = handler.getInitialData();
+
+ expect(component.data.component).to.be.null;
+ expect(component.data.matched).to.be.null;
+ });
+
+ it('when beforeMount() is called it subscribes to router and changes matched and component accordingly', function () {
+ handler.beforeMount();
+
+ expect(component.data.matched).to.be.null;
+ expect(component.data.component).to.equal(componentToRender);
+
+ router.push('/products');
+
+ expect(component.data.matched).to.be.an('object');
+ });
+
+ it('when component and app props changed and propsChange() is called, component data is set to app\'s component', function () {
+ const appComponent = createComponent();
+ component.props.component = undefined;
+
+ component.props.app = createTestApp(undefined, appComponent);
+
+ handler.propsChange(component.props, false, false, true);
+
+ expect(component.data.component).to.equal(appComponent);
+ });
+
+ it('when path prop change and propsChange() is called, matched data is changed', function () {
+ expect(component.data.matched).to.be.an('object');
+
+ component.props.path = '/contact';
+ handler.propsChange(component.props, true, false, false);
+
+ expect(component.data.matched).to.be.null;
+ });
+
+ it('when beforeDestroy() is called, it unsubscribes from router and matched no longer changes', function () {
+ expect(component.data.matched).to.be.null;
+
+ handler.beforeDestroy();
+ router.push('/contact');
+
+ expect(component.data.matched).to.be.null;
+ });
+ });
+
+ describe('RouteHandler functions properly with computedMatch throughout lifecycle', function () {
+ const router = new MemoryRouterService();
+
+ const component = createComponent();
+ const componentToRender = createComponent();
+ const computedMatch = {};
+
+ component.props = {
+ path: '/products',
+ exact: true,
+ computedMatch,
+ component: componentToRender
+ };
+
+ const handler = createRouteHandler(
+ ComponentHandler,
+ createTestAppInstance(router),
+ component
+ );
+
+ router.push('/');
+
+ it('initially matched data is set to null', function () {
+ component.data = handler.getInitialData();
+
+ expect(component.data.matched).to.be.null;
+ });
+
+ it('when beforeMount() is called it doesn\'t subscribe to router and therefore matched data is not affected', function () {
+ expect(component.data.matched).to.be.null;
+
+ handler.beforeMount();
+
+ router.push('/products');
+
+ expect(component.data.matched).to.be.null;
+ });
+
+ it('when computedMatch prop is removed and propChanged() is called it subscribes to router and therefore matched data is affected', function () {
+ component.props.computedMatch = null;
+ handler.propsChange(component.props, false, false, false);
+
+ expect(component.data.matched).to.be.an('object');
+ });
+ });
+});
diff --git a/packages/frint-router-component-handlers/src/createSwitchHandler.js b/packages/frint-router-component-handlers/src/createSwitchHandler.js
new file mode 100644
index 00000000..24268415
--- /dev/null
+++ b/packages/frint-router-component-handlers/src/createSwitchHandler.js
@@ -0,0 +1,54 @@
+import { composeHandlers } from 'frint-component-utils';
+
+const SwitchHandler = {
+ _routerSubscription: null,
+
+ getInitialData() {
+ return {
+ history: null
+ };
+ },
+
+ beforeMount() {
+ this._subscribeToRouter();
+ },
+
+ beforeDestroy() {
+ this._unsubscribeFromRouter();
+ },
+
+ getMatch(path, exact) {
+ return this._getRouter()
+ .getMatch(path, this.getData('history'), { exact });
+ },
+
+ _getRouter() {
+ return this.app.get('router');
+ },
+
+ _subscribeToRouter() {
+ this._routerSubscription = this._getRouter()
+ .getHistory$()
+ .subscribe((history) => {
+ this.setData('history', history);
+ });
+ },
+
+ _unsubscribeFromRouter() {
+ if (this._routerSubscription) {
+ this._routerSubscription.unsubscribe();
+ this._routerSubscription = null;
+ }
+ },
+};
+
+export default function createSwitchHandler(ComponentHandler, app, component) {
+ return composeHandlers(
+ SwitchHandler,
+ ComponentHandler,
+ {
+ app,
+ component
+ },
+ );
+}
diff --git a/packages/frint-router-component-handlers/src/createSwitchHandler.spec.js b/packages/frint-router-component-handlers/src/createSwitchHandler.spec.js
new file mode 100644
index 00000000..e1060150
--- /dev/null
+++ b/packages/frint-router-component-handlers/src/createSwitchHandler.spec.js
@@ -0,0 +1,81 @@
+/* eslint-disable import/no-extraneous-dependencies, func-names, no-unused-expressions */
+/* global describe, it */
+import { expect } from 'chai';
+
+import { MemoryRouterService } from 'frint-router';
+import createSwitchHandler from './createSwitchHandler';
+import { ComponentHandler, createTestAppInstance, createComponent } from './testHelpers';
+
+describe('frint-router-component-handlers › createSwitchHandler', function () {
+ it('creates a handler with getInitialData, beforeMount, beforeDestroy, getMatch methods', function () {
+ const handler = createSwitchHandler(
+ ComponentHandler,
+ createTestAppInstance(new MemoryRouterService()),
+ createComponent()
+ );
+
+ expect(handler).to.include.all.keys('getInitialData', 'beforeMount', 'beforeDestroy', 'getMatch');
+ });
+
+ it('when getInitialData is called, it returns object with history key', function () {
+ const handler = createSwitchHandler(
+ ComponentHandler,
+ createTestAppInstance(new MemoryRouterService()),
+ createComponent()
+ );
+
+ const data = handler.getInitialData();
+ expect(data).to.be.an('object');
+ expect(data).to.have.all.keys('history');
+ });
+
+ describe('SwitchHandler functions properly throughout lifecycle', function () {
+ const router = new MemoryRouterService();
+ const component = createComponent();
+
+ const handler = createSwitchHandler(
+ ComponentHandler,
+ createTestAppInstance(router),
+ component
+ );
+
+ component.data = handler.getInitialData();
+
+ it('initially history is set to null', function () {
+ expect(component.data.history).to.be.null;
+ });
+
+ it('when beforeMount is called, it subscribes to the router and changes history data whenever it changes in router', function () {
+ handler.beforeMount();
+
+ const history1 = component.data.history;
+ expect(history1).to.not.be.null;
+
+ router.push('/contact');
+
+ const history2 = component.data.history;
+ expect(history2).to.not.be.null;
+ expect(history2).to.not.equal(history1);
+ });
+
+ it('when getMatch is called, it matches history correctly from the router', function () {
+ router.push('/about');
+
+ expect(handler.getMatch('/', true)).to.be.null;
+ expect(handler.getMatch('/', false)).to.be.an('object');
+ expect(handler.getMatch('/about', true)).to.be.an('object');
+ expect(handler.getMatch('/about', false)).to.be.an('object');
+ });
+
+ it('when beforeDestroy() is called, it unsubscribes from router and history data no longer changes', function () {
+ handler.beforeDestroy();
+
+ const history1 = component.data.history;
+
+ router.push('/destroyed');
+
+ const history2 = component.data.history;
+ expect(history2).to.equal(history1);
+ });
+ });
+});
diff --git a/packages/frint-router-component-handlers/src/index.js b/packages/frint-router-component-handlers/src/index.js
new file mode 100644
index 00000000..112d00cb
--- /dev/null
+++ b/packages/frint-router-component-handlers/src/index.js
@@ -0,0 +1,9 @@
+import createLinkHandler from './createLinkHandler';
+import createRouteHandler from './createRouteHandler';
+import createSwitchHandler from './createSwitchHandler';
+
+export default {
+ createLinkHandler,
+ createRouteHandler,
+ createSwitchHandler
+};
diff --git a/packages/frint-router-component-handlers/src/index.spec.js b/packages/frint-router-component-handlers/src/index.spec.js
new file mode 100644
index 00000000..c8430c0d
--- /dev/null
+++ b/packages/frint-router-component-handlers/src/index.spec.js
@@ -0,0 +1,12 @@
+/* eslint-disable import/no-extraneous-dependencies, func-names */
+/* global describe, it */
+import { expect } from 'chai';
+
+import index from './index';
+
+describe('frint-router-component-handlers › index', function () {
+ it('exports an object with createLinkHandler, createRouteHandler, createSwitchHandler keys', function () {
+ expect(index).to.be.an('object');
+ expect(index).to.have.all.keys('createLinkHandler', 'createRouteHandler', 'createSwitchHandler');
+ });
+});
diff --git a/packages/frint-router-component-handlers/src/testHelpers.js b/packages/frint-router-component-handlers/src/testHelpers.js
new file mode 100644
index 00000000..9323149c
--- /dev/null
+++ b/packages/frint-router-component-handlers/src/testHelpers.js
@@ -0,0 +1,48 @@
+import { createApp } from 'frint'; // eslint-disable-line import/no-extraneous-dependencies
+
+export const ComponentHandler = {
+ setData(key, value) {
+ this.component.data[key] = value;
+ },
+ getData(key) {
+ return this.component.data[key];
+ },
+ getProp(key) {
+ return this.component.props[key];
+ },
+ getProps() {
+ return this.component.props;
+ },
+ getMountableComponent(app) {
+ return app.get('component');
+ }
+};
+
+export function createTestApp(router, component) {
+ const providers = [];
+
+ if (router) {
+ providers.push({ name: 'router', useValue: router });
+ }
+
+ if (component) {
+ providers.push({ name: 'component', useValue: component });
+ }
+
+ return createApp({
+ name: 'TestApp',
+ providers
+ });
+}
+
+export function createTestAppInstance(router, component) {
+ const AppClass = createTestApp(router, component);
+ return new AppClass();
+}
+
+export function createComponent() {
+ return {
+ data: {},
+ props: {}
+ };
+}
diff --git a/packages/frint-router-component-handlers/webpack.config.js b/packages/frint-router-component-handlers/webpack.config.js
new file mode 100644
index 00000000..ab5caccc
--- /dev/null
+++ b/packages/frint-router-component-handlers/webpack.config.js
@@ -0,0 +1,46 @@
+var webpack = require('webpack');
+
+var minify = process.env.DIST_MIN;
+var plugins = !minify
+ ? []
+ : [
+ new webpack.optimize.UglifyJsPlugin({
+ compress: {
+ warnings: false,
+ drop_console: false
+ }
+ })
+ ];
+var filename = !minify
+ ? 'frint-router-component-handlers.js'
+ : 'frint-router-component-handlers.min.js';
+
+module.exports = {
+ entry: __dirname + '/src',
+ output: {
+ path: __dirname + '/dist',
+ filename: filename,
+ libraryTarget: 'this',
+ library: 'FrintRouterComponentHandlers'
+ },
+ externals: {
+ 'lodash': '_',
+ 'rxjs': 'Rx',
+ },
+ target: 'web',
+ plugins: plugins,
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ exclude: /(node_modules)/,
+ loader: 'babel-loader',
+ query: {
+ presets: [
+ 'travix'
+ ]
+ }
+ }
+ ]
+ }
+};
diff --git a/packages/frint-router-react/package.json b/packages/frint-router-react/package.json
index f6c135f9..8dda44d6 100644
--- a/packages/frint-router-react/package.json
+++ b/packages/frint-router-react/package.json
@@ -28,13 +28,14 @@
"frint"
],
"dependencies": {
- "frint-router": "^3.0.1"
+ "frint-react": "^3.0.1",
+ "frint-router": "^3.0.1",
+ "frint-router-component-handlers": "^3.0.1"
},
"devDependencies": {
"cross-env": "^5.0.5",
"frint": "^3.0.1",
"frint-config": "^3.0.1",
- "frint-react": "^3.0.1",
"frint-test-utils": "^3.0.1"
},
"bugs": {
diff --git a/packages/frint-router-react/src/Link.js b/packages/frint-router-react/src/Link.js
index f6eec37b..9a64b5ef 100644
--- a/packages/frint-router-react/src/Link.js
+++ b/packages/frint-router-react/src/Link.js
@@ -1,5 +1,7 @@
import React from 'react'; // eslint-disable-line import/no-extraneous-dependencies
import PropTypes from 'prop-types'; // eslint-disable-line import/no-extraneous-dependencies
+import { createLinkHandler } from 'frint-router-component-handlers';
+import { ReactHandler } from 'frint-react';
export default class Link extends React.Component {
static contextTypes = {
@@ -18,65 +20,30 @@ export default class Link extends React.Component {
constructor(...args) {
super(...args);
- this.state = {
- active: false,
- };
+ this._handler = createLinkHandler(ReactHandler, this.context.app, this);
- this.subscription = null;
+ this.state = this._handler.getInitialData();
}
componentDidMount() {
- this.considerSubscribingToRouter(this.props);
+ this._handler.beforeMount();
}
componentWillReceiveProps(nextProps) {
- this.considerSubscribingToRouter(nextProps);
- }
-
- componentWillUnmount() {
- this.unsubscribeFromRouter();
- }
-
- considerSubscribingToRouter(nextProps) {
- if (typeof nextProps.activeClassName === 'string') {
- if (!this.subscription ||
- this.props.to !== nextProps.to ||
- this.props.exact !== nextProps.exact) {
- this.resubscribeToRouter(nextProps.to, nextProps.exact);
- }
- }
- }
-
- resubscribeToRouter(to, exact) {
- this.unsubscribeFromRouter();
+ const toChanged = (this.props.to !== nextProps.to);
+ const exactChanged = (this.props.exact !== nextProps.exact);
- this.subscription = this.context.app
- .get('router')
- .getMatch$(to, { exact })
- .subscribe((matched) => {
- if (!matched) {
- return this.setState({ active: false });
- }
-
- return this.setState({ active: true });
- });
+ this._handler.propsChange(nextProps, toChanged, exactChanged);
}
- unsubscribeFromRouter() {
- if (this.subscription) {
- this.subscription.unsubscribe();
- }
+ componentWillUnmount() {
+ this._handler.beforeDestroy();
}
handleClick = (e) => {
e.preventDefault();
- const router = this.context.app.get('router');
- const to = this.props.to;
-
- if (router.getMatch(to, router.getHistory(), { exact: true }) === null) {
- router.push(to);
- }
+ this._handler.handleClick();
};
render() {
@@ -93,7 +60,7 @@ export default class Link extends React.Component {
className: className || '',
};
- if (this.state.active) {
+ if (this._handler.getData('active')) {
linkProps.className += ` ${activeClassName}`;
}
diff --git a/packages/frint-router-react/src/Link.spec.js b/packages/frint-router-react/src/Link.spec.js
index e36a4863..ec65a66c 100644
--- a/packages/frint-router-react/src/Link.spec.js
+++ b/packages/frint-router-react/src/Link.spec.js
@@ -118,9 +118,8 @@ describe('frint-route-react › Link', () => {
expect(wrapper.hasClass('anchor-active')).to.be.false;
});
-
it('subscribes to router service for matching route and changes activeClass accordingly ' +
- 'even when activeClassName is passed later on', function () {
+ 'even when activeClassName is passed and changed later on', function () {
const router = new MemoryRouterService();
const wrapper = mount(
@@ -139,6 +138,9 @@ describe('frint-route-react › Link', () => {
wrapper.setProps({ activeClassName: 'anchor-active' });
expect(wrapper.hasClass('anchor-active')).to.be.true;
+
+ wrapper.setProps({ activeClassName: 'anchor-super-active' });
+ expect(wrapper.hasClass('anchor-super-active')).to.be.true;
});
it('subscribes to router service for matching route and changes activeClass accordingly ' +
diff --git a/packages/frint-router-react/src/Route.js b/packages/frint-router-react/src/Route.js
index 70063383..ec1300ab 100644
--- a/packages/frint-router-react/src/Route.js
+++ b/packages/frint-router-react/src/Route.js
@@ -1,8 +1,9 @@
/* eslint-disable import/no-extraneous-dependencies */
import React from 'react';
import PropTypes from 'prop-types';
-import { getMountableComponent } from 'frint-react';
+import { ReactHandler } from 'frint-react';
/* eslint-enable import/no-extraneous-dependencies */
+import { createRouteHandler } from 'frint-router-component-handlers';
export default class Route extends React.Component {
static contextTypes = {
@@ -21,91 +22,30 @@ export default class Route extends React.Component {
constructor(...args) {
super(...args);
- this._routerSubscription = null;
- this._appInstance = null;
+ this._handler = createRouteHandler(ReactHandler, this.context.app, this);
- this.state = {
- component: null,
- matched: null,
- };
+ this.state = this._handler.getInitialData();
}
componentWillMount() {
- this._calculateMatchedState(this.props);
- this._calculateComponentState(this.props);
+ this._handler.beforeMount();
}
componentWillReceiveProps(nextProps) {
- this._calculateMatchedState(nextProps);
- this._calculateComponentState(nextProps);
- }
-
- _calculateMatchedState(nextProps) {
- if (nextProps.computedMatch) {
- // in case it was subscribed before
- this._unsubscribeFromRouter();
- } else if (nextProps.path) {
- if (!this._routerSubscription || (nextProps.path !== this.props.path) || (nextProps.exact !== this.props.exact)) {
- this._unsubscribeFromRouter();
+ const pathChanged = (this.props.path !== nextProps.path);
+ const exactChanged = (this.props.exact !== nextProps.exact);
+ const appChanged = (this.props.app !== nextProps.app);
- this._routerSubscription = this.context.app
- .get('router')
- .getMatch$(nextProps.path, {
- exact: nextProps.exact,
- })
- .subscribe((matched) => {
- this.setState({
- matched,
- });
- });
- }
- }
- }
-
- _calculateComponentState(nextProps) {
- if (nextProps.component) {
- // component
- this._destroyRouteApp();
-
- this.setState({
- component: nextProps.component,
- });
- } else if (nextProps.app && (this._appInstance === null || nextProps.app !== this.props.app)) {
- // app
- this._destroyRouteApp();
-
- const RouteApp = nextProps.app;
-
- this._appInstance = new RouteApp({
- parentApp: this.context.app,
- });
- this.setState({
- component: getMountableComponent(this._appInstance)
- });
- }
+ this._handler.propsChange(nextProps, pathChanged, exactChanged, appChanged);
}
componentWillUnmount() {
- this._unsubscribeFromRouter();
- this._destroyRouteApp();
- }
-
- _unsubscribeFromRouter() {
- if (this._routerSubscription) {
- this._routerSubscription.unsubscribe();
- }
- }
-
- _destroyRouteApp() {
- if (this._appInstance) {
- this._appInstance.beforeDestroy();
- this._appInstance = null;
- }
+ this._handler.beforeDestroy();
}
render() {
- const ComponentToRender = this.state.component;
- const matched = this.props.computedMatch || this.state.matched || null;
+ const ComponentToRender = this._handler.getData('component');
+ const matched = this.props.computedMatch || this._handler.getData('matched') || null;
if (!matched) {
return null;
diff --git a/packages/frint-router-react/src/Route.spec.js b/packages/frint-router-react/src/Route.spec.js
index d296f385..208e412b 100644
--- a/packages/frint-router-react/src/Route.spec.js
+++ b/packages/frint-router-react/src/Route.spec.js
@@ -27,6 +27,10 @@ function createContext() {
};
}
+function getAppInstance(wrapper) {
+ return wrapper.instance()._handler._appInstance;
+}
+
describe('frint-route-react › Route', function () {
before(function () {
resetDOM();
@@ -211,7 +215,7 @@ describe('frint-route-react › Route', function () {
);
it('registers app with parent app from context', function () {
- const aboutApp = wrapper.instance()._appInstance;
+ const aboutApp = getAppInstance(wrapper);
expect(aboutApp.getParentApp()).to.equal(context.app);
});
@@ -285,7 +289,7 @@ describe('frint-route-react › Route', function () {
it('instantiates AboutApp and registers it with parent app from context', function () {
wrapper.setProps({ app: AboutApp, component: undefined });
- aboutApp = wrapper.instance()._appInstance;
+ aboutApp = getAppInstance(wrapper);
expect(aboutApp.getParentApp()).to.equal(context.app);
expect(wrapper.html()).to.equal('