this.node = node}/>;
86 | }
87 | }
88 | AngularLazyComponent.propTypes = {
89 | router: PropTypes.any
90 | };
91 |
92 | export default AngularLazyComponent;
93 |
--------------------------------------------------------------------------------
/src/base-lazy-component.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ModuleRegistry from './module-registry';
3 | import {filesAppender, unloadStyles} from './tag-appender';
4 | import assign from 'lodash/assign';
5 | import {LazyComponentLoadingError} from './ReactModuleContainerErrors';
6 | import { ReactModuleContainerContext } from './context';
7 |
8 | export default class BaseLazyComponent extends React.Component {
9 | static contextType = ReactModuleContainerContext;
10 |
11 | constructor(props, manifest) {
12 | super(props);
13 | this.manifest = manifest;
14 | }
15 |
16 | get mergedProps() {
17 | return assign({}, this.props, this.resolvedData);
18 | }
19 |
20 | hasSuspensePayload() {
21 | return !!this.context?.suspensePayload;
22 | }
23 |
24 | setupSuspensePayload() {
25 | if (!this.context?.suspense) {
26 | return;
27 | }
28 |
29 | const suspensePayload = this.context.suspensePayload = {};
30 | suspensePayload.promise = this.resourceLoader.then(() => {
31 | // Store the resolvedData from the suspended instance to be reloaded in the new component instance
32 | suspensePayload.resolvedData = this.resolvedData;
33 | });
34 | }
35 |
36 | handleSuspenseRender() {
37 | if (!this.context?.suspense) {
38 | return;
39 | }
40 |
41 | const { suspensePayload } = this.context;
42 | const isResolved = !!suspensePayload.resolvedData;
43 |
44 | if (!isResolved) {
45 | throw suspensePayload.promise;
46 | }
47 |
48 | // Promise is resolved, restore the data from the suspended instance to the instance
49 | if (!this.resolvedData) {
50 | this.resolvedData = suspensePayload.resolvedData;
51 | this.resourceLoader = suspensePayload.promise;
52 | }
53 | }
54 |
55 | UNSAFE_componentWillMount() {
56 | if (this.hasSuspensePayload()) {
57 | // All of this already happened, we just wait for the previous promise to resolve and we'll restore the needed state.
58 | return;
59 | }
60 |
61 | ModuleRegistry.notifyListeners('reactModuleContainer.componentStartLoading', this.manifest.component);
62 | const prepare = this.manifest.prepare ? () => this.manifest.prepare() : () => undefined;
63 | const filesAppenderPromise = filesAppender(this.manifest.files, this.manifest.crossorigin).then(prepare);
64 | const resolvePromise = this.manifest.resolve ? this.manifest.resolve() : Promise.resolve({});
65 | this.resourceLoader = Promise.all([resolvePromise, filesAppenderPromise]).then(([resolvedData]) => {
66 | this.resolvedData = resolvedData;
67 | ModuleRegistry.notifyListeners('reactModuleContainer.componentReady', this.manifest.component);
68 | }).catch(err => {
69 | ModuleRegistry.notifyListeners('reactModuleContainer.error', new LazyComponentLoadingError(this.manifest.component, err));
70 | this.setState({
71 | error: err,
72 | });
73 | });
74 |
75 | // This component instance will be thrown away and a new one created when the promise is resolved.
76 | // Store the promise and reference to the data from this instance
77 | this.setupSuspensePayload();
78 | }
79 |
80 | componentWillUnmount() {
81 | if (this.manifest.unloadStylesOnDestroy !== false) {
82 | unloadStyles(document, this.manifest.files);
83 | }
84 | ModuleRegistry.notifyListeners('reactModuleContainer.componentWillUnmount', this.manifest.component);
85 | }
86 |
87 | renderComponent() {
88 | this.handleSuspenseRender();
89 |
90 | // Make sure any context does not propagate to any children (otherwise this can enter an infinite loop since it's working on the same payload instance)
91 | return this.state.component ?
: null;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/context.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | /** @deprecated `ReactModuleContainerContext` exists as a workaround while transitioning from legacy features, please do not use */
4 | export const ReactModuleContainerContext = createContext(null);
5 | ReactModuleContainerContext.displayName = 'ReactModuleContainerContext(deprecated)';
6 |
--------------------------------------------------------------------------------
/src/demo/EventListener.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ModuleRegistry from '../module-registry';
3 |
4 | export class EventsListener extends React.Component {
5 | constructor(props) {
6 | super(props);
7 | this.state = {
8 | gotStartLoadingEvent: false,
9 | gotComponentReady: false,
10 | gotComponentWillUnmount: false
11 | };
12 | }
13 |
14 | UNSAFE_componentWillMount() {
15 | this.unSubscribeStartLoading = ModuleRegistry.addListener('reactModuleContainer.componentStartLoading', () => {
16 | this.setState({gotStartLoadingEvent: true});
17 | });
18 |
19 | this.unSubscribeComponentReady = ModuleRegistry.addListener('reactModuleContainer.componentReady', () => {
20 | this.setState({gotComponentReady: true});
21 | });
22 |
23 | this.unSubscribeComponentWillUnmount = ModuleRegistry.addListener('reactModuleContainer.componentWillUnmount', () => {
24 | this.setState({gotComponentWillUnmount: true});
25 | });
26 | }
27 |
28 | componentWillUnmount() {
29 | this.unSubscribeStartLoading.remove();
30 | this.unSubscribeComponentReady.remove();
31 | this.unSubscribeComponentWillUnmount.remove();
32 | }
33 |
34 | render() {
35 | return (
36 |
gotStartLoadingEvent:
37 | {this.state.gotStartLoadingEvent ? 'true' : 'false'}
38 |
gotComponentReady: {this.state.gotComponentReady ? 'true' : 'false'}
39 |
40 |
gotComponentWillUnmount:
41 | {this.state.gotComponentWillUnmount ? 'true' : 'false'}
42 |
);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/demo/demo-4.scss:
--------------------------------------------------------------------------------
1 | :global {
2 | .demo-4 {
3 | color: rgba(4, 4, 4, 1);
4 | }
5 | .demo-5 {
6 | display: none;
7 | color: rgba(0, 0, 0, 0);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/demo/demo-5.scss:
--------------------------------------------------------------------------------
1 | :global {
2 | .demo-4 {
3 | color: rgba(0, 0, 0, 0);
4 | display: none;
5 | }
6 | .demo-5 {
7 | color: rgba(5, 5, 5, 1);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/demo/demo-shared.scss:
--------------------------------------------------------------------------------
1 | :global {
2 | .demo-shared {
3 | background-color: rgba(200, 200, 200, 1);
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/demo/demo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {render} from 'react-dom';
4 | import {createStore} from 'redux';
5 | import {Provider, connect} from 'react-redux';
6 | import ModuleRegistry from '../module-registry';
7 | import {EventsListener} from './EventListener';
8 | import {Router, Route, browserHistory, Link, IndexRoute, withRouter} from 'react-router';
9 |
10 | import {activeLink} from './demo.scss';
11 |
12 | const store = createStore((state = 'react-input-value', action) => {
13 | return action.type === 'assign' ? action.value : state;
14 | });
15 | const withStore = connect(
16 | state => ({value: state}),
17 | dispatch => ({assign: value => dispatch({type: 'assign', value})})
18 | );
19 |
20 | const topology = {
21 | staticsUrl: 'http://localhost:3200/lazy/',
22 | baseUrl: 'http://localhost:3200/'
23 | };
24 | const rootElement = document.getElementById('root');
25 | const MyApp = {MyNgComp: ModuleRegistry.component('MyApp.MyNgComp')};
26 | const MyApp2 = {MyNgComp: ModuleRegistry.component('MyApp2.MyNgComp')};
27 | const MyApp3 = {MyReactComp: ModuleRegistry.component('MyApp3.MyReactComp')};
28 | const MyApp4 = {MyNgComp: ModuleRegistry.component('MyApp4.MyNgComp')};
29 | const MyApp5 = {MyNgComp: ModuleRegistry.component('MyApp5.MyNgComp')};
30 | const MyApp5NoUnloadCss = {MyNgComp: ModuleRegistry.component('MyApp5NoUnloadCss.MyNgComp')};
31 | const MyApp6 = {MyReactCompCrossOrigin: ModuleRegistry.component('MyApp6.MyReactCompCrossOrigin')};
32 | const MyApp7 = {MyReactComp: ModuleRegistry.component('MyApp7.MyReactComp')};
33 | const MyApp8 = {MyReactComp: ModuleRegistry.component('MyApp8.MyReactComp')};
34 |
35 | const SplatLink = withRouter(props => {
36 | const newProps = {to: props.to, className: props.className, style: props.style};
37 | if (props.location.pathname.indexOf(props.to) === 0) {
38 | newProps.style = {...props.style, ...props.activeStyle};
39 | newProps.className = `${props.className || ''} ${props.activeClassName || ''}`;
40 | }
41 | return
{props.children};
42 | });
43 | const Navigation = withStore(props => (
44 |
45 |
46 |
props.assign(e.target.value)}/>
47 |
48 |
ng-router-app
49 |
ui-router-app
50 |
rt-router-app
51 |
ng-router-app
52 |
ng-router-app4
53 |
ng-router-app5
54 |
ng-router-app5-no-unload-css
55 |
rt-router-app6
56 |
rt-router-app7
57 |
rt-router-app8
58 |
{props.children}
59 |
60 | ));
61 | Navigation.propTypes = {
62 | children: PropTypes.any
63 | };
64 |
65 | const Home = () =>
hello;
66 |
67 | const App = withStore(withRouter(props =>
));
68 | const App2 = withStore(withRouter(props =>
));
69 | const App3 = withStore(withRouter(props =>
));
70 | const App4 = withStore(withRouter(props =>
));
71 | const App5 = withStore(withRouter(props =>
));
72 | const App5NoUnloadModule = withStore(withRouter(props =>
));
73 | const App6 = withStore(withRouter(props =>
));
74 | const App7 = withStore(withRouter(props =>
));
75 | const App8 = withStore(withRouter(props =>
));
76 |
77 | render(
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | ,
94 | rootElement
95 | );
96 |
--------------------------------------------------------------------------------
/src/demo/demo.scss:
--------------------------------------------------------------------------------
1 | .active-link {
2 | background-color: yellow;
3 | }
4 |
--------------------------------------------------------------------------------
/src/demo/module.js:
--------------------------------------------------------------------------------
1 | /* global React, AngularLazyComponent, ReactLazyComponent, ModuleRegistry */
2 | import {Link} from 'react-router';
3 | import PropTypes from 'prop-types';
4 |
5 | export class MyNgComp extends AngularLazyComponent {
6 | constructor(props) {
7 | super(props, {
8 | files: [`${props.topology.staticsUrl}angular-module.bundle.js`],
9 | module: 'myApp',
10 | component: 'my-comp'
11 | });
12 | }
13 | }
14 |
15 | export class MyNgComp2 extends AngularLazyComponent {
16 | constructor(props) {
17 | super(props, {
18 | files: [`${props.topology.staticsUrl}angular-module.bundle.js`],
19 | resolve: () => {
20 | const experimentsPromise = Promise.resolve({'specs.fed.ReactModuleContainerWithResolve': true});
21 | const customDataPromise = Promise.resolve({user: 'xiw@wix.com'});
22 | return Promise.all([experimentsPromise, customDataPromise]).then(results => {
23 | return {
24 | experiments: results[0],
25 | customData: results[1]
26 | };
27 | });
28 | },
29 | module: 'myApp2',
30 | component: 'my-comp'
31 | });
32 | }
33 | }
34 |
35 | export class MyReactComp extends ReactLazyComponent {
36 | constructor(props) {
37 | super(props, {
38 | files: [`${props.topology.staticsUrl}react-module.bundle.js`],
39 | resolve: () => {
40 | const experimentsPromise = Promise.resolve({'specs.fed.ReactModuleContainerWithResolve': true});
41 | const customDataPromise = Promise.resolve({user: 'xiw@wix.com'});
42 | return Promise.all([experimentsPromise, customDataPromise]).then(results => {
43 | return {
44 | experiments: results[0],
45 | customData: results[1]
46 | };
47 | });
48 | },
49 | component: 'MyApp3.RealReactComp'
50 | });
51 | }
52 | }
53 |
54 | export class MyReactCompCrossOrigin extends ReactLazyComponent {
55 | constructor(props) {
56 | super(props, {
57 | files: [`${props.topology.staticsUrl}react-module.bundle.js`],
58 | crossorigin: true,
59 | component: 'MyApp6.RealReactCompCrossOrigin'
60 | });
61 | }
62 | }
63 |
64 | class Hello extends React.Component {
65 | constructor(props) {
66 | super(props);
67 | this.state = {counter: 0};
68 | }
69 |
70 | handleClick() {
71 | this.setState({counter: this.state.counter + 1});
72 | }
73 |
74 | render() {
75 | return (
76 |
this.handleClick()}>
77 |
React Counter (click me): {this.state.counter}!!!
78 |
{this.props.value}
79 |
80 |
81 | ng-route-app
82 | ui-route-app
83 |
84 |
);
85 | }
86 | }
87 | Hello.propTypes = {
88 | value: PropTypes.string
89 | };
90 |
91 | export class MyNgComp4 extends AngularLazyComponent {
92 | constructor(props) {
93 | super(props, {
94 | files: [
95 | `${props.topology.staticsUrl}angular-module.bundle.js`,
96 | `${props.topology.baseUrl}demo-shared.css`,
97 | `${props.topology.baseUrl}demo-4.css`
98 | ],
99 | module: 'myApp4',
100 | component: 'my-comp'
101 | });
102 | }
103 | }
104 |
105 | export class MyNgComp5 extends AngularLazyComponent {
106 | constructor(props) {
107 | super(props, {
108 | unloadStylesOnDestroy: true,
109 | files: [
110 | `${props.topology.staticsUrl}angular-module.bundle.js`,
111 | `${props.topology.baseUrl}demo-shared.css`,
112 | `${props.topology.baseUrl}demo-5.css`
113 | ],
114 | module: 'myApp5',
115 | component: 'my-comp'
116 | });
117 | }
118 | }
119 |
120 | export class MyNgApp5NoUnloadCss extends MyNgComp5 {
121 | constructor(props) {
122 | super(props);
123 | this.manifest.unloadStylesOnDestroy = false;
124 | }
125 | }
126 |
127 | export class MyReactComp7 extends ReactLazyComponent {
128 | constructor(props) {
129 | super(props, {
130 | files: [
131 | `${props.topology.staticsUrl}react-module.bundle.js`,
132 | `${props.topology.baseUrl}demo-shared.css`,
133 | `${props.topology.baseUrl}demo-4.css`
134 | ],
135 | component: 'MyApp7.RealReactComp'
136 | });
137 | }
138 | }
139 |
140 | export class MyReactComp8 extends ReactLazyComponent {
141 | constructor(props) {
142 | super(props, {
143 | files: [
144 | `${props.topology.staticsUrl}react-module.bundle.js`,
145 | `${props.topology.baseUrl}demo-shared.css`,
146 | `${props.topology.baseUrl}demo-5.css`
147 | ],
148 | component: 'MyApp7.RealReactComp',
149 | unloadStylesOnDestroy: false
150 | });
151 | }
152 | }
153 |
154 | ModuleRegistry.registerComponent('MyApp.MyNgComp', () => MyNgComp);
155 | ModuleRegistry.registerComponent('MyApp2.MyNgComp', () => MyNgComp2);
156 | ModuleRegistry.registerComponent('MyApp3.MyReactComp', () => MyReactComp);
157 | ModuleRegistry.registerComponent('Hello', () => Hello);
158 | ModuleRegistry.registerComponent('MyApp4.MyNgComp', () => MyNgComp4);
159 | ModuleRegistry.registerComponent('MyApp5.MyNgComp', () => MyNgComp5);
160 | ModuleRegistry.registerComponent('MyApp5NoUnloadCss.MyNgComp', () => MyNgApp5NoUnloadCss);
161 | ModuleRegistry.registerComponent('MyApp6.MyReactCompCrossOrigin', () => MyReactCompCrossOrigin);
162 | ModuleRegistry.registerComponent('MyApp7.MyReactComp', () => MyReactComp7);
163 | ModuleRegistry.registerComponent('MyApp8.MyReactComp', () => MyReactComp8);
164 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export {default as ModuleRegistry} from './module-registry';
2 | export {default as ReactLazyComponent} from './react-lazy-component';
3 | export {default as AngularLazyComponent} from './angular-lazy-component';
4 | export {default as ReactLoadableComponent} from './react-loadable-component';
5 | export { ReactModuleContainerContext } from './context';
6 |
--------------------------------------------------------------------------------
/src/lazy/angular-module.js:
--------------------------------------------------------------------------------
1 | /* global angular */
2 | const myApp = angular.module('myApp', ['ngRoute']);
3 |
4 | class MyCompController {
5 | constructor(props) {
6 | this.value = 'angular-input-value';
7 | this.props = props;
8 | }
9 | }
10 |
11 | myApp.config(($routeProvider, $locationProvider) => {
12 | $locationProvider.html5Mode({enabled: true, requireBase: false});
13 | $routeProvider
14 | .when('/ng-router-app/a', {template: '
BAZINGA!
'})
15 | .when('/ng-router-app/b', {template: '
STAGADISH!
'})
16 | .otherwise('/ng-router-app/a');
17 | });
18 |
19 | myApp.component('myComp', {
20 | template:
21 | `
22 |
{{$ctrl.props().value}}
23 |
24 |
25 | a
26 | b
27 | rt-router-app
28 |
29 |
30 |
31 |
`,
32 | controller: MyCompController
33 | });
34 |
35 | const myApp2 = angular.module('myApp2', ['ui.router']);
36 |
37 | class MyCompController2 {
38 | constructor(props) {
39 | this.value = 'angular-input-value';
40 | this.props = props;
41 | }
42 | }
43 |
44 | myApp2.config(($stateProvider, $locationProvider, $urlRouterProvider) => {
45 | $locationProvider.html5Mode({enabled: true, requireBase: false});
46 | $stateProvider.state('a', {url: '/ui-router-app/a', template: 'BAZINGA!'});
47 | $stateProvider.state('b', {url: '/ui-router-app/b', template: 'STAGADISH!'});
48 | $urlRouterProvider.otherwise('/ui-router-app/a');
49 | });
50 |
51 | myApp2.component('myComp', {
52 | template:
53 | `
54 |
{{$ctrl.props().value}}
55 |
{{$ctrl.props().experiments}}
56 |
{{$ctrl.props().customData}}
57 |
58 |
59 |
a
60 |
b
61 |
rt-router-app
62 |
63 |
64 |
65 |
`,
66 | controller: MyCompController2
67 | });
68 |
69 | const SHARED_TEMPLATE = `
70 |
71 |
demo-4
72 |
demo-5
73 |
`;
74 |
75 | angular.module('myApp4', [])
76 | .component('myComp', {template: SHARED_TEMPLATE});
77 |
78 | angular.module('myApp5', [])
79 | .component('myComp', {template: SHARED_TEMPLATE});
80 |
--------------------------------------------------------------------------------
/src/lazy/react-module.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {Link} from 'react-router';
4 | import ModuleRegistry from '../module-registry';
5 |
6 | const RealReactComp = props => (
7 |
8 |
{props.value}
9 |
{JSON.stringify(props.experiments)}
10 |
{JSON.stringify(props.customData)}
11 |
12 | ng-route-app
13 | ui-route-app
14 |
15 |
16 | );
17 | RealReactComp.propTypes = {
18 | value: PropTypes.any,
19 | experiments: PropTypes.any,
20 | customData: PropTypes.any
21 | };
22 | ModuleRegistry.registerComponent('MyApp3.RealReactComp', () => RealReactComp);
23 |
24 | const RealReactCompCrossOrigin = props => (
25 |
28 | );
29 | RealReactCompCrossOrigin.propTypes = {
30 | value: PropTypes.any
31 | };
32 | ModuleRegistry.registerComponent('MyApp6.RealReactCompCrossOrigin', () => RealReactCompCrossOrigin);
33 |
34 | const DemoReactComp = () => (
35 |
36 |
demo-4
37 |
demo-5
38 |
39 | );
40 | ModuleRegistry.registerComponent('MyApp7.RealReactComp', () => DemoReactComp);
41 |
--------------------------------------------------------------------------------
/src/module-registry.js:
--------------------------------------------------------------------------------
1 | import set from 'lodash/set';
2 | import unset from 'lodash/unset';
3 | import forEach from 'lodash/forEach';
4 | import uniqueId from 'lodash/uniqueId';
5 | import {
6 | ListenerCallbackError, UnregisteredComponentUsedError,
7 | UnregisteredMethodInvokedError
8 | } from './ReactModuleContainerErrors';
9 |
10 | class ModuleRegistry {
11 | constructor() {
12 | this.registeredComponents = {};
13 | this.registeredMethods = {};
14 | this.eventListeners = {};
15 | this.modules = {};
16 | }
17 |
18 | cleanAll() {
19 | this.registeredComponents = {};
20 | this.registeredMethods = {};
21 | this.eventListeners = {};
22 | this.modules = {};
23 | }
24 |
25 | registerModule(globalID, ModuleFactory, args = []) {
26 | if (this.modules[globalID]) {
27 | throw new Error(`A module with id "${globalID}" is already registered`);
28 | }
29 |
30 | this.modules[globalID] = new ModuleFactory(...args);
31 | }
32 |
33 | getModule(globalID) {
34 | return this.modules[globalID];
35 | }
36 |
37 | getAllModules() {
38 | return Object.keys(this.modules).map(moduleId => this.modules[moduleId]);
39 | }
40 |
41 | registerComponent(globalID, generator) {
42 | this.registeredComponents[globalID] = generator;
43 | }
44 |
45 | component(globalID) {
46 | const generator = this.registeredComponents[globalID];
47 | if (!generator) {
48 | this.notifyListeners('reactModuleContainer.error', new UnregisteredComponentUsedError(globalID));
49 | return undefined;
50 | }
51 | return generator();
52 | }
53 |
54 | addListener(globalID, callback) {
55 | const callbackKey = uniqueId('eventListener');
56 | set(this.eventListeners, [globalID, callbackKey], callback);
57 | return {
58 | remove: () => unset(this.eventListeners[globalID], callbackKey)
59 | };
60 | }
61 |
62 | notifyListeners(globalID, ...args) {
63 | const listenerCallbacks = this.eventListeners[globalID];
64 | if (!listenerCallbacks) {
65 | return;
66 | }
67 | forEach(listenerCallbacks, callback => invokeSafely(globalID, callback, args));
68 | }
69 |
70 | registerMethod(globalID, generator) {
71 | this.registeredMethods[globalID] = generator;
72 | }
73 |
74 | invoke(globalID, ...args) {
75 | const generator = this.registeredMethods[globalID];
76 | if (!generator) {
77 | this.notifyListeners('reactModuleContainer.error', new UnregisteredMethodInvokedError(globalID));
78 | return undefined;
79 | }
80 | const method = generator();
81 | return method(...args);
82 | }
83 | }
84 |
85 | let singleton;
86 | if (typeof window !== 'undefined') {
87 | singleton = window.ModuleRegistry || new ModuleRegistry();
88 | window.ModuleRegistry = singleton;
89 | } else {
90 | singleton = new ModuleRegistry();
91 | }
92 | export default singleton;
93 |
94 | function invokeSafely(globalID, callback, args) {
95 | try {
96 | callback(...args);
97 | } catch (err) {
98 | singleton.notifyListeners('reactModuleContainer.error', new ListenerCallbackError(globalID, err));
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/react-lazy-component.js:
--------------------------------------------------------------------------------
1 | import ModuleRegistry from './module-registry';
2 | import BaseLazyComponent from './base-lazy-component';
3 |
4 | class ReactLazyComponent extends BaseLazyComponent {
5 | constructor(props, manifest) {
6 | super(props, manifest);
7 | this.state = {component: null};
8 | }
9 |
10 | componentDidMount() {
11 | this.resourceLoader.then(() => {
12 | const component = ModuleRegistry.component(this.manifest.component);
13 | this.setState({component});
14 | });
15 | }
16 |
17 | render() {
18 | return this.renderComponent();
19 | }
20 | }
21 |
22 | export default ReactLazyComponent;
23 |
--------------------------------------------------------------------------------
/src/react-loadable-component.js:
--------------------------------------------------------------------------------
1 | import BaseLazyComponent from './base-lazy-component';
2 |
3 | export default function ReactLoadableComponent(name, resolve, files = []) {
4 | return class LoadableComponent extends BaseLazyComponent {
5 | constructor(props) {
6 | super(props, {component: name, files, resolve});
7 | this.state = {component: null};
8 | }
9 |
10 | componentDidMount() {
11 | this.resourceLoader.then(() => {
12 | if (this.resolvedData) {
13 | const component = this.resolvedData.default || this.resolvedData;
14 | if (component) {
15 | this.setState({ component });
16 | }
17 | }
18 | });
19 | }
20 |
21 | render() {
22 | if (this.state.error) {
23 | throw this.state.error;
24 | }
25 |
26 | return this.renderComponent();
27 | }
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/src/react-module-container.js:
--------------------------------------------------------------------------------
1 | import ModuleRegistry from './module-registry';
2 | import ReactLazyComponent from './react-lazy-component';
3 | import AngularLazyComponent from './angular-lazy-component';
4 | import ReactLoadableComponent from './react-loadable-component';
5 |
6 | window.ModuleRegistry = ModuleRegistry;
7 | window.ReactLazyComponent = ReactLazyComponent;
8 | window.AngularLazyComponent = AngularLazyComponent;
9 | window.ReactLoadableComponent = ReactLoadableComponent;
10 |
--------------------------------------------------------------------------------
/src/tag-appender.js:
--------------------------------------------------------------------------------
1 | import ModuleRegistry from './module-registry';
2 | import {FileAppenderLoadError} from './ReactModuleContainerErrors';
3 |
4 | const requireCache = {};
5 |
6 | function noprotocol(url) {
7 | return url.replace(/^.*:\/\//, '//');
8 | }
9 |
10 | export function createLinkElement(url) {
11 | const fileref = document.createElement('LINK');
12 | fileref.setAttribute('rel', 'stylesheet');
13 | fileref.setAttribute('type', 'text/css');
14 | fileref.setAttribute('href', url);
15 | return fileref;
16 | }
17 |
18 | export function createScriptElement(url, crossorigin) {
19 | const fileref = document.createElement('SCRIPT');
20 | fileref.setAttribute('type', 'text/javascript');
21 | fileref.setAttribute('src', url);
22 | if (crossorigin) {
23 | fileref.setAttribute('crossorigin', 'anonymous');
24 | }
25 | return fileref;
26 | }
27 |
28 | export function tagAppender(url, filetype, crossorigin) {
29 | const styleSheets = document.styleSheets;
30 | return requireCache[url] = new Promise((resolve, reject) => {
31 | if (window.requirejs && filetype === 'js') {
32 | window.requirejs([url], resolve, reject);
33 | return;
34 | } else if (url in requireCache) {
35 | // requireCache[url].then(resolve, reject);
36 | // return;
37 | }
38 |
39 | const fileref = (filetype === 'css') ?
40 | createLinkElement(url) :
41 | createScriptElement(url, crossorigin);
42 |
43 | let done = false;
44 | document.getElementsByTagName('head')[0].appendChild(fileref);
45 | fileref.onerror = function () {
46 | fileref.onerror = fileref.onload = fileref.onreadystatechange = null;
47 | delete requireCache[url];
48 | ModuleRegistry.notifyListeners('reactModuleContainer.error', new FileAppenderLoadError(url));
49 | reject(new Error(`Could not load URL ${url}`));
50 | };
51 | fileref.onload = fileref.onreadystatechange = function () {
52 | if (!done && (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete')) {
53 | done = true;
54 | fileref.onerror = fileref.onload = fileref.onreadystatechange = null;
55 | resolve();
56 | }
57 | };
58 | if (filetype === 'css' && navigator.userAgent.match(' Safari/') && !navigator.userAgent.match(' Chrom') && navigator.userAgent.match(' Version/5.')) {
59 | let attempts = 20;
60 | const interval = setInterval(() => {
61 | for (let i = 0; i < styleSheets.length; i++) {
62 | if (noprotocol(`${styleSheets[i].href}`) === noprotocol(url)) {
63 | clearInterval(interval);
64 | fileref.onload();
65 | return;
66 | }
67 | }
68 | if (--attempts === 0) {
69 | clearInterval(interval);
70 | fileref.onerror();
71 | }
72 | }, 50);
73 | }
74 | });
75 | }
76 |
77 | function append(file, crossorigin) {
78 | return tagAppender(file, file.split('.').pop(), crossorigin);
79 | }
80 |
81 | function onCatch(error, optional = false) {
82 | return optional ? Promise.resolve() : Promise.reject(error);
83 | }
84 |
85 | function appendEntry(entry, crossorigin) {
86 | if (typeof entry === 'object') {
87 | const {optional, url} = entry;
88 | return append(url, crossorigin).catch(err => onCatch(err, optional));
89 | } else {
90 | return append(entry, crossorigin).catch(err => onCatch(err));
91 | }
92 | }
93 |
94 | export function filesAppender(entries, crossorigin) {
95 | return Promise.all(entries.map(entry => {
96 | if (Array.isArray(entry)) {
97 | return entry.reduce(
98 | (promise, entryItem) => promise.then(() => appendEntry(entryItem, crossorigin)),
99 | Promise.resolve());
100 | } else {
101 | return appendEntry(entry, crossorigin);
102 | }
103 | }));
104 | }
105 |
106 | const getStyleSheetLinks = document =>
107 | Array.from(document.querySelectorAll('link'))
108 | .filter(link => link.rel === 'stylesheet' && link.href)
109 | .reduceRight((acc, curr) => ({...acc, [noprotocol(curr.href)]: curr}), {});
110 |
111 | const toUrlString = file => typeof file === 'object' ? file.url : file;
112 |
113 | const getStyleSheetUrls = files =>
114 | [].concat(...files)
115 | .map(toUrlString)
116 | .filter(url => url.endsWith('.css'))
117 | .map(noprotocol);
118 |
119 | export function unloadStyles(document, files) {
120 | const links = getStyleSheetLinks(document);
121 | getStyleSheetUrls(files).forEach(file => {
122 | const link = links[file];
123 | if (link) {
124 | link.parentNode.removeChild(link);
125 | }
126 | });
127 | }
128 |
--------------------------------------------------------------------------------
/test/e2e/app.e2e.js:
--------------------------------------------------------------------------------
1 | describe('React application', () => {
2 | describe('life cycle events', () => {
3 | it('should not have navigation events', () => {
4 | browser.get('/');
5 | expect($('#got-start-loading').getText()).toBe('false');
6 | expect($('#got-component-ready').getText()).toBe('false');
7 | expect($('#got-component-will-unmount').getText()).toBe('false');
8 | });
9 |
10 | it('should have navigation events', () => {
11 | browser.get('/ng-router-app4');
12 | expect($('#got-start-loading').getText()).toBe('true');
13 | expect($('#got-component-ready').getText()).toBe('true');
14 | expect($('#got-component-will-unmount').getText()).toBe('false');
15 | $$('.nav').get(5).click();
16 | expect($('#got-component-will-unmount').getText()).toBe('true');
17 | });
18 | });
19 |
20 | describe('open page', () => {
21 | it('should display hello', () => {
22 | browser.get('/');
23 | expect($('#hello').getText()).toBe('hello');
24 | });
25 |
26 | ['ng', 'ui'].forEach((router, index) => describe(`/${router}-router-app/`, () => {
27 | it(`should display ${router} router app`, () => {
28 | browser.get(`/${router}-router-app/`);
29 | expect($('#value-in-angular').getText()).toBe('react-input-value');
30 | expect($(`${router}-view`).getText()).toBe('BAZINGA!');
31 | expect($('#value-in-react').getText()).toBe('angular-input-value');
32 | expect($('#counter').getText()).toBe('0');
33 | });
34 |
35 | it('should update inputs in nested apps', () => {
36 | browser.get(`/${router}-router-app/`);
37 | $('#counter').click();
38 | $('#angular-input').sendKeys('123');
39 | $('#react-input').sendKeys('123');
40 | expect($('#value-in-angular').getText()).toBe('react-input-value123');
41 | expect($('#value-in-react').getText()).toBe('angular-input-value123');
42 | expect($('#counter').getText()).toBe('1');
43 | });
44 |
45 | it('should support internal navigations', () => {
46 | browser.get(`/${router}-router-app/`);
47 | $('#counter').click();
48 | $('#angular-input').sendKeys('123');
49 | $('#react-input').sendKeys('123');
50 | $('#stagadish').click();
51 | expect($$('.nav').get(router === 'ng' ? 3 : 1).getCssValue('background-color')).toBe('rgba(255, 255, 0, 1)');
52 | expect($('#value-in-angular').getText()).toBe('react-input-value123');
53 | expect($('#value-in-react').getText()).toBe('angular-input-value123');
54 | expect($('#counter').getText()).toBe('1');
55 | expect($(`${router}-view`).getText()).toBe('STAGADISH!');
56 | });
57 |
58 | it('should be able to navigate from within nested app', () => {
59 | browser.get(`/${router}-router-app/a`);
60 | expect($$('.nav').get(index).getCssValue('background-color')).toBe('rgba(255, 255, 0, 1)');
61 | $('#react-app-link').click();
62 | expect($$('.nav').get(2).getCssValue('background-color')).toBe('rgba(255, 255, 0, 1)');
63 | $$('.react-link').get(index).click();
64 | expect($$('.nav').get(index).getCssValue('background-color')).toBe('rgba(255, 255, 0, 1)');
65 | });
66 |
67 | it('should be able to navigate from react embedded in angular', () => {
68 | browser.get(`/${router}-router-app/`);
69 | $('#react-input').sendKeys('123');
70 | $$('.react-link').get(index ? 0 : 1).click();
71 | expect($$('.nav').get(index ? 0 : 1).getCssValue('background-color')).toBe('rgba(255, 255, 0, 1)');
72 | expect($('#value-in-angular').getText()).toBe('react-input-value123');
73 | });
74 | }));
75 | });
76 |
77 | describe('manifest with resolve', () => {
78 | ['ui', 'rt'].forEach(router => describe(`/${router}-router-app/`, () => {
79 | it(`should display the resolved data`, () => {
80 | browser.get(`/${router}-router-app/`);
81 | expect($('#value-of-resolved-experiments').getText()).toBe(JSON.stringify({'specs.fed.ReactModuleContainerWithResolve': true}));
82 | expect($('#value-of-resolved-custom-data').getText()).toBe(JSON.stringify({user: 'xiw@wix.com'}));
83 | });
84 | }));
85 | });
86 |
87 | describe('manifest with crossorigin', () => {
88 | it(`should load the component with the crossorigin attribute`, () => {
89 | browser.get(`/rt-router-app6/`);
90 | const lazyComponentScriptsWithCrossOrigin = $$('script')
91 | .filter(element => element.getAttribute('src').then(value => value.endsWith('react-module.bundle.js')))
92 | .filter(element => element.getAttribute('crossorigin').then(value => value !== null));
93 |
94 | expect(lazyComponentScriptsWithCrossOrigin.count()).toBe(1);
95 | });
96 | });
97 |
98 | describe('unload styles on destroy', () => {
99 |
100 | const linksToModuleWithUnloadCss = {
101 | notDefined: 4,
102 | true: 5,
103 | false: 6
104 | };
105 |
106 | const linksToReactModuleWithUnloadCss = {
107 | notDefined: 8,
108 | false: 9
109 | };
110 |
111 | beforeEach(() => {
112 | browser.get('/');
113 | });
114 |
115 | it('should by default unload css files specified inside angular manifest', () => {
116 |
117 | $$('.nav').get(linksToModuleWithUnloadCss.notDefined).click();
118 | expect(getStyleSheetHrefs()).toEqual([
119 | 'http://localhost:3200/demo.css',
120 | 'http://localhost:3200/demo-shared.css',
121 | 'http://localhost:3200/demo-4.css'
122 | ]);
123 |
124 | expectIsHidden('.demo-5');
125 |
126 | $$('.nav').get(linksToModuleWithUnloadCss.false).click();
127 | expect(getStyleSheetHrefs()).toEqual([
128 | 'http://localhost:3200/demo.css',
129 | 'http://localhost:3200/demo-shared.css',
130 | 'http://localhost:3200/demo-5.css'
131 | ]);
132 | });
133 |
134 | it('should not unload css files specified inside angular manifest', () => {
135 |
136 | $$('.nav').get(linksToModuleWithUnloadCss.false).click();
137 | expect(getStyleSheetHrefs()).toEqual([
138 | 'http://localhost:3200/demo.css',
139 | 'http://localhost:3200/demo-shared.css',
140 | 'http://localhost:3200/demo-5.css'
141 | ]);
142 |
143 | expectIsHidden('.demo-4');
144 |
145 | $$('.nav').get(linksToModuleWithUnloadCss.notDefined).click();
146 | expect(getStyleSheetHrefs()).toEqual([
147 | 'http://localhost:3200/demo.css',
148 | 'http://localhost:3200/demo-shared.css',
149 | 'http://localhost:3200/demo-5.css',
150 | 'http://localhost:3200/demo-shared.css',
151 | 'http://localhost:3200/demo-4.css'
152 | ]);
153 | });
154 |
155 | it('should unload css files specified inside angular manifest', () => {
156 |
157 | $$('.nav').get(linksToModuleWithUnloadCss.true).click();
158 | expect(getStyleSheetHrefs()).toEqual([
159 | 'http://localhost:3200/demo.css',
160 | 'http://localhost:3200/demo-shared.css',
161 | 'http://localhost:3200/demo-5.css'
162 | ]);
163 | expect($('.demo-shared').getCssValue('background-color')).toBe('rgba(200, 200, 200, 1)');
164 | expect($('.demo-5').getCssValue('color')).toBe('rgba(5, 5, 5, 1)');
165 | expectIsHidden('.demo-4');
166 |
167 | $$('.nav').get(linksToModuleWithUnloadCss.notDefined).click();
168 | expect(getStyleSheetHrefs()).toEqual([
169 | 'http://localhost:3200/demo.css',
170 | 'http://localhost:3200/demo-shared.css',
171 | 'http://localhost:3200/demo-4.css'
172 | ]);
173 | expect($('.demo-shared').getCssValue('background-color')).toBe('rgba(200, 200, 200, 1)');
174 | expect($('.demo-4').getCssValue('color')).toBe('rgba(4, 4, 4, 1)');
175 | });
176 |
177 | it('should unload css files specified inside react manifest', () => {
178 | $$('.nav').get(linksToReactModuleWithUnloadCss.notDefined).click();
179 | $$('.nav').get(linksToModuleWithUnloadCss.false).click();
180 | expect(getStyleSheetHrefs()).toEqual([
181 | 'http://localhost:3200/demo.css',
182 | 'http://localhost:3200/demo-shared.css',
183 | 'http://localhost:3200/demo-5.css'
184 | ]);
185 | });
186 |
187 | it('should not unload css files specified inside react manifest', () => {
188 | $$('.nav').get(linksToReactModuleWithUnloadCss.false).click();
189 | $$('.nav').get(linksToReactModuleWithUnloadCss.notDefined).click();
190 | expect(getStyleSheetHrefs()).toEqual([
191 | 'http://localhost:3200/demo.css',
192 | 'http://localhost:3200/demo-shared.css',
193 | 'http://localhost:3200/demo-5.css',
194 | 'http://localhost:3200/demo-shared.css',
195 | 'http://localhost:3200/demo-4.css'
196 | ]);
197 | });
198 |
199 | function getStyleSheetHrefs() {
200 | return $$('link').map(elem => elem.getAttribute('href'));
201 | }
202 |
203 | function expectIsHidden(selector) {
204 | expect($(selector).getCssValue('color')).toBe('rgba(0, 0, 0, 0)');
205 | expect($(selector).getCssValue('display')).toBe('none');
206 | }
207 | });
208 |
209 | });
210 |
--------------------------------------------------------------------------------
/test/mocha-setup.js:
--------------------------------------------------------------------------------
1 | import 'global-jsdom/register'; // eslint-disable-line import/no-unresolved
2 | import { configure } from '@testing-library/react';
3 | import { use } from 'chai';
4 | import sinonChai from 'sinon-chai';
5 |
6 | configure({ testIdAttribute: 'data-hook' });
7 | use(sinonChai);
--------------------------------------------------------------------------------
/test/mock/SomeComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ReactLoadableComponent } from '../../src';
3 |
4 | const SubComponent = ReactLoadableComponent('SomeSubComponentName', () => import('./SomeSubComponent'));
5 |
6 | export default () => (
7 |
8 |
Hello World!
9 |
10 |
11 | );
--------------------------------------------------------------------------------
/test/mock/SomeSubComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default () =>
;
--------------------------------------------------------------------------------
/test/mock/fake-server.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import express from 'express';
3 | import bodyParser from 'body-parser';
4 |
5 | const app = express();
6 | app.use(bodyParser.json());
7 |
8 | app.use((req, res, next) => {
9 | res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
10 | res.setHeader('Pragma', 'no-cache');
11 | res.setHeader('Expires', 0);
12 | return next();
13 | });
14 |
15 | app.use('*', (req, res) => {
16 | res.sendFile(path.join(process.cwd(), './src/index.html'));
17 | });
18 |
19 | const port = process.env.FAKE_SERVER_PORT || 3000;
20 | app.listen(port, () => {
21 | console.log(`Fake server is running on port ${port}`);
22 | });
23 |
24 | module.exports = app;
25 |
--------------------------------------------------------------------------------
/test/module-registry.spec.js:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import chai, {expect} from 'chai';
3 | import sinon from 'sinon';
4 | import sinonChai from 'sinon-chai';
5 |
6 | chai.use(sinonChai);
7 |
8 | import ModuleRegistry from '../src/module-registry';
9 | import {
10 | ListenerCallbackError, UnregisteredComponentUsedError,
11 | UnregisteredMethodInvokedError
12 | } from '../src/ReactModuleContainerErrors';
13 |
14 | describe('Module Registry', () => {
15 | beforeEach(() => {
16 | ModuleRegistry.cleanAll();
17 | });
18 |
19 | it('should be able to register a module', () => {
20 | class MyModule {}
21 | ModuleRegistry.registerModule('GLOBAL_ID', MyModule);
22 | const result = ModuleRegistry.getModule('GLOBAL_ID');
23 | expect(result).to.be.an.instanceOf(MyModule);
24 | });
25 |
26 | it('should be able to pass parameters to the register a module', () => {
27 | class MyModule {
28 | constructor(name) {
29 | this.name = name;
30 | }
31 | }
32 | ModuleRegistry.registerModule('GLOBAL_ID', MyModule, ['DUMMY_NAME']);
33 | const result = ModuleRegistry.getModule('GLOBAL_ID');
34 | expect(result.name).to.eq('DUMMY_NAME');
35 | });
36 |
37 | it('should throw an error if the given module was already registered', () => {
38 | class MyModule {}
39 | expect(() => ModuleRegistry.registerModule('GLOBAL_ID', MyModule)).to.not.throw();
40 | expect(() => ModuleRegistry.registerModule('GLOBAL_ID', MyModule)).to.throw();
41 | });
42 |
43 | it('should be able to get all modules', () => {
44 | class MyModule1 {}
45 | class MyModule2 {}
46 | class MyModule3 {}
47 | ModuleRegistry.registerModule('GLOBAL_ID1', MyModule1);
48 | ModuleRegistry.registerModule('GLOBAL_ID2', MyModule2);
49 | ModuleRegistry.registerModule('GLOBAL_ID3', MyModule3);
50 |
51 | const allModules = ModuleRegistry.getAllModules();
52 | expect(allModules.length).to.eq(3);
53 | expect(allModules.find(m => m instanceof MyModule1)).to.be.an.instanceOf(MyModule1);
54 | expect(allModules.find(m => m instanceof MyModule2)).to.be.an.instanceOf(MyModule2);
55 | expect(allModules.find(m => m instanceof MyModule3)).to.be.an.instanceOf(MyModule3);
56 | });
57 |
58 | it('should be able to register a method and call it', () => {
59 | const method = sinon.spy();
60 | ModuleRegistry.registerMethod('GLOBAL_ID', () => method);
61 | ModuleRegistry.invoke('GLOBAL_ID', 1, 2, 3);
62 | expect(method).calledWith(1, 2, 3);
63 | });
64 |
65 | it('should be able to register a component', () => {
66 | const component = () => '
FAKE_COMPONENT
';
67 | ModuleRegistry.registerComponent('GLOBAL_ID', component);
68 | const resultComponent = ModuleRegistry.component('GLOBAL_ID');
69 | expect(resultComponent).to.eq('
FAKE_COMPONENT
');
70 | });
71 |
72 | it('should notify all event listeners', () => {
73 | const listener1 = sinon.spy();
74 | const listener2 = sinon.spy();
75 | ModuleRegistry.addListener('GLOBAL_ID', listener1);
76 | ModuleRegistry.addListener('GLOBAL_ID', listener2);
77 | ModuleRegistry.notifyListeners('GLOBAL_ID', 1, 2, 3);
78 | expect(listener1).calledWith(1, 2, 3);
79 | expect(listener2).calledWith(1, 2, 3);
80 | });
81 |
82 | it('should clean all the methods, components, events, and modules when calling cleanAll', () => {
83 | ModuleRegistry.registerModule('GLOBAL_ID', class MyModule {});
84 | ModuleRegistry.registerMethod('GLOBAL_ID', () => () => {});
85 | ModuleRegistry.registerComponent('GLOBAL_ID', () => {});
86 | ModuleRegistry.addListener('GLOBAL_ID', () => {});
87 |
88 | ModuleRegistry.cleanAll();
89 |
90 | expect(ModuleRegistry.getModule('GLOBAL_ID')).to.be.undefined;
91 | expect(ModuleRegistry.notifyListeners('GLOBAL_ID')).to.be.undefined;
92 | expect(ModuleRegistry.component('GLOBAL_ID')).to.be.undefined;
93 | expect(ModuleRegistry.invoke('GLOBAL_ID')).to.be.undefined;
94 | });
95 |
96 | describe('ReactModuleContainerError', () => {
97 | let reactModuleContainerErrorCallback;
98 |
99 | beforeEach(() => {
100 | reactModuleContainerErrorCallback = sinon.stub();
101 | ModuleRegistry.addListener('reactModuleContainer.error', reactModuleContainerErrorCallback);
102 | });
103 |
104 | it('should be fired when trying to invoke an unregistered method', () => {
105 | const unregisteredMethodName = 'unregistered-method';
106 | const result = ModuleRegistry.invoke(unregisteredMethodName);
107 | expect(reactModuleContainerErrorCallback).calledOnce;
108 |
109 | const errorCallbackArg = reactModuleContainerErrorCallback.getCall(0).args[0];
110 |
111 | expect(errorCallbackArg).to.be.an.instanceof(UnregisteredMethodInvokedError);
112 | expect(errorCallbackArg.message).to.eq(`ModuleRegistry.invoke ${unregisteredMethodName} used but not yet registered`);
113 |
114 | expect(result).to.eq(undefined);
115 | });
116 |
117 | it('should be fired when trying to use an unregistered component', () => {
118 | const componentId = 'component-id';
119 | const resultComponent = ModuleRegistry.component(componentId);
120 | expect(reactModuleContainerErrorCallback).calledOnce;
121 |
122 | const errorCallbackArg = reactModuleContainerErrorCallback.getCall(0).args[0];
123 |
124 | expect(errorCallbackArg).to.be.an.instanceof(UnregisteredComponentUsedError);
125 | expect(errorCallbackArg.message).to.eq(`ModuleRegistry.component ${componentId} used but not yet registered`);
126 |
127 | expect(resultComponent).to.eq(undefined);
128 | });
129 |
130 | it('should be fired when a listener callback throws an error', () => {
131 | const someRegisteredMethod = 'someRegisteredMethod';
132 | const error = new Error();
133 | ModuleRegistry.addListener(someRegisteredMethod, () => {
134 | throw error;
135 | });
136 | ModuleRegistry.notifyListeners(someRegisteredMethod);
137 |
138 | const errorCallbackArg = reactModuleContainerErrorCallback.getCall(0).args[0];
139 | expect(errorCallbackArg).to.be.an.instanceof(ListenerCallbackError);
140 | expect(errorCallbackArg.message).to.eq(`Error in listener callback of module registry method: ${someRegisteredMethod}`);
141 | });
142 | });
143 | });
144 |
--------------------------------------------------------------------------------
/test/react-loadable-component.spec.js:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react';
2 | import { expect } from 'chai';
3 | import { render, cleanup } from '@testing-library/react';
4 | import { ReactLoadableComponent, ReactModuleContainerContext } from '../src';
5 | import sinon from 'sinon';
6 |
7 | const hooks = {
8 | someComponent: 'some-component-root',
9 | loader: 'loader',
10 | subComponent: 'sub-component-root',
11 | };
12 |
13 | describe('ReactLoadableComponent', () => {
14 | afterEach(cleanup);
15 |
16 | afterEach(() => sinon.reset());
17 |
18 | it('render the component', async () => {
19 | const SomeComponent = ReactLoadableComponent('SomeComponent', () => import('./mock/SomeComponent'));
20 | const { findByTestId } = render(
);
21 |
22 | const element = await findByTestId(hooks.someComponent);
23 |
24 | expect(element.textContent).to.equal('Hello World!');
25 | });
26 |
27 | describe('rendering with suspense support', () => {
28 | const resolver = sinon.fake(async () => { await new Promise(resolve => setTimeout(resolve, 100)); return import('./mock/SomeComponent') });
29 |
30 | let renderResult, SomeComponent;
31 |
32 | beforeEach(() => {
33 | SomeComponent = ReactLoadableComponent('SomeComponent', resolver);
34 | const wrapper = ({children}) => (
35 |
36 | Loading... }>{children}
37 |
38 | );
39 |
40 | renderResult = render(