Skip to content

Commit

Permalink
Added support for SSR on regions and refactored to remove handlers (#429
Browse files Browse the repository at this point in the history
)

* Added support for SSR on regions and refactored to remove handlers

* Added additional check on tests

* Added test in renderToString method

* Fixed import path from renderToString

* Added test to increase coverage

* Added tests for React Handler

* Excluded lib folder from coverage report
  • Loading branch information
jcampalo authored Jul 13, 2018
1 parent 5a7205b commit bed931d
Show file tree
Hide file tree
Showing 5 changed files with 337 additions and 22 deletions.
60 changes: 59 additions & 1 deletion packages/frint-react-server/src/renderToString.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -71,4 +71,62 @@ describe('frint-react-server › renderToString', function () {
const html = renderToString(app);
expect(html).to.contain('>TestAppName</p></div>');
});

it('returns HTML output of an App instance, with childs Apps', function () {
// root
function RootComponent() {
return (
<div>
<Region name="sidebar" />
</div>
);
}
const RootApp = createApp({
name: 'RootApp',
providers: [
{ name: 'component', useValue: RootComponent },
],
});

// apps
function App1Component() {
return <p>App 1</p>;
}
const App1 = createApp({
name: 'App1',
providers: [
{ name: 'component', useValue: App1Component },
],
});

function App2Component() {
return <p>App 2</p>;
}
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</p>');
expect(string).not.to.include('>App 2</p>');
});
});
8 changes: 7 additions & 1 deletion packages/frint-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
169 changes: 149 additions & 20 deletions packages/frint-react/src/components/Region.js
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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 <Region name="${this.name}" />:`, 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() {
Expand Down
80 changes: 80 additions & 0 deletions packages/frint-react/src/components/Region.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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 (
<div>
<Region name="sidebar" />
</div>
);
}
const RootApp = createApp({
name: 'RootApp',
providers: [
{ name: 'component', useValue: RootComponent },
],
});

// apps
function App1Component() {
return <p>App 1</p>;
}
const App1 = createApp({
name: 'App1',
providers: [
{ name: 'component', useValue: App1Component },
],
});

function App2Component() {
return <p>App 2</p>;
}
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 (
<div id="my-component">
<Region name="left-sidebar" />
</div>
);
}

ReactDOM.render(
<MyComponent />,
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);
});
});
Loading

0 comments on commit bed931d

Please sign in to comment.