zoid
Version:
Cross domain components.
943 lines (732 loc) • 31.1 kB
JavaScript
/* @flow */
/* eslint max-lines: 0 */
import { send, bridge, serializeMessage, ProxyWindow } from 'post-robot/src';
import { isSameDomain, isSameTopWindow, matchDomain, getDomainFromUrl, isBlankDomain,
onCloseWindow, getDomain, type CrossDomainWindowType, getDistanceFromTop, isTop, normalizeMockUrl } from 'cross-domain-utils/src';
import { ZalgoPromise } from 'zalgo-promise/src';
import { addEventListener, uniqueID, elementReady, writeElementToWindow,
noop, showAndAnimate, animateAndHide, showElement, hideElement, onResize,
addClass, extend, extendUrl, getElement, memoized, appendChild,
once, stringify, stringifyError, eventEmitter, type EventEmitterType } from 'belter/src';
import { node, dom, ElementNode } from 'jsx-pragmatic/src';
import { buildChildWindowName } from '../window';
import { POST_MESSAGE, CONTEXT, CLASS_NAMES, ANIMATION_NAMES,
CLOSE_REASONS, DELEGATE, INITIAL_PROPS, WINDOW_REFERENCES, EVENTS } from '../../constants';
import type { Component } from '../component';
import { global, cleanup, type CleanupType } from '../../lib';
import type { PropsType, BuiltInPropsType } from '../component/props';
import type { ChildExportsType } from '../child';
import type { CancelableType, DimensionsType, ElementRefType } from '../../types';
import { RENDER_DRIVERS, type ContextDriverType } from './drivers';
import { propsToQuery, normalizeProps } from './props';
global.props = global.props || {};
global.windows = global.windows || {};
export type RenderOptionsType<P> = {
id : string,
props : PropsType & P,
tag : string,
context : string,
outlet : HTMLElement,
CLASS : typeof CLASS_NAMES,
ANIMATION : typeof ANIMATION_NAMES,
CONTEXT : typeof CONTEXT,
EVENT : typeof EVENTS,
actions : {
close : (?string) => ZalgoPromise<void>,
focus : () => ZalgoPromise<ProxyWindow>
},
on : (string, () => void) => CancelableType,
jsxDom : typeof node,
document : Document,
container? : HTMLElement,
dimensions : DimensionsType,
doc? : Document
};
export type ParentExportsType<P> = {
init : (ChildExportsType<P>) => ZalgoPromise<void>,
close : (string) => ZalgoPromise<void>,
checkClose : () => ZalgoPromise<void>,
resize : ({ width? : ?number, height? : ?number }) => ZalgoPromise<void>,
trigger : (string) => ZalgoPromise<void>,
hide : () => ZalgoPromise<void>,
show : () => ZalgoPromise<void>,
error : (mixed) => ZalgoPromise<void>
};
export type PropRef =
{| type : typeof INITIAL_PROPS.RAW, uid? : string, value? : string |};
export type WindowRef =
{| type : typeof WINDOW_REFERENCES.OPENER |} |
{| type : typeof WINDOW_REFERENCES.TOP |} |
{| type : typeof WINDOW_REFERENCES.PARENT, distance : number |} |
{| type : typeof WINDOW_REFERENCES.GLOBAL, uid : string |};
export type ChildPayload = {
uid : string,
tag : string,
context : $Values<typeof CONTEXT>,
domain : string,
parent : WindowRef,
props : PropRef,
exports : string
};
/* Parent Component
----------------
This manages the state of the component on the parent window side - i.e. the window the component is being rendered into.
It handles opening the necessary windows/iframes, launching the component's url, and listening for messages back from the component.
*/
export class ParentComponent<P> {
component : Component<P>
driver : ContextDriverType
props : BuiltInPropsType & P
onInit : ZalgoPromise<ParentComponent<P>>
errored : boolean
event : EventEmitterType
clean : CleanupType
container : HTMLElement
element : HTMLElement
iframe : HTMLIFrameElement
prerenderIframe : HTMLIFrameElement
childExports : ?ChildExportsType<P>
timeout : ?TimeoutID // eslint-disable-line no-undef
constructor(component : Component<P>, context : string, { props } : { props : (PropsType & P) }) {
ZalgoPromise.try(() => {
this.onInit = new ZalgoPromise();
this.clean = cleanup(this);
this.event = eventEmitter();
this.component = component;
this.driver = RENDER_DRIVERS[context];
this.setProps(props);
this.registerActiveComponent();
this.watchForUnload();
return this.onInit;
}).catch(err => {
return this.error(err, props);
});
}
render(context : $Values<typeof CONTEXT>, element : ElementRefType, target? : CrossDomainWindowType = window) : ZalgoPromise<ParentComponent<P>> {
return this.tryInit(() => {
this.component.log(`render`);
let uid = uniqueID();
let tasks = {};
tasks.onRender = this.props.onRender();
let domain = this.getDomain();
let initialDomain = this.getInitialDomain();
tasks.elementReady = ZalgoPromise.try(() => {
if (element) {
return this.elementReady(element);
}
});
let focus = () => {
return tasks.open.then(proxyWin => proxyWin.focus());
};
tasks.openContainer = tasks.elementReady.then(() => {
return this.openContainer(element, { context, uid, focus });
});
tasks.open = this.driver.renderedIntoContainer
? tasks.openContainer.then(() => this.open())
: this.open();
tasks.awaitWindow = tasks.open.then(proxyWin => {
return proxyWin.awaitWindow();
});
tasks.showContainer = tasks.openContainer.then(() => {
return this.showContainer();
});
tasks.buildWindowName = tasks.open.then(proxyWin => {
return this.buildWindowName({ proxyWin, initialDomain, domain, target, context, uid });
});
tasks.setWindowName = ZalgoPromise.all([ tasks.open, tasks.buildWindowName ]).then(([ proxyWin, windowName ]) => {
return this.setWindowName(proxyWin, windowName);
});
tasks.watchForClose = ZalgoPromise.all([ tasks.awaitWindow, tasks.setWindowName ]).then(([ win ]) => {
return this.watchForClose(win);
});
tasks.prerender = ZalgoPromise.all([ tasks.awaitWindow, tasks.openContainer ]).then(([ win ]) => {
return this.prerender(win, { context, uid });
});
tasks.showComponent = tasks.prerender.then(() => {
return this.showComponent();
});
tasks.buildUrl = this.buildUrl();
tasks.openBridge = ZalgoPromise.all([ tasks.awaitWindow, tasks.buildUrl ]).then(([ win, url ]) => {
return this.openBridge(win, getDomainFromUrl(url), context);
});
tasks.loadUrl = ZalgoPromise.all([
tasks.open,
tasks.buildUrl,
tasks.setWindowName
]).then(([ proxyWin, url ]) => {
return this.loadUrl(proxyWin, url);
});
tasks.switchPrerender = ZalgoPromise.all([ tasks.prerender, this.onInit ]).then(() => {
return this.switchPrerender();
});
tasks.runTimeout = tasks.loadUrl.then(() => {
return this.runTimeout();
});
return ZalgoPromise.hash(tasks);
}).then(() => {
return this.props.onEnter();
}).then(() => {
return this;
});
}
renderTo(context : $Values<typeof CONTEXT>, target : CrossDomainWindowType, element : ?string) : ZalgoPromise<ParentComponent<P>> {
return this.tryInit(() => {
if (target === window) {
return this.render(context, element);
}
if (element && typeof element !== 'string') {
throw new Error(`Element passed to renderTo must be a string selector, got ${ typeof element } ${ element }`);
}
this.checkAllowRemoteRender(target);
this.component.log(`render_${ context }_to_win`, { element: stringify(element), context });
this.delegate(context, target);
return this.render(context, element, target);
});
}
on(eventName : string, handler : () => void) : CancelableType {
return this.event.on(eventName, handler);
}
checkAllowRemoteRender(target : CrossDomainWindowType) {
if (!target) {
throw this.component.createError(`Must pass window to renderTo`);
}
if (!isSameTopWindow(window, target)) {
throw new Error(`Can only renderTo an adjacent frame`);
}
let origin = getDomain();
let domain = this.getDomain();
if (!matchDomain(domain, origin) && !isSameDomain(target)) {
throw new Error(`Can not render remotely to ${ domain.toString() } - can only render to ${ origin }`);
}
}
registerActiveComponent() {
ParentComponent.activeComponents.push(this);
this.clean.register(() => {
ParentComponent.activeComponents.splice(ParentComponent.activeComponents.indexOf(this), 1);
});
}
getWindowRef(target : CrossDomainWindowType, domain : string, uid : string, context : $Values<typeof CONTEXT>) : WindowRef {
if (domain === getDomain(window)) {
global.windows[uid] = window;
this.clean.register(() => {
delete global.windows[uid];
});
return { type: WINDOW_REFERENCES.GLOBAL, uid };
}
if (target !== window) {
throw new Error(`Can not currently create window reference for different target with a different domain`);
}
if (context === CONTEXT.POPUP) {
return { type: WINDOW_REFERENCES.OPENER };
}
if (isTop(window)) {
return { type: WINDOW_REFERENCES.TOP };
}
return { type: WINDOW_REFERENCES.PARENT, distance: getDistanceFromTop(window) };
}
buildWindowName({ proxyWin, initialDomain, domain, target, uid, context } : { proxyWin : ProxyWindow, initialDomain : string, domain : string | RegExp, target : CrossDomainWindowType, context : $Values<typeof CONTEXT>, uid : string }) : string {
return buildChildWindowName(this.component.name, this.buildChildPayload({ proxyWin, initialDomain, domain, target, context, uid }));
}
getPropsRef(proxyWin : ProxyWindow, target : CrossDomainWindowType, domain : string | RegExp, uid : string) : PropRef {
let value = serializeMessage(proxyWin, domain, this.getPropsForChild(domain));
let propRef = isSameDomain(target)
? { type: INITIAL_PROPS.RAW, value }
: { type: INITIAL_PROPS.UID, uid };
if (propRef.type === INITIAL_PROPS.UID) {
global.props[uid] = value;
this.clean.register(() => {
delete global.props[uid];
});
}
return propRef;
}
buildChildPayload({ proxyWin, initialDomain, domain, target = window, context, uid } : { proxyWin : ProxyWindow, initialDomain : string, domain : string | RegExp, target : CrossDomainWindowType, context : $Values<typeof CONTEXT>, uid : string } = {}) : ChildPayload {
let childPayload : ChildPayload = {
uid,
context,
domain: getDomain(window),
tag: this.component.tag,
parent: this.getWindowRef(target, initialDomain, uid, context),
props: this.getPropsRef(proxyWin, target, domain, uid),
exports: serializeMessage(proxyWin, domain, this.buildParentExports(proxyWin))
};
return childPayload;
}
setProps(props : (PropsType & P), isUpdate : boolean = false) {
if (this.component.validate) {
this.component.validate(this.component, props);
}
// $FlowFixMe
this.props = this.props || {};
extend(this.props, normalizeProps(this.component, this, props, isUpdate));
}
buildUrl() : ZalgoPromise<string> {
return propsToQuery({ ...this.component.props, ...this.component.builtinProps }, this.props)
.then(query => {
let url = normalizeMockUrl(this.component.getUrl(this.props));
return extendUrl(url, { query: { ...query } });
});
}
getDomain() : string | RegExp {
return this.component.getDomain(this.props);
}
getInitialDomain() : string {
return this.component.getInitialDomain(this.props);
}
getPropsForChild(domain : string | RegExp) : (BuiltInPropsType & P) {
let result = {};
for (let key of Object.keys(this.props)) {
let prop = this.component.getProp(key);
if (prop && prop.sendToChild === false) {
continue;
}
if (prop && prop.sameDomain && !matchDomain(domain, getDomain(window))) {
continue;
}
// $FlowFixMe
result[key] = this.props[key];
}
// $FlowFixMe
return result;
}
updateProps(props : (PropsType & P)) : ZalgoPromise<void> {
this.setProps(props, true);
return this.onInit.then(() => {
if (this.childExports) {
return this.childExports.updateProps(this.getPropsForChild(this.getDomain()));
} else {
throw new Error(`Child exports were not available`);
}
});
}
openBridge(win : CrossDomainWindowType, domain : string, context : $Values<typeof CONTEXT>) : ZalgoPromise<?CrossDomainWindowType> {
return ZalgoPromise.try(() => {
if (!bridge || !bridge.needsBridge({ win, domain }) || bridge.hasBridge(domain, domain)) {
return;
}
let bridgeUrl = this.component.getBridgeUrl(this.props);
if (!bridgeUrl) {
throw new Error(`Bridge url and domain needed to render ${ context }`);
}
let bridgeDomain = getDomainFromUrl(bridgeUrl);
bridge.linkUrl(win, domain);
return bridge.openBridge(bridgeUrl, bridgeDomain);
});
}
open() : ZalgoPromise<ProxyWindow> {
return ZalgoPromise.try(() => {
this.component.log(`open`);
let windowProp = this.props.window;
if (windowProp) {
this.clean.register('destroyProxyWindow', () => {
return windowProp.close();
});
return windowProp;
}
return this.driver.open.call(this);
});
}
setWindowName(proxyWin : ProxyWindow, name : string) : ZalgoPromise<ProxyWindow> {
return proxyWin.setName(name);
}
switchPrerender() : ZalgoPromise<void> {
return ZalgoPromise.try(() => {
if (this.component.prerenderTemplate && this.driver.switchPrerender) {
return this.driver.switchPrerender.call(this);
}
});
}
elementReady(element : ElementRefType) : ZalgoPromise<void> {
return elementReady(element).then(noop);
}
delegate(context : $Values<typeof CONTEXT>, target : CrossDomainWindowType) {
this.component.log(`delegate`);
let props = {
window: this.props.window,
onClose: this.props.onClose,
onDisplay: this.props.onDisplay
};
for (let propName of this.component.getPropNames()) {
let prop = this.component.getProp(propName);
if (prop.allowDelegate) {
props[propName] = this.props[propName];
}
}
let delegate = send(target, `${ POST_MESSAGE.DELEGATE }_${ this.component.name }`, {
context,
props,
overrides: {
userClose: () => this.userClose(),
error: (err) => this.error(err),
on: (eventName, handler) => this.on(eventName, handler)
}
}).then(({ data }) => {
this.clean.register(data.destroy);
return data;
}).catch(err => {
throw new Error(`Unable to delegate rendering. Possibly the component is not loaded in the target window.\n\n${ stringifyError(err) }`);
});
let overrides = this.driver.delegateOverrides;
for (let key of Object.keys(overrides)) {
let val = overrides[key];
if (val === DELEGATE.CALL_DELEGATE) {
// $FlowFixMe
this[key] = function overridenFunction() : ZalgoPromise<mixed> {
return delegate.then(data => {
return data.overrides[key].apply(this, arguments);
});
};
}
}
}
watchForClose(win : CrossDomainWindowType) {
let closeWindowListener = onCloseWindow(win, () => {
this.component.log(`detect_close_child`);
return ZalgoPromise.try(() => {
return this.props.onClose(CLOSE_REASONS.CLOSE_DETECTED);
}).finally(() => {
return this.destroy();
});
}, 3000);
this.clean.register('destroyCloseWindowListener', closeWindowListener.cancel);
}
watchForUnload() {
// Our child has no way of knowing if we navigated off the page. So we have to listen for unload
// and close the child manually if that happens.
let onunload = once(() => {
this.component.log(`navigate_away`);
this.destroyComponent();
});
let unloadWindowListener = addEventListener(window, 'unload', onunload);
this.clean.register('destroyUnloadWindowListener', unloadWindowListener.cancel);
}
loadUrl(proxyWin : ProxyWindow, url : string) : ZalgoPromise<ProxyWindow> {
this.component.log(`load_url`);
return proxyWin.setLocation(url);
}
runTimeout() {
let timeout = this.props.timeout;
if (timeout) {
let id = this.timeout = setTimeout(() => {
this.component.log(`timed_out`, { timeout: timeout.toString() });
this.error(this.component.createError(`Loading component timed out after ${ timeout } milliseconds`));
}, timeout);
this.clean.register(() => {
clearTimeout(id);
delete this.timeout;
});
}
}
initChild(childExports : ChildExportsType<P>) : ZalgoPromise<void> {
return ZalgoPromise.try(() => {
this.childExports = childExports;
this.onInit.resolve(this);
if (this.timeout) {
clearTimeout(this.timeout);
}
});
}
buildParentExports(win : ProxyWindow) : ParentExportsType<P> {
return {
init: (childExports) => this.initChild(childExports),
close: (reason) => this.close(reason),
checkClose: () => this.checkClose(win),
resize: ({ width, height }) => this.resize({ width, height }),
trigger: (name) => ZalgoPromise.try(() => this.event.trigger(name)),
hide: () => ZalgoPromise.try(() => this.hide()),
show: () => ZalgoPromise.try(() => this.show()),
error: (err) => this.error(err)
};
}
resize({ width, height } : { width? : ?number, height? : ?number }) : ZalgoPromise<void> {
return ZalgoPromise.try(() => {
this.driver.resize.call(this, { width, height });
});
}
hide() : void {
if (this.container) {
hideElement(this.container);
}
return this.driver.hide.call(this);
}
show() : void {
if (this.container) {
showElement(this.container);
}
return this.driver.show.call(this);
}
checkClose(win : ProxyWindow) : ZalgoPromise<void> {
return win.isClosed().then(closed => {
if (closed) {
return this.userClose();
}
return ZalgoPromise.delay(200)
.then(() => win.isClosed())
.then(secondClosed => {
if (secondClosed) {
return this.userClose();
}
});
});
}
userClose() : ZalgoPromise<void> {
return this.close(CLOSE_REASONS.USER_CLOSED);
}
/* Close
-----
Close the child component
*/
close(reason? : string = CLOSE_REASONS.PARENT_CALL) : ZalgoPromise<void> {
return ZalgoPromise.try(() => {
this.component.log(`close`, { reason });
this.event.triggerOnce(EVENTS.CLOSE);
return this.props.onClose(reason);
}).then(() => {
return ZalgoPromise.all([
this.closeComponent(),
this.closeContainer()
]);
}).then(() => {
return this.destroy();
});
}
closeContainer(reason : string = CLOSE_REASONS.PARENT_CALL) : ZalgoPromise<void> {
return ZalgoPromise.try(() => {
this.event.triggerOnce(EVENTS.CLOSE);
return this.props.onClose(reason);
}).then(() => {
return ZalgoPromise.all([
this.closeComponent(reason),
this.hideContainer()
]);
}).then(() => {
return this.destroyContainer();
});
}
destroyContainer() : ZalgoPromise<void> {
return ZalgoPromise.try(() => {
this.clean.run('destroyContainerEvents');
this.clean.run('destroyContainerTemplate');
});
}
closeComponent(reason : string = CLOSE_REASONS.PARENT_CALL) : ZalgoPromise<void> {
return ZalgoPromise.try(() => {
return this.cancelContainerEvents();
}).then(() => {
this.event.triggerOnce(EVENTS.CLOSE);
return this.props.onClose(reason);
}).then(() => {
return this.hideComponent();
}).then(() => {
return this.destroyComponent();
}).then(() => {
// IE in metro mode -- child window needs to close itself, or close will hang
if (this.childExports && this.driver.callChildToClose) {
this.childExports.close().catch(noop);
}
});
}
destroyComponent() {
this.clean.run('destroyUnloadWindowListener');
this.clean.run('destroyCloseWindowListener');
this.clean.run('destroyContainerEvents');
this.clean.run('destroyWindow');
}
showContainer() : ZalgoPromise<void> {
return ZalgoPromise.try(() => {
if (this.props.onDisplay) {
return this.props.onDisplay();
}
}).then(() => {
if (this.container) {
return showAndAnimate(this.container, ANIMATION_NAMES.SHOW_CONTAINER, this.clean.register);
}
});
}
showComponent() : ZalgoPromise<void> {
return ZalgoPromise.try(() => {
if (this.props.onDisplay) {
return this.props.onDisplay();
}
}).then(() => {
if (this.element) {
return showAndAnimate(this.element, ANIMATION_NAMES.SHOW_COMPONENT, this.clean.register);
}
});
}
hideContainer() : ZalgoPromise<void> {
return ZalgoPromise.try(() => {
if (this.container) {
return animateAndHide(this.container, ANIMATION_NAMES.HIDE_CONTAINER, this.clean.register);
}
});
}
hideComponent() : ZalgoPromise<void> {
return ZalgoPromise.try(() => {
if (this.element) {
return animateAndHide(this.element, ANIMATION_NAMES.HIDE_COMPONENT, this.clean.register);
}
});
}
/* Create Component Template
-------------------------
Creates an initial template and stylesheet which are loaded into the child window, to be displayed before the url is loaded
*/
prerender(win : CrossDomainWindowType, { context, uid } : { context : $Values<typeof CONTEXT>, uid : string }) : ZalgoPromise<void> {
return ZalgoPromise.try(() => {
if (!this.component.prerenderTemplate) {
return;
}
return ZalgoPromise.try(() => {
return this.driver.openPrerender.call(this, win);
}).then(prerenderWindow => {
if (!prerenderWindow || !isSameDomain(prerenderWindow) || !isBlankDomain(prerenderWindow)) {
return;
}
let doc = prerenderWindow.document;
let el = this.renderTemplate(this.component.prerenderTemplate, { context, uid, document: doc });
if (el instanceof ElementNode) {
el = el.render(dom({ doc }));
}
try {
writeElementToWindow(prerenderWindow, el);
} catch (err) {
return;
}
let { width = false, height = false, element = 'body' } = this.component.autoResize || {};
if (width || height) {
onResize(getElement(element, prerenderWindow.document), ({ width: newWidth, height: newHeight }) => {
this.resize({
width: width ? newWidth : undefined,
height: height ? newHeight : undefined
});
}, { width, height, win: prerenderWindow });
}
});
});
}
renderTemplate<T : HTMLElement | ElementNode>(renderer : (RenderOptionsType<P>) => T, { context, uid, focus, container, document, outlet } : { context : $Values<typeof CONTEXT>, uid : string, focus? : () => ZalgoPromise<ProxyWindow>, container? : HTMLElement, document? : Document, outlet? : HTMLElement }) : T {
focus = focus || (() => ZalgoPromise.resolve());
// $FlowFixMe
return renderer.call(this, {
context,
uid,
id: `${ CLASS_NAMES.ZOID }-${ this.component.tag }-${ uid }`,
props: renderer.__xdomain__ ? null : this.props,
tag: this.component.tag,
CLASS: CLASS_NAMES,
ANIMATION: ANIMATION_NAMES,
CONTEXT,
EVENT: EVENTS,
actions: {
focus,
close: () => this.userClose()
},
on: (eventName, handler) => this.on(eventName, handler),
jsxDom: node,
document,
dimensions: this.component.dimensions,
container,
outlet
});
}
openContainer(element : ?HTMLElement, { context, uid, focus } : { context : $Values<typeof CONTEXT>, uid : string, focus : () => ZalgoPromise<ProxyWindow> }) : ZalgoPromise<void> {
return ZalgoPromise.try(() => {
let el;
if (element) {
el = getElement(element);
} else {
el = document.body;
}
if (!el) {
throw new Error(`Could not find element to open container into`);
}
if (!this.component.containerTemplate) {
if (this.driver.renderedIntoContainer) {
throw new Error(`containerTemplate needed to render ${ context }`);
}
return;
}
let outlet = document.createElement('div');
addClass(outlet, CLASS_NAMES.OUTLET);
let container = this.renderTemplate(this.component.containerTemplate, { context, uid, container: el, focus, outlet });
if (container instanceof ElementNode) {
container = container.render(dom({ doc: document }));
}
this.container = container;
hideElement(this.container);
appendChild(el, this.container);
if (this.driver.renderedIntoContainer) {
this.element = outlet;
hideElement(this.element);
if (!this.element) {
throw new Error('Could not find element to render component into');
}
hideElement(this.element);
}
this.clean.register('destroyContainerTemplate', () => {
if (this.container && this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
}
delete this.container;
});
});
}
cancelContainerEvents() {
this.clean.run('destroyContainerEvents');
}
destroy() : ZalgoPromise<void> {
return ZalgoPromise.try(() => {
if (this.clean.hasTasks()) {
this.component.log(`destroy`);
return this.clean.all();
}
});
}
tryInit(method : () => mixed) : ZalgoPromise<ParentComponent<P>> {
return ZalgoPromise.try(method).catch(err => {
this.onInit.reject(err);
}).then(() => {
return this.onInit;
});
}
// $FlowFixMe
error(err : mixed, props : PropsType & P = this.props) : ZalgoPromise<void> {
if (this.errored) {
return;
}
this.errored = true;
// eslint-disable-next-line promise/no-promise-in-callback
return ZalgoPromise.try(() => {
this.onInit = this.onInit || new ZalgoPromise();
this.onInit.reject(err);
return this.destroy();
}).then(() => {
if (props.onError) {
return props.onError(err);
}
}).catch(errErr => { // eslint-disable-line unicorn/catch-error-name
throw new Error(`An error was encountered while handling error:\n\n ${ stringifyError(err) }\n\n${ stringifyError(errErr) }`);
}).then(() => {
if (!props.onError) {
throw err;
}
});
}
static activeComponents : Array<ParentComponent<*>> = []
static destroyAll() : ZalgoPromise<void> {
let results = [];
while (ParentComponent.activeComponents.length) {
results.push(ParentComponent.activeComponents[0].destroy());
}
return ZalgoPromise.all(results).then(noop);
}
}