UNPKG

@layr/react-integration

Version:
318 lines 13.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BrowserActionView = exports.Customizer = exports.useCustomization = exports.CustomizationContext = exports.useNavigator = exports.BrowserNavigatorView = exports.NavigatorContext = exports.BrowserRootView = void 0; const tslib_1 = require("tslib"); const routable_1 = require("@layr/routable"); const browser_navigator_1 = require("@layr/browser-navigator"); const utilities_1 = require("@layr/utilities"); const react_1 = tslib_1.__importStar(require("react")); const hooks_1 = require("./hooks"); const plugins_1 = require("./plugins"); /** * A React component providing sensible defaults for a web app. * * You should use this component once at the top of your app. * * Note that if you use [Boostr](https://boostr.dev/) to manage your app development, this component will be automatically mounted, so you don't have to use it explicitly in your code. * * The main point of this component is to provide the default behavior of high-level hooks such as [`useData()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) or [`useAction()`](https://layrjs.com/docs/v2/reference/react-integration#use-action-react-hook): * * - `useData()` will render `null` while the `getter()` function is running, and, in the case an error is thrown, a `<div>` containing an error message will be rendered. * - `useAction()` will prevent the user from interacting with any UI element in the browser page while the `handler()` function is running, and, in the case an error is thrown, the browser's `alert()` function will be called to display the error message. * * @examplelink See an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component. * * @category React Components * @reactcomponent */ function BrowserRootView({ children, ...customization }) { const previousCustomization = (0, react_1.useContext)(exports.CustomizationContext); if (previousCustomization !== undefined) { throw new Error("An app shouldn't have more than one RootView"); } const actionView = (0, react_1.useRef)(null); return (react_1.default.createElement(exports.CustomizationContext.Provider, { value: { dataPlaceholder: () => null, errorRenderer: (error) => { console.error(error); return react_1.default.createElement("div", null, (0, utilities_1.formatError)(error)); }, actionWrapper: async (actionHandler, args) => { actionView.current.open(); try { return await actionHandler(...args); } finally { actionView.current.close(); } }, errorNotifier: async (error) => { alert((0, utilities_1.formatError)(error)); }, ...customization } }, react_1.default.createElement(BrowserActionView, { ref: actionView }), children)); } exports.BrowserRootView = BrowserRootView; exports.NavigatorContext = react_1.default.createContext(undefined); /** * A React component providing a [`BrowserNavigator`](https://layrjs.com/docs/v2/reference/browser-navigator#browser-navigator-class) to your app. * * You should use this component once at the top of your app after the [`BrowserRootView`](https://layrjs.com/docs/v2/reference/react-integration#browser-root-view-react-component) component. * * Note that if you use [Boostr](https://boostr.dev/) to manage your app development, this component will be automatically mounted, so you don't have to use it explicitly in your code. * * @param props.rootComponent The root Layr component of your app. Note that this Layr component should be [`Routable`](https://layrjs.com/docs/v2/reference/routable#routable-component-class). * * @example * ``` * // JS * * import React, {Fragment} from 'react'; * import ReactDOM from 'react-dom'; * import {Component} from '@layr/component'; * import {Routable} from '@layr/routable'; * import {BrowserRootView, BrowserNavigatorView, layout, page} from '@layr/react-integration'; * * class Application extends Routable(Component) { * // `@layout('/')` is a shortcut for `@wrapper('/') @view()` * ﹫layout('/') static MainLayout({children}) { * return ( * <> * <this.HomePage.Link> * <h1>My App</h1> * </this.HomePage.Link> * * {children()} // Renders the subcomponents using this layout * </> * ); * } * * // `@page('[/]')` is a shortcut for `@route('[/]') @view()` * ﹫page('[/]') static HomePage() { * return <p>Hello, World!</p>; * } * } * * // Note that you don't need the following code when you use Boostr * ReactDOM.render( * <BrowserRootView> * <BrowserNavigatorView rootComponent={Application} /> * </BrowserRootView>, * // Your `index.html` page should contain `<div id="root"></div>` * document.getElementById('root') * ); * ``` * * @example * ``` * // TS * * import React, {Fragment} from 'react'; * import ReactDOM from 'react-dom'; * import {Component} from '@layr/component'; * import {Routable} from '@layr/routable'; * import {BrowserRootView, BrowserNavigatorView, layout, page} from '@layr/react-integration'; * * class Application extends Routable(Component) { * // `@layout('/')` is a shortcut for `@wrapper('/') @view()` * ﹫layout('/') static MainLayout({children}: {children: () => any}) { * return ( * <> * <this.HomePage.Link> * <h1>My App</h1> * </this.HomePage.Link> * * {children()} // Renders the subcomponents using this layout * </> * ); * } * * // `@page('[/]')` is a shortcut for `@route('[/]') @view()` * ﹫page('[/]') static HomePage() { * return <p>Hello, World!</p>; * } * } * * // Note that you don't need the following code when you use Boostr * ReactDOM.render( * <BrowserRootView> * <BrowserNavigatorView rootComponent={Application} /> * </BrowserRootView>, * // Your `index.html` page should contain `<div id="root"></div>` * document.getElementById('root') * ); * ``` * * @category React Components * @reactcomponent */ function BrowserNavigatorView({ rootComponent }) { (0, routable_1.assertIsRoutableClass)(rootComponent); const navigatorRef = (0, react_1.useRef)(); if (navigatorRef.current === undefined) { navigatorRef.current = new browser_navigator_1.BrowserNavigator({ plugins: [(0, plugins_1.BrowserNavigatorPlugin)()] }); rootComponent.registerNavigator(navigatorRef.current); } const [isReady, setIsReady] = (0, react_1.useState)(false); const forceUpdate = (0, hooks_1.useForceUpdate)(); (0, react_1.useEffect)(() => { navigatorRef.current.addObserver(forceUpdate); setIsReady(true); return function () { navigatorRef.current.removeObserver(forceUpdate); navigatorRef.current.unmount(); }; }, []); if (!isReady) { return null; } return (react_1.default.createElement(exports.NavigatorContext.Provider, { value: navigatorRef.current }, (0, routable_1.callRouteByURL)(rootComponent, navigatorRef.current.getCurrentURL()))); } exports.BrowserNavigatorView = BrowserNavigatorView; /** * A hook allowing you to get the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#navigator-class) used in your app. * * @returns A [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#navigator-class) instance. * * @example * ``` * import {Component} from '﹫layr/component'; * import {Routable} from '﹫layr/routable'; * import React from 'react'; * import {view, useNavigator} from '﹫layr/react-integration'; * * import logo from '../assets/app-logo.svg'; * * class Application extends Routable(Component) { * // ... * * ﹫view() static LogoView() { * const navigator = useNavigator(); * * return <img src={logo} onClick={() => { navigator.navigate('/); }} />; * } * } * ``` * * @category High-Level Hooks * @reacthook */ function useNavigator() { const navigator = (0, react_1.useContext)(exports.NavigatorContext); if (navigator === undefined) { throw new Error("Couldn't get a navigator. Please make sure you have included a NavigatorView at the top of your React component tree."); } return navigator; } exports.useNavigator = useNavigator; exports.CustomizationContext = react_1.default.createContext(undefined); function useCustomization() { const customization = (0, react_1.useContext)(exports.CustomizationContext); if (customization === undefined) { throw new Error("Couldn't get the current customization. Please make sure you have included a RootView at the top of your React component tree."); } return customization; } exports.useCustomization = useCustomization; /** * A React component allowing you to customize the behavior of high-level hooks such as [`useData()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) or [`useAction()`](https://layrjs.com/docs/v2/reference/react-integration#use-action-react-hook). * * @param [props.dataPlaceholder] A function returning a React element (or `null`) that is rendered while the `getter()` function of the [`useData()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) hook is running. A typical use case is to render a spinner. * @param [props.errorRenderer] A function returning a React element (or `null`) that is rendered when the `getter()` function of the [`useData()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) hook throws an error. The `errorRenderer()` function receives the error as first parameter. A typical use case is to render an error message. * @param [props.actionWrapper] An asynchronous function allowing you to wrap the `handler()` function of the [`useAction()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) hook. The `actionWrapper()` function receives the `handler()` function as first parameter, should execute it, and return its result. A typical use case is to lock the screen while the `handler()` function is running so the user cannot interact with any UI element. * @param [props.errorNotifier] An asynchronous function that is executed when the `handler()` function of the [`useAction()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) hook throws an error. The `errorNotifier()` function receives the error as first parameter. A typical use case is to display an error alert dialog. * * @example * ``` * <Customizer * dataPlaceholder={() => { * // Renders a custom `LoadingSpinner` component * return <LoadingSpinner />; * }} * errorRenderer={(error) => { * // Renders a custom `ErrorMessage` component * return <ErrorMessage>{error}</ErrorMessage>; * }} * actionWrapper={async (actionHandler, args) => { * // Do whatever you want here (e.g., custom screen locking) * try { * return await actionHandler(...args); * } finally { * // Do whatever you want here (e.g., custom screen unlocking) * } * }} * errorNotifier={async (error) => { * // Calls a custom `alert()` asynchronous function * await alert(error.message); * }} * > * <YourChildComponent /> * </Customizer> * ``` * * @category React Components * @reactcomponent */ function Customizer({ children, ...customization }) { const previousCustomization = useCustomization(); return (react_1.default.createElement(exports.CustomizationContext.Provider, { value: { ...previousCustomization, ...customization } }, children)); } exports.Customizer = Customizer; class BrowserActionView extends react_1.default.Component { constructor() { super(...arguments); this.state = { count: 0, activeElement: null }; } open() { this.setState(({ count, activeElement }) => { count++; if (count === 1) { activeElement = document.activeElement; setTimeout(() => { if (typeof activeElement?.blur === 'function') { activeElement.blur(); } }, 0); } return { count, activeElement }; }); } close() { this.setState(({ count, activeElement }) => { count--; if (count === 0) { const savedActiveElement = activeElement; setTimeout(() => { if (typeof savedActiveElement?.focus === 'function') { savedActiveElement.focus(); } }, 0); activeElement = null; } return { count, activeElement }; }); } render() { if (this.state.count === 0) { return null; } if (this.props.children !== undefined) { return this.props.children; } return (react_1.default.createElement("div", { style: { position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh', zIndex: 30000 } })); } } exports.BrowserActionView = BrowserActionView; //# sourceMappingURL=components.js.map