@layr/react-integration
Version:
React integration for Layr
308 lines • 12.8 kB
JavaScript
import { callRouteByURL, assertIsRoutableClass } from '@layr/routable';
import { BrowserNavigator } from '@layr/browser-navigator';
import { formatError } from '@layr/utilities';
import React, { useRef, useState, useEffect, useContext } from 'react';
import { useForceUpdate } from './hooks';
import { BrowserNavigatorPlugin } from './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
*/
export function BrowserRootView({ children, ...customization }) {
const previousCustomization = useContext(CustomizationContext);
if (previousCustomization !== undefined) {
throw new Error("An app shouldn't have more than one RootView");
}
const actionView = useRef(null);
return (React.createElement(CustomizationContext.Provider, { value: {
dataPlaceholder: () => null,
errorRenderer: (error) => {
console.error(error);
return React.createElement("div", null, formatError(error));
},
actionWrapper: async (actionHandler, args) => {
actionView.current.open();
try {
return await actionHandler(...args);
}
finally {
actionView.current.close();
}
},
errorNotifier: async (error) => {
alert(formatError(error));
},
...customization
} },
React.createElement(BrowserActionView, { ref: actionView }),
children));
}
export const NavigatorContext = React.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
*/
export function BrowserNavigatorView({ rootComponent }) {
assertIsRoutableClass(rootComponent);
const navigatorRef = useRef();
if (navigatorRef.current === undefined) {
navigatorRef.current = new BrowserNavigator({ plugins: [BrowserNavigatorPlugin()] });
rootComponent.registerNavigator(navigatorRef.current);
}
const [isReady, setIsReady] = useState(false);
const forceUpdate = useForceUpdate();
useEffect(() => {
navigatorRef.current.addObserver(forceUpdate);
setIsReady(true);
return function () {
navigatorRef.current.removeObserver(forceUpdate);
navigatorRef.current.unmount();
};
}, []);
if (!isReady) {
return null;
}
return (React.createElement(NavigatorContext.Provider, { value: navigatorRef.current }, callRouteByURL(rootComponent, navigatorRef.current.getCurrentURL())));
}
/**
* 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
*/
export function useNavigator() {
const navigator = useContext(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;
}
export const CustomizationContext = React.createContext(undefined);
export function useCustomization() {
const customization = useContext(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;
}
/**
* 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
*/
export function Customizer({ children, ...customization }) {
const previousCustomization = useCustomization();
return (React.createElement(CustomizationContext.Provider, { value: { ...previousCustomization, ...customization } }, children));
}
export class BrowserActionView extends React.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.createElement("div", { style: {
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
zIndex: 30000
} }));
}
}
//# sourceMappingURL=components.js.map