Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3680069
Setup Sample App
heldergoncalves92 Nov 28, 2025
94cea36
Update TaskListView.tsx
heldergoncalves92 Nov 28, 2025
36dccd1
Update TaskListView.tsx
heldergoncalves92 Nov 28, 2025
b74b077
Update MainView.tsx
heldergoncalves92 Nov 28, 2025
7ca088c
First version
heldergoncalves92 Dec 2, 2025
2a10ed2
Remove console log
heldergoncalves92 Dec 3, 2025
9a4c4f7
Send FT to View
heldergoncalves92 Dec 3, 2025
5dad99b
Update ReactViewFactory.cs
heldergoncalves92 Dec 3, 2025
a4aebcf
Add legacy files again
heldergoncalves92 Dec 3, 2025
c1086a0
Update ViewPortalsCollections.tsx
heldergoncalves92 Dec 3, 2025
d1dbbb0
Update ViewPortalsCollectionsLegacy.tsx
heldergoncalves92 Dec 3, 2025
ac39aad
Update ViewPortalLegacy.tsx
heldergoncalves92 Dec 3, 2025
4e0bc1b
Update Loader.View.tsx
heldergoncalves92 Dec 3, 2025
bc75cd2
Update Loader.View.tsx
heldergoncalves92 Dec 3, 2025
49066cf
Update Loader.View.tsx
heldergoncalves92 Dec 3, 2025
95e7c56
Update Loader.View.tsx
heldergoncalves92 Dec 3, 2025
2cc5e83
Update Loader.View.tsx
heldergoncalves92 Dec 3, 2025
62afa92
Update ViewMetadataContext.ts
heldergoncalves92 Dec 3, 2025
14f9b98
d
heldergoncalves92 Dec 3, 2025
73aba24
Update ViewPortal.tsx
heldergoncalves92 Dec 3, 2025
e3d093c
Update ReactViewFactory.cs
heldergoncalves92 Dec 3, 2025
5e7e9ae
r
heldergoncalves92 Dec 3, 2025
9c4f96d
fix duplicated code
heldergoncalves92 Dec 12, 2025
8ac6c22
Update ViewPortal.tsx
heldergoncalves92 Dec 12, 2025
54c5529
Update Loader.ts
heldergoncalves92 Dec 12, 2025
e9eb108
Update Directory.Build.props
heldergoncalves92 Dec 12, 2025
d9574c3
more small fixes
heldergoncalves92 Dec 12, 2025
19ea575
Update ViewFrame.tsx
heldergoncalves92 Dec 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<AssemblyVersion>2.0.0.0</AssemblyVersion>
<FileVersion>2.0.0.0</FileVersion>
<!-- Please see https://github.com/OutSystems/reactview?tab=readme-ov-file#versioning for versioning rules -->
<Version>5.120.1</Version>
<Version>5.120.2</Version>
<Authors>OutSystems</Authors>
<Product>ReactView</Product>
<Copyright>Copyright © OutSystems 2023</Copyright>
Expand Down
2 changes: 1 addition & 1 deletion ReactViewControl/ReactView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public abstract partial class ReactView : IDisposable {

private static ReactViewRender CreateReactViewInstance(ReactViewFactory factory) {
ReactViewRender InnerCreateView() {
var view = new ReactViewRender(factory.DefaultStyleSheet, () => factory.InitializePlugins(), factory.EnableViewPreload, factory.EnableDebugMode);
var view = new ReactViewRender(factory.DefaultStyleSheet, () => factory.InitializePlugins(), factory.EnableViewPreload, factory.EnableDebugMode, factory.EnsureInnerViewsAreDisposed);
if (factory.ShowDeveloperTools) {
view.ShowDeveloperTools();
}
Expand Down
4 changes: 3 additions & 1 deletion ReactViewControl/ReactViewFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@ public class ReactViewFactory {
/// The view is cached and preloaded. First render occurs earlier.
/// </summary>
public virtual bool EnableViewPreload => true;

public virtual bool EnsureInnerViewsAreDisposed => true;
}
}
}
3 changes: 2 additions & 1 deletion ReactViewControl/ReactViewRender.LoaderModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public LoaderModule(ReactViewRender viewRender) {
/// <summary>
/// Loads the specified react component into the specified frame
/// </summary>
public void LoadComponent(IViewModule component, string frameName, bool hasStyleSheet, bool hasPlugins) {
public void LoadComponent(IViewModule component, string frameName, bool hasStyleSheet, bool hasPlugins, bool ensureDisposeInnerViews) {
var mainSource = ViewRender.ToFullUrl(NormalizeUrl(component.MainJsSource));
var dependencySources = component.DependencyJsSources.Select(s => ViewRender.ToFullUrl(NormalizeUrl(s))).ToArray();
var cssSources = component.CssSources.Select(s => ViewRender.ToFullUrl(NormalizeUrl(s))).ToArray();
Expand Down Expand Up @@ -56,6 +56,7 @@ public void LoadComponent(IViewModule component, string frameName, bool hasStyle
componentSerialization,
JavascriptSerializer.Serialize(frameName),
JavascriptSerializer.Serialize(componentHash),
JavascriptSerializer.Serialize(ensureDisposeInnerViews),
};

ExecuteLoaderFunction("loadComponent", loadArgs);
Expand Down
10 changes: 6 additions & 4 deletions ReactViewControl/ReactViewRender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ internal partial class ReactViewRender : IChildViewHost, IDisposable {
private bool enableDebugMode;
private ResourceUrl defaultStyleSheet;
private bool isInputDisabled; // used primarly to control the intention to disable input (before the browser is ready)
private readonly bool ensureDisposeInnerViews;

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

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

ExtraInitialize();

var urlParams = new string[] {
var urlParams = new[] {
new ResourceUrl(ResourcesAssembly).ToString(),
enableDebugMode ? "true" : "false",
ExecutionEngine.ModulesObjectName,
NativeAPI.NativeObjectName,
ResourceUrl.CustomScheme + Uri.SchemeDelimiter + CustomResourceBaseUrl
ResourceUrl.CustomScheme + Uri.SchemeDelimiter + CustomResourceBaseUrl,
};

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

RegisterNativeObject(frame.Component, frame);

Loader.LoadComponent(frame.Component, frame.Name, DefaultStyleSheet != null, frame.Plugins.Length > 0);
Loader.LoadComponent(frame.Component, frame.Name, DefaultStyleSheet != null, frame.Plugins.Length > 0, ensureDisposeInnerViews);
if (isInputDisabled && frame.IsMain) {
Loader.DisableMouseInteractions();
}
Expand Down
39 changes: 16 additions & 23 deletions ReactViewResources/Loader/Internal/Loader.View.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,41 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { ViewMetadataContext } from "../Internal/ViewMetadataContext";
import { getEnsureDisposeInnerViewsFlag, ViewMetadataContext } from "../Internal/ViewMetadataContext";
import { PluginsContext, PluginsContextHolder } from "../Public/PluginsContext";
import { formatUrl, ResourceLoader } from "../Public/ResourceLoader";
import { handleError } from "./ErrorHandler";
import { notifyViewDestroyed, notifyViewInitialized } from "./NativeAPI";
import { ViewMetadata } from "./ViewMetadata";
import { ViewPortalsCollection } from "./ViewPortalsCollection";
import { addView, deleteView } from "./ViewsCollection";
import { onChildViewAdded, onChildViewRemoved, onChildViewErrorRaised } from "./ViewPortal";
import { ViewPortalsCollectionLegacy } from "./ViewPortalsCollectionsLegacy";

export function createView(componentClass: any, properties: {}, view: ViewMetadata, componentName: string) {
componentClass.contextType = PluginsContext;

const makeResourceUrl = (resourceKey: string, ...params: string[]) => formatUrl(view.name, resourceKey, ...params);

return (
<ViewMetadataContext.Provider value={view}>
if(!getEnsureDisposeInnerViewsFlag()) {
return <ViewMetadataContext.Provider value={view}>
<PluginsContext.Provider value={new PluginsContextHolder(Array.from(view.modules.values()))}>
<ResourceLoader.Provider value={makeResourceUrl}>
<ViewPortalsCollection views={view.childViews}
<ViewPortalsCollectionLegacy views={view.childViews}
viewAdded={onChildViewAdded}
viewRemoved={onChildViewRemoved}
viewErrorRaised={onChildViewErrorRaised} />
{React.createElement(componentClass, { ref: e => view.modules.set(componentName, e), ...properties })}
</ResourceLoader.Provider>
</PluginsContext.Provider>
</ViewMetadataContext.Provider>;
}

return (
<ViewMetadataContext.Provider value={view}>
<PluginsContext.Provider value={new PluginsContextHolder(Array.from(view.modules.values()))}>
<ResourceLoader.Provider value={makeResourceUrl}>
{React.createElement(componentClass, { ref: e => view.modules.set(componentName, e), ...properties })}
</ResourceLoader.Provider>
</PluginsContext.Provider>
</ViewMetadataContext.Provider>
);
}

export function renderMainView(children: React.ReactElement, container: Element) {
return new Promise<void>(resolve => ReactDOM.hydrate(children, container, resolve));
}

function onChildViewAdded(childView: ViewMetadata) {
addView(childView.name, childView);
notifyViewInitialized(childView.name);
}

function onChildViewRemoved(childView: ViewMetadata) {
deleteView(childView.name);
notifyViewDestroyed(childView.name);
}

function onChildViewErrorRaised(childView: ViewMetadata, error: Error) {
handleError(error, childView);
}
12 changes: 11 additions & 1 deletion ReactViewResources/Loader/Internal/ViewMetadataContext.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import * as React from "react";
import { ViewMetadata } from "./ViewMetadata";

export const ViewMetadataContext = React.createContext<ViewMetadata>(null!);
const EnsureDisposeInnerViewsFlagKey = "ENSURE_DISPOSE_INNER_VIEWS";

export const ViewMetadataContext = React.createContext<ViewMetadata>(null!);

export function getEnsureDisposeInnerViewsFlag(): boolean {
return !!window[EnsureDisposeInnerViewsFlagKey];
}

export function setEnsureDisposeInnerViewsFlag(ensureDisposeInnerViews: boolean): void {
window[EnsureDisposeInnerViewsFlagKey] = ensureDisposeInnerViews;
}
43 changes: 25 additions & 18 deletions ReactViewResources/Loader/Internal/ViewPortal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,48 @@ import { webViewRootId } from "../Internal/Environment";
import { getStylesheets } from "./Common";
import { ViewMetadata } from "./ViewMetadata";
import { ViewSharedContext } from "../Public/ViewSharedContext";
import { addView, deleteView } from "./ViewsCollection";
import { notifyViewDestroyed, notifyViewInitialized } from "./NativeAPI";
import { handleError } from "./ErrorHandler";

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

export interface IViewPortalProps {
view: ViewMetadata
viewMounted: ViewLifecycleEventHandler;
viewUnmounted: ViewLifecycleEventHandler;
viewErrorRaised: ViewErrorHandler;
shadowRoot: Element;
}

interface IViewPortalState {
component: React.ReactElement;
}

export function onChildViewAdded(childView: ViewMetadata) {
addView(childView.name, childView);
notifyViewInitialized(childView.name);
}

export function onChildViewRemoved(childView: ViewMetadata) {
deleteView(childView.name);
notifyViewDestroyed(childView.name);
}

export function onChildViewErrorRaised(childView: ViewMetadata, error: Error) {
handleError(error, childView);
}

/**
* A ViewPortal is were a view is rendered. The view DOM is then moved into the appropriate placeholder.
* This way we avoid a view being recreated (and losing state) when its ViewFrame is moved in the tree.
*
* A View Frame notifies its sibling view collection when a new instance is mounted.
* Upon mount, a View Portal is created and it will be responsible for rendering its view component in the shadow dom.
* A view portal is persisted until its View Frame counterpart disappears.
* */
*/
export class ViewPortal extends React.Component<IViewPortalProps, IViewPortalState> {

private head: Element;
private shadowRoot: HTMLElement;
private head: HTMLElement;

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

this.state = { component: null! };

this.shadowRoot = props.view.placeholder.attachShadow({ mode: "open" }).getRootNode() as HTMLElement;

props.view.renderHandler = component => this.renderPortal(component);
}

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

this.props.viewMounted(this.props.view);
onChildViewAdded(this.props.view);
}

public componentWillUnmount() {
this.props.viewUnmounted(this.props.view);
onChildViewRemoved(this.props.view);
}

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

public render(): React.ReactNode {
Expand All @@ -90,6 +97,6 @@ export class ViewPortal extends React.Component<IViewPortalProps, IViewPortalSta
</div>
</body>
</>,
this.shadowRoot);
this.props.shadowRoot);
}
}
}
95 changes: 95 additions & 0 deletions ReactViewResources/Loader/Internal/ViewPortalLegacy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import * as React from "react";
import { webViewRootId } from "../Internal/Environment";
import { getStylesheets } from "./Common";
import { ViewMetadata } from "./ViewMetadata";
import { ViewSharedContext } from "../Public/ViewSharedContext";

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

export interface IViewPortalProps {
view: ViewMetadata
viewMounted: ViewLifecycleEventHandler;
viewUnmounted: ViewLifecycleEventHandler;
viewErrorRaised: ViewErrorHandler;
}

interface IViewPortalState {
component: React.ReactElement;
}

/**
* A ViewPortal is were a view is rendered. The view DOM is then moved into the appropriate placeholder.
* This way we avoid a view being recreated (and losing state) when its ViewFrame is moved in the tree.
*
* A View Frame notifies its sibling view collection when a new instance is mounted.
* Upon mount, a View Portal is created and it will be responsible for rendering its view component in the shadow dom.
* A view portal is persisted until its View Frame counterpart disappears.
* */
export class ViewPortalLegacy extends React.Component<IViewPortalProps, IViewPortalState> {

private head: Element;
private shadowRoot: HTMLElement;

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

this.state = { component: null! };

this.shadowRoot = props.view.placeholder.attachShadow({ mode: "open" }).getRootNode() as HTMLElement;

props.view.renderHandler = component => this.renderPortal(component);
}

private renderPortal(component: React.ReactElement) {
const wrappedComponent = (
<ViewSharedContext.Provider value={this.props.view.context}>
{component}
</ViewSharedContext.Provider>
);
return new Promise<void>(resolve => this.setState({ component: wrappedComponent }, resolve));
}

public shouldComponentUpdate(nextProps: IViewPortalProps, nextState: IViewPortalState) {
// only update if the component was set (once)
return this.state.component === null && nextState.component !== this.state.component;
}

public componentDidMount() {
this.props.view.head = this.head;

const styleResets = document.createElement("style");
styleResets.innerHTML = ":host { all: initial; display: block; }";

this.head.appendChild(styleResets);

// get sticky stylesheets
const stylesheets = getStylesheets(document.head).filter(s => s.dataset.sticky === "true");
stylesheets.forEach(s => this.head.appendChild(document.importNode(s, true)));

this.props.viewMounted(this.props.view);
}

public componentWillUnmount() {
this.props.viewUnmounted(this.props.view);
}

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

public render(): React.ReactNode {
return ReactDOM.createPortal(
<>
<head ref={e => this.head = e!}>
</head>
<body>
<div id={webViewRootId} ref={e => this.props.view.root = e!}>
{this.state.component ? this.state.component : null}
</div>
</body>
</>,
this.shadowRoot);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from "react";
import * as React from "react";
import { ObservableListCollection } from "./ObservableCollection";
import { ViewMetadata } from "./ViewMetadata";
import { ViewPortal, ViewLifecycleEventHandler, ViewErrorHandler } from "./ViewPortal";
import { ViewPortalLegacy, ViewLifecycleEventHandler, ViewErrorHandler } from "./ViewPortalLegacy";
export { ViewLifecycleEventHandler, ViewErrorHandler } from "./ViewPortal";

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

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

private renderViewPortal(view: ViewMetadata) {
return (
<ViewPortal key={view.name}
<ViewPortalLegacy key={view.name}
view={view}
viewMounted={this.props.viewAdded}
viewUnmounted={this.props.viewRemoved}
Expand Down
Loading
Loading