Skip to content

Commit afc9d2f

Browse files
RDEV-8923 - Ensure Inner Views are Disposed (#211)
* Setup Sample App * Update TaskListView.tsx * Update TaskListView.tsx * Update MainView.tsx * First version * Remove console log * Send FT to View * Update ReactViewFactory.cs * Add legacy files again * Update ViewPortalsCollections.tsx * Update ViewPortalsCollectionsLegacy.tsx * Update ViewPortalLegacy.tsx * Update Loader.View.tsx * Update Loader.View.tsx * Update Loader.View.tsx * Update Loader.View.tsx * Update Loader.View.tsx * Update ViewMetadataContext.ts * d * Update ViewPortal.tsx * Update ReactViewFactory.cs * r * fix duplicated code * Update ViewPortal.tsx * Update Loader.ts * Update Directory.Build.props * more small fixes * Update ViewFrame.tsx
1 parent 7a2402f commit afc9d2f

23 files changed

+384
-88
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<AssemblyVersion>2.0.0.0</AssemblyVersion>
66
<FileVersion>2.0.0.0</FileVersion>
77
<!-- Please see https://github.com/OutSystems/reactview?tab=readme-ov-file#versioning for versioning rules -->
8-
<Version>5.120.1</Version>
8+
<Version>5.120.2</Version>
99
<Authors>OutSystems</Authors>
1010
<Product>ReactView</Product>
1111
<Copyright>Copyright © OutSystems 2023</Copyright>

ReactViewControl/ReactView.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public abstract partial class ReactView : IDisposable {
1919

2020
private static ReactViewRender CreateReactViewInstance(ReactViewFactory factory) {
2121
ReactViewRender InnerCreateView() {
22-
var view = new ReactViewRender(factory.DefaultStyleSheet, () => factory.InitializePlugins(), factory.EnableViewPreload, factory.EnableDebugMode);
22+
var view = new ReactViewRender(factory.DefaultStyleSheet, () => factory.InitializePlugins(), factory.EnableViewPreload, factory.EnableDebugMode, factory.EnsureInnerViewsAreDisposed);
2323
if (factory.ShowDeveloperTools) {
2424
view.ShowDeveloperTools();
2525
}

ReactViewControl/ReactViewFactory.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,7 @@ public class ReactViewFactory {
3030
/// The view is cached and preloaded. First render occurs earlier.
3131
/// </summary>
3232
public virtual bool EnableViewPreload => true;
33+
34+
public virtual bool EnsureInnerViewsAreDisposed => true;
3335
}
34-
}
36+
}

ReactViewControl/ReactViewRender.LoaderModule.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public LoaderModule(ReactViewRender viewRender) {
2222
/// <summary>
2323
/// Loads the specified react component into the specified frame
2424
/// </summary>
25-
public void LoadComponent(IViewModule component, string frameName, bool hasStyleSheet, bool hasPlugins) {
25+
public void LoadComponent(IViewModule component, string frameName, bool hasStyleSheet, bool hasPlugins, bool ensureDisposeInnerViews) {
2626
var mainSource = ViewRender.ToFullUrl(NormalizeUrl(component.MainJsSource));
2727
var dependencySources = component.DependencyJsSources.Select(s => ViewRender.ToFullUrl(NormalizeUrl(s))).ToArray();
2828
var cssSources = component.CssSources.Select(s => ViewRender.ToFullUrl(NormalizeUrl(s))).ToArray();
@@ -56,6 +56,7 @@ public void LoadComponent(IViewModule component, string frameName, bool hasStyle
5656
componentSerialization,
5757
JavascriptSerializer.Serialize(frameName),
5858
JavascriptSerializer.Serialize(componentHash),
59+
JavascriptSerializer.Serialize(ensureDisposeInnerViews),
5960
};
6061

6162
ExecuteLoaderFunction("loadComponent", loadArgs);

ReactViewControl/ReactViewRender.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ internal partial class ReactViewRender : IChildViewHost, IDisposable {
3535
private bool enableDebugMode;
3636
private ResourceUrl defaultStyleSheet;
3737
private bool isInputDisabled; // used primarly to control the intention to disable input (before the browser is ready)
38+
private readonly bool ensureDisposeInnerViews;
3839

39-
public ReactViewRender(ResourceUrl defaultStyleSheet, Func<IViewModule[]> initializePlugins, bool preloadWebView, bool enableDebugMode) {
40+
public ReactViewRender(ResourceUrl defaultStyleSheet, Func<IViewModule[]> initializePlugins, bool preloadWebView, bool enableDebugMode, bool ensureInnerViewsAreDisposed) {
41+
this.ensureDisposeInnerViews = ensureInnerViewsAreDisposed;
4042
UserCallingAssembly = GetUserCallingMethod().ReflectedType.Assembly;
4143

4244
// must useSharedDomain for the local storage to be shared
@@ -68,12 +70,12 @@ public ReactViewRender(ResourceUrl defaultStyleSheet, Func<IViewModule[]> initia
6870

6971
ExtraInitialize();
7072

71-
var urlParams = new string[] {
73+
var urlParams = new[] {
7274
new ResourceUrl(ResourcesAssembly).ToString(),
7375
enableDebugMode ? "true" : "false",
7476
ExecutionEngine.ModulesObjectName,
7577
NativeAPI.NativeObjectName,
76-
ResourceUrl.CustomScheme + Uri.SchemeDelimiter + CustomResourceBaseUrl
78+
ResourceUrl.CustomScheme + Uri.SchemeDelimiter + CustomResourceBaseUrl,
7779
};
7880

7981
WebView.LoadResource(new ResourceUrl(ResourcesAssembly, ReactViewResources.Resources.DefaultUrl + "?" + string.Join("&", urlParams)));
@@ -270,7 +272,7 @@ private void TryLoadComponent(FrameInfo frame) {
270272

271273
RegisterNativeObject(frame.Component, frame);
272274

273-
Loader.LoadComponent(frame.Component, frame.Name, DefaultStyleSheet != null, frame.Plugins.Length > 0);
275+
Loader.LoadComponent(frame.Component, frame.Name, DefaultStyleSheet != null, frame.Plugins.Length > 0, ensureDisposeInnerViews);
274276
if (isInputDisabled && frame.IsMain) {
275277
Loader.DisableMouseInteractions();
276278
}
Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,41 @@
11
import * as React from "react";
22
import * as ReactDOM from "react-dom";
3-
import { ViewMetadataContext } from "../Internal/ViewMetadataContext";
3+
import { getEnsureDisposeInnerViewsFlag, ViewMetadataContext } from "../Internal/ViewMetadataContext";
44
import { PluginsContext, PluginsContextHolder } from "../Public/PluginsContext";
55
import { formatUrl, ResourceLoader } from "../Public/ResourceLoader";
6-
import { handleError } from "./ErrorHandler";
7-
import { notifyViewDestroyed, notifyViewInitialized } from "./NativeAPI";
86
import { ViewMetadata } from "./ViewMetadata";
9-
import { ViewPortalsCollection } from "./ViewPortalsCollection";
10-
import { addView, deleteView } from "./ViewsCollection";
7+
import { onChildViewAdded, onChildViewRemoved, onChildViewErrorRaised } from "./ViewPortal";
8+
import { ViewPortalsCollectionLegacy } from "./ViewPortalsCollectionsLegacy";
119

1210
export function createView(componentClass: any, properties: {}, view: ViewMetadata, componentName: string) {
1311
componentClass.contextType = PluginsContext;
14-
1512
const makeResourceUrl = (resourceKey: string, ...params: string[]) => formatUrl(view.name, resourceKey, ...params);
1613

17-
return (
18-
<ViewMetadataContext.Provider value={view}>
14+
if(!getEnsureDisposeInnerViewsFlag()) {
15+
return <ViewMetadataContext.Provider value={view}>
1916
<PluginsContext.Provider value={new PluginsContextHolder(Array.from(view.modules.values()))}>
2017
<ResourceLoader.Provider value={makeResourceUrl}>
21-
<ViewPortalsCollection views={view.childViews}
18+
<ViewPortalsCollectionLegacy views={view.childViews}
2219
viewAdded={onChildViewAdded}
2320
viewRemoved={onChildViewRemoved}
2421
viewErrorRaised={onChildViewErrorRaised} />
2522
{React.createElement(componentClass, { ref: e => view.modules.set(componentName, e), ...properties })}
2623
</ResourceLoader.Provider>
2724
</PluginsContext.Provider>
25+
</ViewMetadataContext.Provider>;
26+
}
27+
28+
return (
29+
<ViewMetadataContext.Provider value={view}>
30+
<PluginsContext.Provider value={new PluginsContextHolder(Array.from(view.modules.values()))}>
31+
<ResourceLoader.Provider value={makeResourceUrl}>
32+
{React.createElement(componentClass, { ref: e => view.modules.set(componentName, e), ...properties })}
33+
</ResourceLoader.Provider>
34+
</PluginsContext.Provider>
2835
</ViewMetadataContext.Provider>
2936
);
3037
}
3138

3239
export function renderMainView(children: React.ReactElement, container: Element) {
3340
return new Promise<void>(resolve => ReactDOM.hydrate(children, container, resolve));
3441
}
35-
36-
function onChildViewAdded(childView: ViewMetadata) {
37-
addView(childView.name, childView);
38-
notifyViewInitialized(childView.name);
39-
}
40-
41-
function onChildViewRemoved(childView: ViewMetadata) {
42-
deleteView(childView.name);
43-
notifyViewDestroyed(childView.name);
44-
}
45-
46-
function onChildViewErrorRaised(childView: ViewMetadata, error: Error) {
47-
handleError(error, childView);
48-
}
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
import * as React from "react";
22
import { ViewMetadata } from "./ViewMetadata";
33

4-
export const ViewMetadataContext = React.createContext<ViewMetadata>(null!);
4+
const EnsureDisposeInnerViewsFlagKey = "ENSURE_DISPOSE_INNER_VIEWS";
5+
6+
export const ViewMetadataContext = React.createContext<ViewMetadata>(null!);
7+
8+
export function getEnsureDisposeInnerViewsFlag(): boolean {
9+
return !!window[EnsureDisposeInnerViewsFlagKey];
10+
}
11+
12+
export function setEnsureDisposeInnerViewsFlag(ensureDisposeInnerViews: boolean): void {
13+
window[EnsureDisposeInnerViewsFlagKey] = ensureDisposeInnerViews;
14+
}

ReactViewResources/Loader/Internal/ViewPortal.tsx

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,48 @@ import { webViewRootId } from "../Internal/Environment";
33
import { getStylesheets } from "./Common";
44
import { ViewMetadata } from "./ViewMetadata";
55
import { ViewSharedContext } from "../Public/ViewSharedContext";
6+
import { addView, deleteView } from "./ViewsCollection";
7+
import { notifyViewDestroyed, notifyViewInitialized } from "./NativeAPI";
8+
import { handleError } from "./ErrorHandler";
69

710
export type ViewLifecycleEventHandler = (view: ViewMetadata) => void;
811
export type ViewErrorHandler = (view: ViewMetadata, error: Error) => void;
912

1013
export interface IViewPortalProps {
1114
view: ViewMetadata
12-
viewMounted: ViewLifecycleEventHandler;
13-
viewUnmounted: ViewLifecycleEventHandler;
14-
viewErrorRaised: ViewErrorHandler;
15+
shadowRoot: Element;
1516
}
1617

1718
interface IViewPortalState {
1819
component: React.ReactElement;
1920
}
2021

22+
export function onChildViewAdded(childView: ViewMetadata) {
23+
addView(childView.name, childView);
24+
notifyViewInitialized(childView.name);
25+
}
26+
27+
export function onChildViewRemoved(childView: ViewMetadata) {
28+
deleteView(childView.name);
29+
notifyViewDestroyed(childView.name);
30+
}
31+
32+
export function onChildViewErrorRaised(childView: ViewMetadata, error: Error) {
33+
handleError(error, childView);
34+
}
35+
2136
/**
2237
* A ViewPortal is were a view is rendered. The view DOM is then moved into the appropriate placeholder.
2338
* This way we avoid a view being recreated (and losing state) when its ViewFrame is moved in the tree.
24-
*
25-
* A View Frame notifies its sibling view collection when a new instance is mounted.
26-
* Upon mount, a View Portal is created and it will be responsible for rendering its view component in the shadow dom.
27-
* A view portal is persisted until its View Frame counterpart disappears.
28-
* */
39+
*/
2940
export class ViewPortal extends React.Component<IViewPortalProps, IViewPortalState> {
30-
31-
private head: Element;
32-
private shadowRoot: HTMLElement;
41+
private head: HTMLElement;
3342

3443
constructor(props: IViewPortalProps, context: any) {
3544
super(props, context);
3645

3746
this.state = { component: null! };
3847

39-
this.shadowRoot = props.view.placeholder.attachShadow({ mode: "open" }).getRootNode() as HTMLElement;
40-
4148
props.view.renderHandler = component => this.renderPortal(component);
4249
}
4350

@@ -67,16 +74,16 @@ export class ViewPortal extends React.Component<IViewPortalProps, IViewPortalSta
6774
const stylesheets = getStylesheets(document.head).filter(s => s.dataset.sticky === "true");
6875
stylesheets.forEach(s => this.head.appendChild(document.importNode(s, true)));
6976

70-
this.props.viewMounted(this.props.view);
77+
onChildViewAdded(this.props.view);
7178
}
7279

7380
public componentWillUnmount() {
74-
this.props.viewUnmounted(this.props.view);
81+
onChildViewRemoved(this.props.view);
7582
}
7683

7784
public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
7885
// execute error handling inside promise, to avoid the error handler to rethrow exception inside componentDidCatch
79-
Promise.resolve(null).then(() => this.props.viewErrorRaised(this.props.view, error));
86+
Promise.resolve(null).then(() => onChildViewErrorRaised(this.props.view, error));
8087
}
8188

8289
public render(): React.ReactNode {
@@ -90,6 +97,6 @@ export class ViewPortal extends React.Component<IViewPortalProps, IViewPortalSta
9097
</div>
9198
</body>
9299
</>,
93-
this.shadowRoot);
100+
this.props.shadowRoot);
94101
}
95-
}
102+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import * as React from "react";
2+
import { webViewRootId } from "../Internal/Environment";
3+
import { getStylesheets } from "./Common";
4+
import { ViewMetadata } from "./ViewMetadata";
5+
import { ViewSharedContext } from "../Public/ViewSharedContext";
6+
7+
export type ViewLifecycleEventHandler = (view: ViewMetadata) => void;
8+
export type ViewErrorHandler = (view: ViewMetadata, error: Error) => void;
9+
10+
export interface IViewPortalProps {
11+
view: ViewMetadata
12+
viewMounted: ViewLifecycleEventHandler;
13+
viewUnmounted: ViewLifecycleEventHandler;
14+
viewErrorRaised: ViewErrorHandler;
15+
}
16+
17+
interface IViewPortalState {
18+
component: React.ReactElement;
19+
}
20+
21+
/**
22+
* A ViewPortal is were a view is rendered. The view DOM is then moved into the appropriate placeholder.
23+
* This way we avoid a view being recreated (and losing state) when its ViewFrame is moved in the tree.
24+
*
25+
* A View Frame notifies its sibling view collection when a new instance is mounted.
26+
* Upon mount, a View Portal is created and it will be responsible for rendering its view component in the shadow dom.
27+
* A view portal is persisted until its View Frame counterpart disappears.
28+
* */
29+
export class ViewPortalLegacy extends React.Component<IViewPortalProps, IViewPortalState> {
30+
31+
private head: Element;
32+
private shadowRoot: HTMLElement;
33+
34+
constructor(props: IViewPortalProps, context: any) {
35+
super(props, context);
36+
37+
this.state = { component: null! };
38+
39+
this.shadowRoot = props.view.placeholder.attachShadow({ mode: "open" }).getRootNode() as HTMLElement;
40+
41+
props.view.renderHandler = component => this.renderPortal(component);
42+
}
43+
44+
private renderPortal(component: React.ReactElement) {
45+
const wrappedComponent = (
46+
<ViewSharedContext.Provider value={this.props.view.context}>
47+
{component}
48+
</ViewSharedContext.Provider>
49+
);
50+
return new Promise<void>(resolve => this.setState({ component: wrappedComponent }, resolve));
51+
}
52+
53+
public shouldComponentUpdate(nextProps: IViewPortalProps, nextState: IViewPortalState) {
54+
// only update if the component was set (once)
55+
return this.state.component === null && nextState.component !== this.state.component;
56+
}
57+
58+
public componentDidMount() {
59+
this.props.view.head = this.head;
60+
61+
const styleResets = document.createElement("style");
62+
styleResets.innerHTML = ":host { all: initial; display: block; }";
63+
64+
this.head.appendChild(styleResets);
65+
66+
// get sticky stylesheets
67+
const stylesheets = getStylesheets(document.head).filter(s => s.dataset.sticky === "true");
68+
stylesheets.forEach(s => this.head.appendChild(document.importNode(s, true)));
69+
70+
this.props.viewMounted(this.props.view);
71+
}
72+
73+
public componentWillUnmount() {
74+
this.props.viewUnmounted(this.props.view);
75+
}
76+
77+
public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
78+
// execute error handling inside promise, to avoid the error handler to rethrow exception inside componentDidCatch
79+
Promise.resolve(null).then(() => this.props.viewErrorRaised(this.props.view, error));
80+
}
81+
82+
public render(): React.ReactNode {
83+
return ReactDOM.createPortal(
84+
<>
85+
<head ref={e => this.head = e!}>
86+
</head>
87+
<body>
88+
<div id={webViewRootId} ref={e => this.props.view.root = e!}>
89+
{this.state.component ? this.state.component : null}
90+
</div>
91+
</body>
92+
</>,
93+
this.shadowRoot);
94+
}
95+
}

ReactViewResources/Loader/Internal/ViewPortalsCollection.tsx renamed to ReactViewResources/Loader/Internal/ViewPortalsCollectionsLegacy.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import * as React from "react";
1+
import * as React from "react";
22
import { ObservableListCollection } from "./ObservableCollection";
33
import { ViewMetadata } from "./ViewMetadata";
4-
import { ViewPortal, ViewLifecycleEventHandler, ViewErrorHandler } from "./ViewPortal";
4+
import { ViewPortalLegacy, ViewLifecycleEventHandler, ViewErrorHandler } from "./ViewPortalLegacy";
55
export { ViewLifecycleEventHandler, ViewErrorHandler } from "./ViewPortal";
66

77
interface IViewPortalsCollectionProps {
@@ -15,7 +15,7 @@ interface IViewPortalsCollectionProps {
1515
* Handles notifications from the views collection. Whenever a view is added or removed
1616
* the corresponding ViewPortal is added or removed
1717
* */
18-
export class ViewPortalsCollection extends React.Component<IViewPortalsCollectionProps> {
18+
export class ViewPortalsCollectionLegacy extends React.Component<IViewPortalsCollectionProps> {
1919

2020
constructor(props: IViewPortalsCollectionProps, context: any) {
2121
super(props, context);
@@ -28,7 +28,7 @@ export class ViewPortalsCollection extends React.Component<IViewPortalsCollectio
2828

2929
private renderViewPortal(view: ViewMetadata) {
3030
return (
31-
<ViewPortal key={view.name}
31+
<ViewPortalLegacy key={view.name}
3232
view={view}
3333
viewMounted={this.props.viewAdded}
3434
viewUnmounted={this.props.viewRemoved}

0 commit comments

Comments
 (0)