Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 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.123.0</Version>
<Authors>OutSystems</Authors>
<Product>ReactView</Product>
<Copyright>Copyright © OutSystems 2023</Copyright>
Expand All @@ -14,6 +14,9 @@

<!-- File with mtime of last successful npm install -->
<NpmInstallStampFile>.npm-install-stamp</NpmInstallStampFile>

<!-- Enable building Windows-targeted projects on non-Windows platforms -->
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>

<PropertyGroup>
Expand Down
4 changes: 4 additions & 0 deletions ReactViewResources/AMDLoader/AMDLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace AMDLoader {
export let timeout = 5000;

export function getOrCreateDependencyPromise(name: string): Promise<any> {
console.log("AMDLoader :: getOrCreateDependencyPromise: ", name);
name = name.replace(/^.\//, "").toLowerCase();
if (!promises[name]) {
promises[name] = new Promise((resolve, reject) => {
Expand All @@ -29,12 +30,14 @@ namespace AMDLoader {
}

export function resolve(name: string, value: any): void {
console.log("AMDLoader :: resolve: ", name);1
getOrCreateDependencyPromise(name); // create promise if necessary
resolves[name](value);
defines[name] = true;
}

export function require(deps: string[], definition: Function): void {
console.log("AMDLoader :: require: ", deps);
if (!deps || deps.length === 0) {
definition.apply(null, []);
return;
Expand All @@ -49,6 +52,7 @@ namespace AMDLoader {
}

const define = function (name: string, deps: string[], definition: Function): void {
console.log("AMDLoader :: define: ", name);
if (typeof name !== "string") {
throw new Error("Unnamed modules are not supported");
}
Expand Down
44 changes: 23 additions & 21 deletions ReactViewResources/Loader/Internal/ComponentsRenderCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,29 @@ export interface IRenderCacheEntry {
}

export async function renderCachedView(view: ViewMetadata, componentSource: string, componentPropertiesHash: string): Promise<IRenderCacheEntry | null> {
if (!view.isMain) {
// disable render from cache for inner views, since react does not currently support portals hydration
return null;
}

const componentCacheKey = componentSource + "|" + componentPropertiesHash;

const cachedComponentHtml = localStorage.getItem(componentCacheKey);
if (cachedComponentHtml) {
// render cached component html to reduce time to first render
view.root!.innerHTML = cachedComponentHtml;
await waitForNextPaint();

// already on cache, skip storing on cache
return null;
}

return {
cacheKey: componentCacheKey,
componentSource: componentSource
};
return null;
//
// if (!view.isMain) {
// // disable render from cache for inner views, since react does not currently support portals hydration
// return null;
// }
//
// const componentCacheKey = componentSource + "|" + componentPropertiesHash;
//
// const cachedComponentHtml = localStorage.getItem(componentCacheKey);
// if (cachedComponentHtml) {
// // render cached component html to reduce time to first render
// view.root!.innerHTML = cachedComponentHtml;
// await waitForNextPaint();
//
// // already on cache, skip storing on cache
// return null;
// }
//
// return {
// cacheKey: componentCacheKey,
// componentSource: componentSource
// };
}

export function storeViewRenderInCache(view: ViewMetadata, cacheEntry: IRenderCacheEntry, maxPreRenderedCacheEntries: number): Promise<void> {
Expand Down
31 changes: 24 additions & 7 deletions ReactViewResources/Loader/Internal/Loader.View.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,36 @@ import { ViewMetadata } from "./ViewMetadata";
import { ViewPortalsCollection } from "./ViewPortalsCollection";
import { addView, deleteView } from "./ViewsCollection";

export function createView(componentClass: any, properties: {}, view: ViewMetadata, componentName: string) {
componentClass.contextType = PluginsContext;
export function createView(componentClass: any, properties: {}, view: ViewMetadata, componentName: string, componentNativeObject: any, componentNativeObjectName: string) {
return <View componentClass={componentClass} properties={properties} view={view} componentName={componentName} componentNativeObject={componentNativeObject} componentNativeObjectName={componentNativeObjectName} />;
}

interface IViewProps {
componentClass: any;
properties: {};
view: ViewMetadata;
componentName: string
componentNativeObject: any;
componentNativeObjectName: string
}

const View = ({ componentClass, properties, view, componentName, componentNativeObject, componentNativeObjectName }: IViewProps) => {
componentClass.contextType = PluginsContext;
const makeResourceUrl = (resourceKey: string, ...params: string[]) => formatUrl(view.name, resourceKey, ...params);

const pluginsContext = React.useRef(new PluginsContextHolder(Array.from(view.modules.values())));

React.useEffect(() => {
return () => {
pluginsContext.current.dispose();
pluginsContext.current = null!;
}
}, []);

return (
<ViewMetadataContext.Provider value={view}>
<PluginsContext.Provider value={new PluginsContextHolder(Array.from(view.modules.values()))}>
<PluginsContext.Provider value={pluginsContext.current}>
<ResourceLoader.Provider value={makeResourceUrl}>
<ViewPortalsCollection views={view.childViews}
viewAdded={onChildViewAdded}
viewRemoved={onChildViewRemoved}
viewErrorRaised={onChildViewErrorRaised} />
{React.createElement(componentClass, { ref: e => view.modules.set(componentName, e), ...properties })}
</ResourceLoader.Provider>
</PluginsContext.Provider>
Expand Down
6 changes: 4 additions & 2 deletions ReactViewResources/Loader/Internal/NativeAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,7 @@ export function notifyViewLoaded(viewName: string, id: string): void {
}

export function notifyViewDestroyed(viewName: string): void {
withAPI(api => api.notifyViewDestroyed(viewName));
}
withAPI(api => {
api.notifyViewDestroyed(viewName)
});
}
91 changes: 75 additions & 16 deletions ReactViewResources/Loader/Internal/ResourcesLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,47 @@ import { Task } from "./Task";
import { ViewMetadata } from "./ViewMetadata";
import { getView } from "./ViewsCollection";

console.log("Loading resources loader file...");
const LoadedScriptsKey = "LOADED_SCRIPTS_KEY";
const ScriptLoadTasksKey = "SCRIPT_LOAD_TASKS_KEY";
let loadedScripts: Set<string> = window[LoadedScriptsKey];
let scriptLoadTasks: Map<string, Task<void>> = window[ScriptLoadTasksKey];

if (!loadedScripts) {
console.log("Create new 'loadedScripts' SET !!");
loadedScripts = new Set<string>();
window[LoadedScriptsKey] = loadedScripts;
}

if (!scriptLoadTasks) {
console.log("Create new 'scriptLoadTasks' MAP !!");
scriptLoadTasks = new Map<string, Task<void>>();
window[LoadedScriptsKey] = loadedScripts;
}

export function loadScript(scriptSrc: string, view: ViewMetadata): Promise<void> {
return new Promise(async (resolve) => {
const frameScripts = view.scriptsLoadTasks;

// check if script was already added, fallback to main frame
const scriptLoadTask = frameScripts.get(scriptSrc) || !view.isMain ? getView(mainFrameName).scriptsLoadTasks.get(scriptSrc) : null;
const scriptLoadTask = scriptLoadTasks.get(scriptSrc) || null;
if (scriptLoadTask) {
console.log("Wait loading TASK for: " + scriptSrc);
// wait for script to be loaded
await scriptLoadTask.promise;
scriptLoadTasks.delete(scriptSrc);
resolve();
return;
}

if(loadedScripts.has(scriptSrc)) {
console.log("Load skipped for: " + scriptSrc);
resolve();
return;
}

console.log("Load script Task: " + scriptSrc);
loadedScripts.add(scriptSrc);
const loadTask = new Task<void>();
frameScripts.set(scriptSrc, loadTask);
scriptLoadTasks.set(scriptSrc, loadTask);

const script = document.createElement("script");
script.src = scriptSrc;
Expand Down Expand Up @@ -54,18 +80,51 @@ export function loadStyleSheet(stylesheet: string, containerElement: Element, ma
}

function waitForLoad<T extends HTMLElement>(element: T, url: string, timeout: number): Promise<T> {
return new Promise((resolve) => {
const timeoutHandle = setTimeout(
() => {
if (isDebugModeEnabled) {
showWarningMessage(`Timeout loading resouce: '${url}'. If you paused the application to debug, you may disregard this message.`);
}
},
timeout);

element.addEventListener("load", () => {
clearTimeout(timeoutHandle);
return new Promise((resolve, reject) => {
// const timeoutHandle = setTimeout(
// () => {
// if (isDebugModeEnabled) {
// showWarningMessage(`Timeout loading resouce: '${url}'. If you paused the application to debug, you may disregard this message.`);
// }
// },
// timeout);

// element.addEventListener("load", () => {
// clearTimeout(timeoutHandle);
// resolve(element);
// });

let timeoutHandle: number | undefined;

// Define handlers ahead of time
const loadHandler = () => {
cleanup();
resolve(element);
});
};

const errorHandler = () => {
cleanup();
reject(new Error(`Failed to load resource: '${url}'`));
};

// This central function is key to disposing of everything
const cleanup = () => {
clearTimeout(timeoutHandle);
element.removeEventListener("load", loadHandler);
element.removeEventListener("error", errorHandler);
};

// Set the timeout to reject the promise and clean up
timeoutHandle = setTimeout(() => {
cleanup();
const message = `Timeout loading resource: '${url}'.`;
if (isDebugModeEnabled) {
showWarningMessage(`${message} If you paused the application to debug, you may disregard this message.`);
}
reject(new Error(message));
}, timeout);

element.addEventListener("load", loadHandler);
element.addEventListener("error", errorHandler);
});
}
2 changes: 0 additions & 2 deletions ReactViewResources/Loader/Internal/ViewMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export type ViewMetadata = {
placeholder: Element; // element were the view is mounted (where the shadow root is mounted in case of child views)
root?: Element; // view root element
head?: Element; // view head element
scriptsLoadTasks: Map<string, Task<void>>; // maps scripts urls to load tasks
pluginsLoadTask: Task<void>; // plugins load task
viewLoadTask: Task<void>; // resolved when view is loaded
modules: Map<string, any>; // maps module name to module instance
Expand All @@ -33,7 +32,6 @@ export function newView(id: number, name: string, isMain: boolean, placeholder:
nativeObjectNames: [],
pluginsLoadTask: new Task(),
viewLoadTask: new Task(),
scriptsLoadTasks: new Map<string, Task<void>>(),
childViews: new ObservableListCollection<ViewMetadata>(),
context: null,
parentView: null!
Expand Down
17 changes: 11 additions & 6 deletions ReactViewResources/Loader/Internal/ViewPortal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ interface IViewPortalState {
* */
export class ViewPortal extends React.Component<IViewPortalProps, IViewPortalState> {

private head: Element;
private shadowRoot: HTMLElement;
private head: Element | null = null;
private shadowRoot: HTMLElement | null = null;

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

debugger;
this.state = { component: null! };

this.shadowRoot = props.view.placeholder.attachShadow({ mode: "open" }).getRootNode() as HTMLElement;
Expand All @@ -42,6 +43,7 @@ export class ViewPortal extends React.Component<IViewPortalProps, IViewPortalSta
}

private renderPortal(component: React.ReactElement) {
debugger;
const wrappedComponent = (
<ViewSharedContext.Provider value={this.props.view.context}>
{component}
Expand All @@ -56,21 +58,24 @@ export class ViewPortal extends React.Component<IViewPortalProps, IViewPortalSta
}

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

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

this.head.appendChild(styleResets);
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)));
stylesheets.forEach(s => this.head!.appendChild(document.importNode(s, true)));

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

public componentWillUnmount() {
this.head = null;
this.shadowRoot = null;
this.props.viewUnmounted(this.props.view);
}

Expand All @@ -90,6 +95,6 @@ export class ViewPortal extends React.Component<IViewPortalProps, IViewPortalSta
</div>
</body>
</>,
this.shadowRoot);
this.shadowRoot!);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export function createPropertiesProxy(rootElement: Element, objProperties: {}, n
} else {
proxy[key] = async function () {
const nativeObject = window[nativeObjName] || await bindNativeObject(nativeObjName);

const result = nativeObject[key].apply(window, arguments);

if (componentRenderedWaitTask) {
Expand Down
5 changes: 4 additions & 1 deletion ReactViewResources/Loader/Internal/ViewsCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { modulesFunctionName } from "./Environment";

const views = new Map<string, ViewMetadata>();

window["my-views"] = views; // for debugging
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete


export function addView(name: string, view: ViewMetadata): void {
views.set(name, view);
}
Expand All @@ -23,6 +25,7 @@ export function getView(viewName: string): ViewMetadata {
return view;
}

console.log("modulesFunctionName", modulesFunctionName);
window[modulesFunctionName] = function getModule(viewName: string, id: string, moduleName: string) {
const view = views.get(viewName);
if (view && view.id.toString() === id) {
Expand All @@ -35,7 +38,7 @@ window[modulesFunctionName] = function getModule(viewName: string, id: string, m

return new Proxy({}, {
get: function () {
// return a dummy function, call will be ingored, but no exception thrown
// return a dummy function, call will be ignored, but no exception thrown
return new Function();
}
});
Expand Down
Loading