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 ( +App 1
; + } + const App1 = createApp({ + name: 'App1', + providers: [ + { name: 'component', useValue: App1Component }, + ], + }); + + function App2Component() { + returnApp 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 forApp 1
; + } + const App1 = createApp({ + name: 'App1', + providers: [ + { name: 'component', useValue: App1Component }, + ], + }); + + function App2Component() { + returnApp 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 ( +