@plone/volto
Version:
Volto
162 lines (131 loc) • 4.57 kB
JSX
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import omit from 'lodash/omit';
import { loadLazyLibrary } from '@plone/volto/actions/lazyLibraries/lazyLibraries';
import config from '@plone/volto/registry';
const validateLibs = (maybeLibs) => {
if (Array.isArray(maybeLibs)) {
return maybeLibs.map(validateLibs).filter((x) => !!x).length > 0;
}
const { loadables, lazyBundles } = config.settings;
return (
Object.keys(lazyBundles).includes(maybeLibs) ||
Object.keys(loadables).includes(maybeLibs)
);
};
/**
* @param name {string|string[]} Name or names of a register bundle or lazy lib
* @returns {string[]} an array of registered lib names.
*/
const flattenLazyBundle = (maybeNames) => {
const { lazyBundles } = config.settings;
if (!validateLibs(maybeNames)) {
throw new Error(`Invalid lib or bundle name ${maybeNames}`);
}
if (
typeof maybeNames === 'string' &&
typeof lazyBundles === 'object' &&
Object.keys(lazyBundles).includes(maybeNames)
) {
const val = lazyBundles[maybeNames];
return Array.isArray(val) ? val : [val];
}
return Array.isArray(maybeNames) ? maybeNames : [maybeNames];
};
// TODO: make an unit test that checks if it is possible to have multiple
// useLoadables hooks inside a single component?
export function useLazyLibs(maybeNames, options = {}) {
const libraries = flattenLazyBundle(maybeNames);
const { shouldRerender = true } = options;
const { loadables } = config.settings;
const dispatch = useDispatch();
const globalLoadedLibraries = useSelector(
(state) => state.lazyLibraries || {},
(left, right) => (shouldRerender ? shallowEqual(left, right) : true),
);
const loaded = getLoadables(libraries, globalLoadedLibraries);
libraries.forEach((name) => {
const LoadableLibrary = loadables[name];
if (!globalLoadedLibraries[name]) {
LoadableLibrary.load().then((val) => {
if (!globalLoadedLibraries[name] && val) {
dispatch(loadLazyLibrary(name, val));
}
});
}
return;
});
return loaded;
}
export function preloadLazyLibs(maybeNames, forwardRef = true) {
const decorator = (WrappedComponent) => {
function PreloadLoadables(props) {
useLazyLibs(maybeNames, { shouldRerender: false });
const key = Array.isArray(maybeNames) ? maybeNames.join(',') : maybeNames;
PreloadLoadables.displayName = `PreloadLoadables(${key})(${getDisplayName(
WrappedComponent,
)})`;
return (
<WrappedComponent
key={key}
{...omit(props, 'forwardedRef')}
ref={forwardRef ? props.forwardedRef : null}
/>
);
}
if (forwardRef) {
return hoistNonReactStatics(
React.forwardRef((props, ref) => {
return <PreloadLoadables {...props} forwardedRef={ref} />;
}),
WrappedComponent,
);
}
return hoistNonReactStatics(PreloadLoadables, WrappedComponent);
};
return decorator;
}
export function injectLazyLibs(maybeNames, forwardRef = false) {
const decorator = (WrappedComponent) => {
let libraries;
function WithLoadables(props) {
libraries = libraries || flattenLazyBundle(maybeNames);
const loaded = useLazyLibs(libraries, { shouldRerender: true });
const isLoaded = Object.keys(loaded).length === libraries.length;
WithLoadables.displayName = `WithLoadables(${libraries.join(
',',
)})(${getDisplayName(WrappedComponent)})`;
// The component is rendered when all libraries are loaded!
return isLoaded ? (
<WrappedComponent
key={Object.keys(loaded).join('|')}
{...omit(props, 'forwardedRef')}
{...loaded}
ref={forwardRef ? props.forwardedRef : null}
/>
) : null;
}
if (forwardRef) {
return hoistNonReactStatics(
React.forwardRef((props, ref) => {
return <WithLoadables {...props} forwardedRef={ref} />;
}),
WrappedComponent,
);
}
return hoistNonReactStatics(WithLoadables, WrappedComponent);
};
return decorator;
}
function getLoadables(names, loadedLibraries) {
return Object.assign(
{},
...names.map((libName) =>
loadedLibraries[libName] ? { [libName]: loadedLibraries[libName] } : {},
),
);
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}