diff --git a/capacities.js b/capacities.js new file mode 100644 index 0000000..01b9f3c --- /dev/null +++ b/capacities.js @@ -0,0 +1,6 @@ +"use strict"; + +exports.__esModule = true; +exports.IntersectionObserver = void 0; +var IntersectionObserver = typeof window !== 'undefined' && window.IntersectionObserver; +exports.IntersectionObserver = IntersectionObserver; \ No newline at end of file diff --git a/createLoadableVisibilityComponent.js b/createLoadableVisibilityComponent.js new file mode 100644 index 0000000..8d878c5 --- /dev/null +++ b/createLoadableVisibilityComponent.js @@ -0,0 +1,132 @@ +"use strict"; + +exports.__esModule = true; +exports["default"] = void 0; + +var _react = _interopRequireWildcard(require("react")); + +var _capacities = require("./capacities"); + +function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } + +function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } + +var intersectionObserver; +var trackedElements = new Map(); + +if (_capacities.IntersectionObserver) { + intersectionObserver = new window.IntersectionObserver(function (entries, observer) { + entries.forEach(function (entry) { + var visibilityHandler = trackedElements.get(entry.target); + + if (visibilityHandler && (entry.isIntersecting || entry.intersectionRatio > 0)) { + visibilityHandler(); + } + }); + }); +} + +function createLoadableVisibilityComponent(args, _ref) { + var Loadable = _ref.Loadable, + preloadFunc = _ref.preloadFunc, + loadFunc = _ref.loadFunc, + LoadingComponent = _ref.LoadingComponent; + var preloaded = false, + loaded = false; + var visibilityHandlers = []; + var LoadableComponent = Loadable.apply(void 0, args); + + function LoadableVisibilityComponent(props) { + var visibilityElementRef = (0, _react.useRef)(); + + var _useState = (0, _react.useState)(preloaded), + isVisible = _useState[0], + setVisible = _useState[1]; + + function visibilityHandler() { + if (visibilityElementRef.current) { + intersectionObserver.unobserve(visibilityElementRef.current); + trackedElements["delete"](visibilityElementRef.current); + } + + setVisible(true); + } + + (0, _react.useEffect)(function () { + var element = visibilityElementRef.current; + + if (!isVisible && element) { + visibilityHandlers.push(visibilityHandler); + trackedElements.set(element, visibilityHandler); + intersectionObserver.observe(element); + return function () { + var handlerIndex = visibilityHandlers.indexOf(visibilityHandler); + + if (handlerIndex >= 0) { + visibilityHandlers.splice(handlerIndex, 1); + } + + intersectionObserver.unobserve(element); + trackedElements["delete"](element); + }; + } + }, [isVisible, visibilityElementRef.current]); + + if (isVisible) { + return _react["default"].createElement(LoadableComponent, props); + } + + if (LoadingComponent || props.fallback) { + return _react["default"].createElement("div", _extends({ + style: { + display: "inline-block", + minHeight: "1px", + minWidth: "1px" + } + }, props, { + ref: visibilityElementRef + }), LoadingComponent ? _react["default"].createElement(LoadingComponent, _extends({ + isLoading: true + }, props)) : props.fallback); + } + + return _react["default"].createElement("div", _extends({ + style: { + display: "inline-block", + minHeight: "1px", + minWidth: "1px" + } + }, props, { + ref: visibilityElementRef + })); + } + + LoadableVisibilityComponent[preloadFunc] = function () { + if (!preloaded) { + preloaded = true; + visibilityHandlers.forEach(function (handler) { + return handler(); + }); + } + + return LoadableComponent[preloadFunc](); + }; + + LoadableVisibilityComponent[loadFunc] = function () { + if (!loaded) { + loaded = true; + visibilityHandlers.forEach(function (handler) { + return handler(); + }); + } + + return LoadableComponent[loadFunc](); + }; + + return LoadableVisibilityComponent; +} + +var _default = createLoadableVisibilityComponent; +exports["default"] = _default; \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..c9c4d31 --- /dev/null +++ b/index.js @@ -0,0 +1,3 @@ +"use strict"; + +module.exports = require("./loadable-components"); \ No newline at end of file diff --git a/loadable-components.js b/loadable-components.js new file mode 100644 index 0000000..a6fa7c6 --- /dev/null +++ b/loadable-components.js @@ -0,0 +1,36 @@ +"use strict"; + +var _react = _interopRequireWildcard(require("react")); + +var _component = _interopRequireDefault(require("@loadable/component")); + +var _createLoadableVisibilityComponent = _interopRequireDefault(require("./createLoadableVisibilityComponent")); + +var _capacities = require("./capacities"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } + +function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } + +function loadableVisiblity(load, opts) { + if (opts === void 0) { + opts = {}; + } + + if (_capacities.IntersectionObserver) { + return (0, _createLoadableVisibilityComponent["default"])([load, opts], { + Loadable: _component["default"], + preloadFunc: "preload", + loadFunc: "load", + LoadingComponent: opts.fallback ? function () { + return opts.fallback; + } : null + }); + } else { + return (0, _component["default"])(load, opts); + } +} + +module.exports = loadableVisiblity; \ No newline at end of file diff --git a/react-loadable.js b/react-loadable.js new file mode 100644 index 0000000..c0d727f --- /dev/null +++ b/react-loadable.js @@ -0,0 +1,46 @@ +"use strict"; + +var _react = _interopRequireWildcard(require("react")); + +var _reactLoadable = _interopRequireDefault(require("react-loadable")); + +var _createLoadableVisibilityComponent = _interopRequireDefault(require("./createLoadableVisibilityComponent")); + +var _capacities = require("./capacities"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } + +function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } + +function LoadableVisibility(opts) { + if (_capacities.IntersectionObserver) { + return (0, _createLoadableVisibilityComponent["default"])([opts], { + Loadable: _reactLoadable["default"], + preloadFunc: 'preload', + + /* Preload helps in synchronously loading a component and returns a promise */ + loadFunc: 'preload', + LoadingComponent: opts.loading + }); + } else { + return (0, _reactLoadable["default"])(opts); + } +} + +function LoadableVisibilityMap(opts) { + if (_capacities.IntersectionObserver) { + return (0, _createLoadableVisibilityComponent["default"])([opts], { + Loadable: _reactLoadable["default"].Map, + preloadFunc: 'preload', + loadFunc: 'preload', + LoadingComponent: opts.loading + }); + } else { + return _reactLoadable["default"].Map(opts); + } +} + +LoadableVisibility.Map = LoadableVisibilityMap; +module.exports = LoadableVisibility; \ No newline at end of file diff --git a/src/__mocks__/@loadable/component.js b/src/__mocks__/@loadable/component.js index c937c2f..0cc7188 100644 --- a/src/__mocks__/@loadable/component.js +++ b/src/__mocks__/@loadable/component.js @@ -8,6 +8,7 @@ const loadableObject = props => { }; loadableObject.preload = jest.fn(); +loadableObject.load = jest.fn(); function loadable(opts) { return loadableObject; diff --git a/src/__tests__/loadable-components/intersection-observer.test.js b/src/__tests__/loadable-components/intersection-observer.test.js index 8d065d4..3ef8502 100644 --- a/src/__tests__/loadable-components/intersection-observer.test.js +++ b/src/__tests__/loadable-components/intersection-observer.test.js @@ -126,6 +126,34 @@ describe("Loadable", () => { expect(queryByTestId("loaded-component")).toBeTruthy(); }); + test("load calls loadable load", () => { + // Mock @loadable/component to get a stable `load` function + jest.doMock("@loadable/component"); + + const loadable = require("@loadable/component"); + const loadableVisiblity = require("../../loadable-components"); // Require our tested module with the above mock applied + + loadableVisiblity(loader).load(); + + expect(loadable().load).toHaveBeenCalled(); + }); + + test("load will cause the loadable component to be displayed", async () => { + const Loader = loadableVisiblity(loader); + let returnedValue; + + const { queryByTestId } = render(); + expect(queryByTestId("loaded-component")).toBeNull(); + + act(() => { + returnedValue = Loader.load() + expect(returnedValue).toBeInstanceOf(Promise); + }); + returnedValue.then(() => { + expect(queryByTestId("loaded-component")).toBeTruthy(); + }) + }); + test("it displays the loadable component when it becomes visible", async () => { const Loader = loadableVisiblity(loader); diff --git a/src/__tests__/react-loadable/intersection-observer.test.js b/src/__tests__/react-loadable/intersection-observer.test.js index f14675b..4b48160 100644 --- a/src/__tests__/react-loadable/intersection-observer.test.js +++ b/src/__tests__/react-loadable/intersection-observer.test.js @@ -99,18 +99,19 @@ describe("Loadable", () => { test("preload will cause the loadable component to be displayed", async () => { const Loader = LoadableVisibility(opts); + let returnedValue; const { queryByTestId } = render(); expect(queryByTestId("loaded-component")).toBeNull(); act(() => { - Loader.preload(); + returnedValue = Loader.preload() + expect(returnedValue).toBeInstanceOf(Promise); }); - - await waitForElement(() => queryByTestId("loaded-component")); - - expect(queryByTestId("loaded-component")).toBeTruthy(); + returnedValue.then(() => { + expect(queryByTestId("loaded-component")).toBeTruthy(); + }) }); test("it displays the loadable component when it becomes visible", async () => { @@ -210,4 +211,4 @@ describe("Loadable.Map", () => { expect(Loadable.Map().preload).toHaveBeenCalled(); }); -}); +}) diff --git a/src/createLoadableVisibilityComponent.js b/src/createLoadableVisibilityComponent.js index c1e8fb7..192cb88 100644 --- a/src/createLoadableVisibilityComponent.js +++ b/src/createLoadableVisibilityComponent.js @@ -23,9 +23,9 @@ if (IntersectionObserver) { function createLoadableVisibilityComponent( args, - { Loadable, preloadFunc, LoadingComponent } + { Loadable, preloadFunc, loadFunc, LoadingComponent } ) { - let preloaded = false; + let preloaded = false, loaded = false; const visibilityHandlers = []; const LoadableComponent = Loadable(...args); @@ -108,6 +108,14 @@ function createLoadableVisibilityComponent( return LoadableComponent[preloadFunc](); }; + LoadableVisibilityComponent[loadFunc] = () => { + if (!loaded) { + loaded = true; + visibilityHandlers.forEach(handler => handler()); + } + return LoadableComponent[loadFunc](); + }; + return LoadableVisibilityComponent; } diff --git a/src/loadable-components.js b/src/loadable-components.js index a28d8d2..406acb1 100644 --- a/src/loadable-components.js +++ b/src/loadable-components.js @@ -8,6 +8,7 @@ function loadableVisiblity(load, opts = {}) { return createLoadableVisibilityComponent([load, opts], { Loadable: loadable, preloadFunc: "preload", + loadFunc: "load", LoadingComponent: opts.fallback ? () => opts.fallback : null }); } else { diff --git a/src/react-loadable.js b/src/react-loadable.js index 4f9a392..77ac76a 100644 --- a/src/react-loadable.js +++ b/src/react-loadable.js @@ -8,6 +8,8 @@ function LoadableVisibility (opts) { return createLoadableVisibilityComponent([opts], { Loadable, preloadFunc: 'preload', + /* Preload helps in synchronously loading a component and returns a promise */ + loadFunc: 'preload', LoadingComponent: opts.loading, }) } else { @@ -20,6 +22,7 @@ function LoadableVisibilityMap (opts) { return createLoadableVisibilityComponent([opts], { Loadable: Loadable.Map, preloadFunc: 'preload', + loadFunc: 'preload', LoadingComponent: opts.loading, }) } else { diff --git a/yarn.lock b/yarn.lock index 68e9938..beb72ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -699,13 +699,20 @@ "@babel/plugin-transform-react-jsx-self" "^7.7.4" "@babel/plugin-transform-react-jsx-source" "^7.7.4" -"@babel/runtime@^7.6.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6": +"@babel/runtime@^7.6.2", "@babel/runtime@^7.7.6": version "7.7.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.7.tgz#194769ca8d6d7790ec23605af9ee3e42a0aa79cf" integrity sha512-uCnC2JEVAu8AKB5do1WRIsvrdJ0flYx/A/9f/6chdacnEZ7LmavjdsDXr5ksYBegxtuTPR5Va9/+13QF/kFkCA== dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.7.7": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6" + integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.4.0", "@babel/template@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.7.4.tgz#428a7d9eecffe27deac0a98e23bf8e3675d2a77b" @@ -895,13 +902,14 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" -"@loadable/component@^5.10.1": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@loadable/component/-/component-5.11.0.tgz#e495834279663d20aabf0e4ca25e5c44bef308eb" - integrity sha512-RzXAH419hnC6vF/ZAEv+k1JgbE/4G7veZo3RvwQDRfp2BFrwbJp9qasjccuk6RrQR/nWWgufaW/CGlL6NSBNGw== +"@loadable/component@^5.12.0": + version "5.15.0" + resolved "https://registry.yarnpkg.com/@loadable/component/-/component-5.15.0.tgz#48b9524237be553f48b158f8c9152593f3f3fded" + integrity sha512-g63rQzypPOZi0BeGsK4ST2MYhsFR+i7bhL8k/McUoWDNMDuTTdUlQ2GACKxqh5sI/dNC/6nVoPrycMnSylnAgQ== dependencies: - "@babel/runtime" "^7.7.4" + "@babel/runtime" "^7.7.7" hoist-non-react-statics "^3.3.1" + react-is "^16.12.0" "@sheerun/mutationobserver-shim@^0.3.2": version "0.3.2" @@ -3378,6 +3386,11 @@ react-dom@16.12.0: prop-types "^15.6.2" scheduler "^0.18.0" +react-is@^16.12.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4: version "16.12.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" @@ -3462,6 +3475,11 @@ regenerator-runtime@^0.13.2: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw== +regenerator-runtime@^0.13.4: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== + regenerator-transform@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.1.tgz#3b2fce4e1ab7732c08f665dfdb314749c7ddd2fb" @@ -4276,3 +4294,8 @@ yargs@^13.3.0: which-module "^2.0.0" y18n "^4.0.0" yargs-parser "^13.1.1" + +yarn@^1.22.10: + version "1.22.10" + resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.10.tgz#c99daa06257c80f8fa2c3f1490724e394c26b18c" + integrity sha512-IanQGI9RRPAN87VGTF7zs2uxkSyQSrSPsju0COgbsKQOOXr5LtcVPeyXWgwVa0ywG3d8dg6kSYKGBuYK021qeA==