diff --git a/packages/frint-react-server/src/renderToString.spec.js b/packages/frint-react-server/src/renderToString.spec.js index f13a7cc7..209b39c9 100644 --- a/packages/frint-react-server/src/renderToString.spec.js +++ b/packages/frint-react-server/src/renderToString.spec.js @@ -4,7 +4,7 @@ import React from 'react'; import { expect } from 'chai'; import { createApp } from 'frint'; -import { observe, streamProps } from 'frint-react'; +import { observe, streamProps, Region } from 'frint-react'; import renderToString from './renderToString'; @@ -71,4 +71,62 @@ describe('frint-react-server › renderToString', function () { const html = renderToString(app); expect(html).to.contain('>TestAppName

'); }); + + it('returns HTML output of an App instance, with childs Apps', function () { + // root + function RootComponent() { + return ( +
+ +
+ ); + } + const RootApp = createApp({ + name: 'RootApp', + providers: [ + { name: 'component', useValue: RootComponent }, + ], + }); + + // apps + function App1Component() { + return

App 1

; + } + const App1 = createApp({ + name: 'App1', + providers: [ + { name: 'component', useValue: App1Component }, + ], + }); + + function App2Component() { + return

App 2

; + } + const App2 = createApp({ + name: 'App2', + providers: [ + { name: 'component', useValue: App2Component }, + ], + }); + + // render + const rootApp = new RootApp(); + + // register apps + rootApp.registerApp(App1, { + regions: ['sidebar'], + weight: 10, + }); + + rootApp.registerApp(App2, { + regions: ['sidebar2'], + weight: 10, + }); + + const string = renderToString(rootApp); + + // verify + expect(string).to.include('>App 1

'); + expect(string).not.to.include('>App 2

'); + }); }); diff --git a/packages/frint-react/package.json b/packages/frint-react/package.json index 2241a537..bb91642a 100644 --- a/packages/frint-react/package.json +++ b/packages/frint-react/package.json @@ -34,11 +34,17 @@ "cross-env": "^5.0.5", "frint": "^5.5.0", "frint-config": "^5.5.0", - "frint-test-utils": "^5.5.0" + "frint-test-utils": "^5.5.0", + "frint-react-server": "^5.5.0" }, "bugs": { "url": "https://github.com/frintjs/frint/issues" }, + "nyc": { + "exclude": [ + "lib" + ] + }, "license": "MIT", "types": "index.d.ts" } diff --git a/packages/frint-react/src/components/Region.js b/packages/frint-react/src/components/Region.js index 26b445b7..9cab27a8 100644 --- a/packages/frint-react/src/components/Region.js +++ b/packages/frint-react/src/components/Region.js @@ -1,11 +1,10 @@ /* eslint-disable no-console, no-underscore-dangle, import/no-extraneous-dependencies */ import React from 'react'; import PropTypes from 'prop-types'; +import isEqual from 'lodash/isEqual'; +import zipWith from 'lodash/zipWith'; -import composeHandlers from 'frint-component-utils/lib/composeHandlers'; -import RegionHandler from 'frint-component-handlers/lib/RegionHandler'; - -import ReactHandler from '../handlers/ReactHandler'; +import getMountableComponent from './getMountableComponent'; export default class Region extends React.Component { static propTypes = { @@ -19,35 +18,165 @@ export default class Region extends React.Component { app: PropTypes.object, }; - constructor(...args) { - super(...args); + static sendProps(appInstance, props) { + const regionService = appInstance.get(appInstance.options.providerNames.region); - this._handler = composeHandlers( - ReactHandler, - RegionHandler, - { - component: this, - }, - ); + if (!regionService) { + return; + } - this.state = this._handler.getInitialData(); + regionService.emit(props); + } + + constructor(props, context) { + super(props, context); + + if (context.app) { + const rootApp = context.app.getRootApp(); + const list = rootApp._appsCollection.filter(({ regions }) => { + return regions.some(name => props.name === name); + }); + + this.state = { + list, + listForRendering: this.getListForRendering(list, rootApp) + }; + } else { + this.state = { + list: [], // array of apps ==> { name, instance } + listForRendering: [] // array of { name, Component } objects + }; + } + } + + getListForRendering(list, rootApp, listForRendering = []) { + const { + uniqueKey, + data + } = this.props; + + return list + .map((item) => { + const { + name, + weight, + multi + } = item; + const isPresent = listForRendering.some((w) => { + return w.name === name; + }); + + // @TODO: take care of removal in streamed list too? + if (isPresent) { + return null; + } + + const regionArgs = uniqueKey + ? [this.props.name, uniqueKey] + : [this.props.name]; + + if ( + uniqueKey && + !rootApp.hasAppInstance(name, ...regionArgs) + ) { + rootApp.instantiateApp(name, ...regionArgs); + } + + const instance = rootApp.getAppInstance(name, ...regionArgs); + + if (instance) { + Region.sendProps(instance, { + name: this.props.name, + uniqueKey, + data, + }); + } + + return { + name, + weight, + instance, + multi, + Component: getMountableComponent(instance), + }; + }) + .filter(item => !!item) + .concat(listForRendering) + .sort((a, b) => a.weight - b.weight); } shouldComponentUpdate(nextProps, nextState) { - return this._handler.shouldUpdate(nextProps, nextState); + let shouldUpdate = !isEqual(this.props, nextProps); + + if (!shouldUpdate) { + const { listForRendering } = nextState; + shouldUpdate = shouldUpdate || this.state.listForRendering.length !== listForRendering.length; + shouldUpdate = shouldUpdate || + zipWith(this.state.listForRendering, listForRendering, (a, b) => a.name === b.name) + .some(value => !value); + } + + return shouldUpdate; } - componentWillMount() { - this._handler.app = this.context.app; - this._handler.beforeMount(); + componentDidMount() { + if (!this.context.app) { + return; + } + + const rootApp = this.context.app.getRootApp(); + + this.rootApp = rootApp; + const apps$ = rootApp.getApps$( + this.props.name, + this.props.uniqueKey + ); + + this._subscription = apps$.subscribe({ + next: (list) => { + this.setState({ + list, + listForRendering: this.getListForRendering(list, rootApp, this.state.listForRendering) + }); + }, + error: (err) => { + console.warn(`Subscription error for :`, err); + } + }); } componentWillReceiveProps(nextProps) { - this._handler.afterUpdate(nextProps); + const { + name = this.props.name, + uniqueKey = this.props.uniqueKey, + data = this.props.data, + } = nextProps; + + this.state.listForRendering + .filter(item => item.instance) + .forEach(item => Region.sendProps(item.instance, { + name, + uniqueKey, + data, + })); } componentWillUnmount() { - this._handler.beforeDestroy(); + if (this._subscription) { + this._subscription.unsubscribe(); + } + + if (this.rootApp) { + this.state.listForRendering + .filter(item => item.multi) + .forEach((item) => { + this.rootApp.destroyApp( + item.name, + this.props.name, + this.props.uniqueKey + ); + }); + } } render() { diff --git a/packages/frint-react/src/components/Region.spec.js b/packages/frint-react/src/components/Region.spec.js index c6e672db..6abe5df6 100644 --- a/packages/frint-react/src/components/Region.spec.js +++ b/packages/frint-react/src/components/Region.spec.js @@ -8,11 +8,13 @@ import { Subject } from 'rxjs/Subject'; import sinon from 'sinon'; import { createApp } from 'frint'; +import { renderToString } from 'frint-react-server'; import render from '../render'; import observe from './observe'; import Region from './Region'; import RegionService from '../services/Region'; + import streamProps from '../streamProps'; describe('frint-react › components › Region', function () { @@ -400,4 +402,82 @@ describe('frint-react › components › Region', function () { expect(paragraph.parentElement.className).to.equal(className); expect(paragraph.innerHTML).to.equal('App 1'); }); + + it('should render when renderToString is called', function () { + // root + function RootComponent() { + return ( +
+ +
+ ); + } + const RootApp = createApp({ + name: 'RootApp', + providers: [ + { name: 'component', useValue: RootComponent }, + ], + }); + + // apps + function App1Component() { + return

App 1

; + } + const App1 = createApp({ + name: 'App1', + providers: [ + { name: 'component', useValue: App1Component }, + ], + }); + + function App2Component() { + return

App 2

; + } + const App2 = createApp({ + name: 'App2', + providers: [ + { name: 'component', useValue: App2Component }, + ], + }); + + // render + const rootApp = new RootApp(); + + // register apps + rootApp.registerApp(App1, { + regions: ['sidebar'], + weight: 10, + }); + + rootApp.registerApp(App2, { + regions: ['sidebar2'], + weight: 10, + }); + + const string = renderToString(rootApp); + + // verify + expect(string).to.include('App 1'); + expect(string).not.to.include('App 2'); + }); + + it('should unmount component when no root app is available', function () { + function MyComponent() { + return ( +
+ +
+ ); + } + + ReactDOM.render( + , + document.getElementById('root') + ); + + const element = document.getElementById('my-component'); + expect(element.innerHTML).to.eql(''); + expect(ReactDOM.unmountComponentAtNode(document.getElementById('root'))).to.equal(true); + expect(document.getElementById('my-component')).to.equal(null); + }); }); diff --git a/packages/frint-react/src/handlers/ReactHandler.spec.js b/packages/frint-react/src/handlers/ReactHandler.spec.js new file mode 100644 index 00000000..7f8919d0 --- /dev/null +++ b/packages/frint-react/src/handlers/ReactHandler.spec.js @@ -0,0 +1,42 @@ +/* eslint-disable import/no-extraneous-dependencies, func-names */ +/* global describe, it */ +import { expect } from 'chai'; + +import { composeHandlers } from 'frint-component-utils'; + +import ReactHandler from './ReactHandler'; + +describe('frint-react › ReactHandler', function () { + const component = { + state: { + text: '' + }, + setState(newState, cb) { + this.state = { ...this.state, ...newState }; + cb && cb(); + }, + props: { + value: 'hi' + } + }; + + it('is an object', function () { + expect(ReactHandler).to.be.an('object'); + }); + + it('gets and sets data from component', function () { + const handler = composeHandlers( + ReactHandler, + { + component + }, + ); + + handler.setData('text', 'hello'); + expect(handler.getData('text')).to.equal('hello'); + handler.setDataWithCallback('text', 'hello1', () => {}); + expect(handler.getData('text')).to.equal('hello1'); + expect(handler.getProps()).to.have.property('value', 'hi'); + expect(handler.getProp('value')).to.equal('hi'); + }); +});