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
198 changes: 193 additions & 5 deletions src/component/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,173 @@ export function component<P, X, C, ExtType>(
const global = getGlobal(window);
const driverCache = {};
const instances = [];
let latestRenderedRerender: ?() => ZalgoPromise<void>;
const latestRenderStorageKey = `__zoid_latest_render__${tag}`;
const latestRenderContainerAttr = `data-zoid-latest-container-${tag}`;
const escapeAttr = (value: string): string => {
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
};
const waitForSelector = (
selector: string,
timeout: number = 3000,
interval: number = 100
): ZalgoPromise<string> => {
return new ZalgoPromise((resolve, reject) => {
const start = Date.now();

const check = () => {
try {
if (document.querySelector(selector)) {
resolve(selector);
return;
}
} catch (err) {
reject(err);
return;
}

if (Date.now() - start >= timeout) {
reject(
new Error(
`Timed out waiting for container selector to appear: ${selector}`
)
);
return;
}

setTimeout(check, interval);
};

check();
});
};

const readLatestRender = (): ?{|
container: string,
context: $Values<typeof CONTEXT>,
|} => {
try {
if (!window.sessionStorage) {
return;
}

const latestRender = window.sessionStorage.getItem(
latestRenderStorageKey
);
if (!latestRender) {
return;
}

const parsed = JSON.parse(latestRender);
const persistedContainers =
parsed && Array.isArray(parsed.containers)
? parsed.containers.filter(
(candidate) => typeof candidate === "string"
)
: [];

let candidates = persistedContainers;

if (candidates.length === 0) {
if (parsed && typeof parsed.container === "string") {
candidates = [parsed.container];
} else {
candidates = [];
}
}

if (!parsed || candidates.length === 0) {
return;
}

if (
parsed.context !== CONTEXT.IFRAME &&
parsed.context !== CONTEXT.POPUP
) {
return;
}

const container =
candidates.find((candidate) => {
try {
return Boolean(document.querySelector(candidate));
} catch (err) {
return false;
}
}) || candidates[0];

return {
container,
context: parsed.context,
};
} catch (err) {
// ignored
}
};

const writeLatestRender = (
container: ContainerReferenceType,
context: $Values<typeof CONTEXT>
) => {
let persistedContainer: ?string;
const persistedContainers = [];

if (typeof container === "string") {
persistedContainer = container;
persistedContainers.push(container);
} else if (isElement(container)) {
const elementID = container.getAttribute("id");
if (elementID) {
persistedContainers.push(`[id="${escapeAttr(elementID)}"]`);
}

const elementName = container.getAttribute("name");
if (elementName) {
persistedContainers.push(`[name="${escapeAttr(elementName)}"]`);
}

const className = container.className;
if (typeof className === "string" && className.trim()) {
const firstClass = className.trim().split(/\s+/)[0];
if (firstClass) {
persistedContainers.push(`.${firstClass}`);
}
}

let marker = container.getAttribute(latestRenderContainerAttr);

if (!marker) {
marker = uniqueID();
container.setAttribute(latestRenderContainerAttr, marker);
}

persistedContainers.push(
`[${latestRenderContainerAttr}="${escapeAttr(marker)}"]`
);
persistedContainer = persistedContainers[0];
}

if (!persistedContainer) {
return;
}

try {
if (!window.sessionStorage) {
return;
}

window.sessionStorage.setItem(
latestRenderStorageKey,
JSON.stringify({
container: persistedContainer,
containers: persistedContainers,
context,
})
);
} catch (err) {
// ignored
}
};

const isChild = (): boolean => {
if (isChildComponentWindow(name)) {
Expand Down Expand Up @@ -521,6 +688,22 @@ export function component<P, X, C, ExtType>(
const parent = parentComponent({
uid,
options,
getFallbackRerender: () => {
if (latestRenderedRerender) {
return latestRenderedRerender;
}

const latestRender = readLatestRender();
if (!latestRender) {
return;
}

return () => {
return waitForSelector(latestRender.container).then((selector) => {
return instance.render(selector, latestRender.context);
});
};
},
});

parent.init();
Expand Down Expand Up @@ -597,15 +780,20 @@ export function component<P, X, C, ExtType>(
);
}

const rerender = () => {
const newInstance = clone();
extend(instance, newInstance);
return newInstance.renderTo(target, container, context);
};

latestRenderedRerender = rerender;
writeLatestRender(container, finalContext);

return parent.render({
target,
container,
context: finalContext,
rerender: () => {
const newInstance = clone();
extend(instance, newInstance);
return newInstance.renderTo(target, container, context);
},
rerender,
});
})
.catch((err) => {
Expand Down
26 changes: 26 additions & 0 deletions src/parent/parent.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export type ParentHelpers<P> = {|
event: EventEmitterType,
show: () => ZalgoPromise<void>,
hide: () => ZalgoPromise<void>,
rerender: () => ZalgoPromise<void>,
|};

function getDefaultProps<P>(): PropsType<P> {
Expand Down Expand Up @@ -208,6 +209,7 @@ type OpenPrerender = (
type WatchForUnload = () => ZalgoPromise<void>;
type GetInternalState = () => ZalgoPromise<InternalState>;
type SetInternalState = (InternalState) => ZalgoPromise<InternalState>;
type GetFallbackRerender = () => ?Rerender;

type ParentDelegateOverrides<P> = {|
props: PropsType<P>,
Expand Down Expand Up @@ -276,13 +278,15 @@ type ParentOptions<P, X, C, ExtType> = {|
options: NormalizedComponentOptionsType<P, X, C, ExtType>,
overrides?: ParentDelegateOverrides<P>,
parentWin?: CrossDomainWindowType,
getFallbackRerender?: GetFallbackRerender,
|};

export function parentComponent<P, X, C, ExtType>({
uid,
options,
overrides = getDefaultOverrides(),
parentWin = window,
getFallbackRerender = () => null,
}: ParentOptions<P, X, C, ExtType>): ParentComponent<P, X> {
const {
propsDef,
Expand Down Expand Up @@ -317,6 +321,9 @@ export function parentComponent<P, X, C, ExtType>({
let childComponent: ?ChildExportsType<P>;
let currentChildDomain: ?string;
let currentContainer: HTMLElement | void;
let currentRerender: ?Rerender = null;
let lastRerender: ?Rerender = null;
let hasBeenRendered: boolean = false;
let isRenderFinished: boolean = false;

const onErrorOverride: ?OnError = overrides.onError;
Expand Down Expand Up @@ -1280,6 +1287,22 @@ export function parentComponent<P, X, C, ExtType>({
updateProps,
show,
hide,
rerender: () => {
const fallbackRerender = getFallbackRerender();
const rerenderFn = currentRerender || lastRerender || fallbackRerender;

if (!rerenderFn) {
if (!hasBeenRendered) {
throw new Error(
"Rerender not available - component must be rendered first."
);
}
throw new Error(
"Rerender callback lost after render - component may be destroyed or re-initialized."
);
}
return rerenderFn();
},
};
};

Expand Down Expand Up @@ -1509,6 +1532,9 @@ export function parentComponent<P, X, C, ExtType>({
rerender,
}: RenderOptions): ZalgoPromise<void> => {
return ZalgoPromise.try(() => {
currentRerender = rerender;
lastRerender = rerender;
hasBeenRendered = true;
const initialChildDomain = getInitialChildDomain();
const childDomainMatch = getDomainMatcher();

Expand Down
Loading