Skip to content

Commit

Permalink
fix(Portal): Support creating portals for multiple nested UIViews
Browse files Browse the repository at this point in the history
- Change API for creation/cleanup of portals for child UIView to `portalView.createPortalToChildUIView` and `portalView.removePortalToChildUIView`
- Reference child react uiviews in PortalView by $id
- Rename debug to debugLog
  • Loading branch information
christopherthielen committed Nov 15, 2018
1 parent a2483ec commit ab4edcb
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 42 deletions.
19 changes: 10 additions & 9 deletions src/angularjs/ReactUIViewAdapterComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import * as ReactDOM from 'react-dom';
import { filter } from '@uirouter/core';
import { IPortalScope } from '../react/AngularUIView';
import { PortalView } from '../react/PortalView';
import ReactUIView from '../react/ReactUIView';
import { ReactUIView } from '../react/ReactUIView';
import { hybridModule } from './module';
import { debug as debugLog } from '../debug';
import { debugLog } from '../debug';

// When an angularjs `ui-view` is instantiated, also create an react-ui-view-adapter (which creates a react UIView)
hybridModule.directive('uiView', function() {
Expand Down Expand Up @@ -42,7 +42,7 @@ hybridModule.directive('reactUiViewAdapter', function() {
debug('.link()', 'linking react-ui-view-adapter into ', el, attrs);

// The UIView ref callback, which is called after the initial render
const ref = ref => {
const ref = (ref: HTMLElement) => {
// If refs are the same - don't re-render React component.
const isSameRef = ref && _ref === ref;

Expand Down Expand Up @@ -91,22 +91,23 @@ hybridModule.directive('reactUiViewAdapter', function() {
return;
}

const props = { ...attrs, render, wrap: false, refFn: ref };
const childUIViewProps = { ...attrs, render, wrap: false, refFn: ref };
const portalView: PortalView = scope.$uiRouterReactHybridPortalView;

if (portalView) {
debug('.renderReactUIView()', `will createPortalToTarget({ name: '${props['name']}' })`, el);
portalView.createPortalToTarget(props, el);
debug('.renderReactUIView()', `will createPortalToChildUIView({ name: '${childUIViewProps['name']}' })`, el);
portalView.createPortalToChildUIView($id, { childUIViewProps, portalTarget: el });
} else {
debug('.renderReactUIView()', `ReactDOM.render(<ReactUIView name="${props['name']}"/>)`, el);
ReactDOM.render<any>(<ReactUIView {...props} />, el as any);
debug('.renderReactUIView()', `ReactDOM.render(<ReactUIView name="${childUIViewProps['name']}"/>)`, el);
ReactDOM.render<any>(<ReactUIView {...childUIViewProps} />, el as any);
}
}

scope.$on('$destroy', () => {
destroyed = true;
const portalView: PortalView = scope.$uiRouterReactHybridPortalView;
if (portalView) {
portalView.createPortalToTarget(null, null);
portalView.removePortalToChildUIView($id);
} else {
const unmounted = ReactDOM.unmountComponentAtNode(el);
debug('.$on("$destroy")', `unmountComponentAtNode(): ${unmounted}`, el);
Expand Down
2 changes: 1 addition & 1 deletion src/debug.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { padString } from '@uirouter/core';

export const debug = (
export const debugLog = (
angularOrReact: 'angularjs' | 'react',
component: string,
id: string,
Expand Down
76 changes: 51 additions & 25 deletions src/react/PortalView.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { UIView, UIViewProps } from '@uirouter/react';
import { UIViewProps } from '@uirouter/react';
import { debugLog } from '../debug';
import { AngularUIView } from './AngularUIView';
import ReactUIView from './ReactUIView';
import { debug as debugLog } from '../debug';
import { IReactUIViewProps, ReactUIView } from './ReactUIView';

let id = 0;

interface IPortalViewState {
props: any;
target: HTMLElement;
portals: {
[key: number]: ChildUIView;
};
}

export interface ChildUIView {
childUIViewProps: IReactUIViewProps;
portalTarget: HTMLElement;
}

/**
* This react component renders the AngularUIView react component
* and also creates React Portals as needed for child React UIViews.
*/
export class PortalView extends React.PureComponent<UIViewProps, IPortalViewState> {
private $id = id++;

public state = {
props: null,
target: null,
};
public state: IPortalViewState = { portals: [] };

private debug = (method: string, message: string, ...args) =>
debugLog('react', 'PortalView', `${this.$id}/${this.props['name']}`, method, message, ...args);
Expand All @@ -27,22 +33,42 @@ export class PortalView extends React.PureComponent<UIViewProps, IPortalViewStat
this.debug('.componentWillUnmount()', '');
}

createPortalToTarget = (props, target) => {
this.debug('.createPortalToTarget()', JSON.stringify(props), target);
this.setState({ props, target });
createPortalToChildUIView = (uiViewId: number, childUIView: ChildUIView) => {
this.debug('.createPortalToChildUIView()', JSON.stringify(childUIView.childUIViewProps), childUIView.portalTarget);
this.setState(prev => {
const portals = { ...prev.portals, [uiViewId]: childUIView };
return { portals };
});
};

renderPortal() {
const { props, target } = this.state;
const method = `.renderPortal({ name: ${this.props['name']} })`;

if (props && target) {
this.debug(method, 'ReactDOM.createPortal()', this.state.props, this.state.target);
return ReactDOM.createPortal(<ReactUIView {...props} />, target);
} else {
this.debug(method, 'no target; not rendering portal');
return null;
}
removePortalToChildUIView = (uiViewId: number) => {
const childUIView = this.state.portals[uiViewId] || ({} as ChildUIView);
this.debug('.removePortalToChildUIView()', `${uiViewId}`, childUIView.childUIViewProps, childUIView.portalTarget);
this.setState(prev => {
const portals = { ...prev.portals };
delete portals[uiViewId];
return { portals };
});
};

renderPortals() {
const { portals } = this.state;
const method = `.renderPortals()`;

Object.keys(portals).forEach(key => {
const portal = portals[key];
this.debug(method, `ReactDOM.createPortal(${key})`, '', portal.childUIViewProps, portal.portalTarget);
});

return Object.keys(portals).map(key => {
const portal = portals[key];
// No mechanism to provide a key when creating a portals array?
return (
<div style={{ display: 'none' }} key={`${key}`}>
{ReactDOM.createPortal(<ReactUIView {...portal.childUIViewProps} />, portal.portalTarget)}
</div>
);
});
}

render() {
Expand All @@ -51,7 +77,7 @@ export class PortalView extends React.PureComponent<UIViewProps, IPortalViewStat
return (
<React.Fragment>
<AngularUIView {...this.props} portalView={this} />
{this.renderPortal()}
{this.renderPortals()}
</React.Fragment>
);
}
Expand Down
16 changes: 10 additions & 6 deletions src/react/ReactUIView.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import * as React from 'react';
import { UIRouterConsumer, UIView, UIViewConsumer } from '@uirouter/react';
import { UIRouterConsumer, UIView, UIViewConsumer, UIViewProps } from '@uirouter/react';
import { UIRouterContextComponent } from './UIRouterReactContext';
import { debug as debugLog } from '../debug';
import { debugLog } from '../debug';

const InternalUIView = UIView.__internalViewComponent;

const ReactUIView = ({ refFn, ...props }) => {
export interface IReactUIViewProps extends UIViewProps {
refFn: (ref: HTMLElement) => void;
}

export const ReactUIView = ({ refFn, ...props }: IReactUIViewProps) => {
debugLog('react', 'ReactUIView', `?/${props['name']}`, '.render()', '');

return (
<UIRouterContextComponent parentContextLevel="3" inherited={false}>
<UIRouterConsumer>
{router => (
<UIViewConsumer>
{parentUiView => <InternalUIView {...props} ref={refFn} parentUIView={parentUiView} router={router} />}
{parentUiView => (
<InternalUIView {...props} ref={refFn as any} parentUIView={parentUiView} router={router} />
)}
</UIViewConsumer>
)}
</UIRouterConsumer>
</UIRouterContextComponent>
);
};

export default ReactUIView;
2 changes: 1 addition & 1 deletion src/react/UIViewMonkeyPatch.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { UIView } from '@uirouter/react';
import { debug as debugLog } from '../debug';
import { debugLog } from '../debug';
import { PortalView } from './PortalView';

const realRender = UIView.prototype.render;
Expand Down

0 comments on commit ab4edcb

Please sign in to comment.