react-view-slot
Version:
Dynamic view slots for React
221 lines • 7.15 kB
JavaScript
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
/**
* Provides `Slot` and `Plug` mechanic.
*
* `Slot`s are places that other components can mount to using `Plug`s.
* To define a slot mount-point, use `<Slot name="name" />`.
*
* @example
* <SlotsProvider>
* <header>
* <Slot name="header" />
* </header>
*
* <Plug slot="header" header="user-view">
* Im mouted in header!
* </Plug>
* </SlotsProvider>
*
* @example
*
* // type-safe API
* const [SomeSlot, SomePlug] = createSlotAndPlug("some-slot");
*
* const SomeComponent = () => (
* <>
* <SomeSlot />
* <SomePlug>test</SomePlug>
* </>
* );
*
* @example
*
* // type-safe API
* const SomeSlot = createSlot("some-slot");
*
* const SomeComponent = () => (
* <>
* <SomeSlot />
* <SomeSlot.Plug>test</SomeSlot.Plug>
* </>
* );
*
* @see Slot
* @see Plug
* @see SlotProvider
*/
import * as React from 'react';
import sortBy from 'lodash.sortby';
/**
* Context instance of this module.
*/
const context = React.createContext(null);
/**
* Hook that returns all plugs for specified slot name.
*
* This hook is meant for advanced use or more fine-grained control.
* For most cases the `Slot` component, or `createSlot` should be used.
*
* In case no plugs have been registered or slot was not yet defined,
* empty array is returned.
*
* @param name Slot name to fetch
*/
export function useSlot(name) {
const ctx = React.useContext(context);
if (!ctx) {
throw new Error("Using <Slot> component or useSlot hook outside of <SlotProvider>");
}
return ctx.slots[name] || [];
}
/**
* React component that renders all registered plugs for specified slot name.
*
* Rendering can be customized using render function as children.
*
* @example
*
* <Slot name="header" />
*
* @example
*
* <Slot name="header">
* {plugs => (
* <React.Fragment>
* {plugs.map(Plug => (
* <Plug key={Plug.id} myCustomProp={5} />
* ))}
* </React.Fragment>
* )}
* </Slot>
*/
export function Slot(props) {
const { name, maxCount, reversed, children, params } = props, rest = __rest(props, ["name", "maxCount", "reversed", "children", "params"]);
let slots = useSlot(name);
if (params && children) {
throw Error("Cannot specify render function and params props at the same time");
}
return React.useMemo(() => {
if (maxCount !== undefined) {
slots = slots.slice(0, maxCount);
}
if (reversed !== undefined) {
slots = [...slots].reverse();
}
if (children) {
return children(slots);
}
return (React.createElement(React.Fragment, {}, slots.map((R, i) => React.createElement(R, Object.assign({ key: i.toString() }, params)))));
}, [reversed, maxCount, params, slots, children]);
}
/**
* Creates a hook that defines a new plug.
*
* This function is rather low-level and probably `<Plug>` component
* or `createSlotAndPlug` is a better choice.
*
* @param slot Slot name
* @param id Unique plug name
* @param renderer Function to render components
* @param deps Array of dependencies to trigger the update
* @param options Additional plug options
* @see createSlotAndPlug
* @see createSlot
* @see Plug
*/
export function usePlug(slot, id, renderer, deps, options) {
const slotContext = React.useContext(context);
if (!slotContext) {
throw new Error("Using <Plug> component or usePlug hook outside of <SlotProvider>");
}
const { setPlug, removePlug } = slotContext;
const { name, order, extra } = options;
Object.assign(renderer, { slotName: name, extra, order });
React.useEffect(() => {
setPlug(slot, id, renderer);
return () => removePlug(slot, id);
}, deps);
}
/**
* Plug component.
*
* Connects to specified slot and adds `children` to it.
*
* Children of this component can be normal react sub-components that will be mounted in the
* target slot, or a render function can take slot parameters.
*
* Slot name should be imported from target module to prevent any typing errors.
* More type-safe approach is to use `createSlotAndPlug` that creates a slot and plug components
* with bound name.
*
* @param slot Name of the target slot
* @param id Unique name of the plug
* @param deps Optional list of dependencies to trigger the update
* @param children Contents to mount to slot
* @param options Additional plug options
*/
export const Plug = (_a) => {
var { slot, id, deps = [], children } = _a, options = __rest(_a, ["slot", "id", "deps", "children"]);
const renderer = (typeof children === 'function') ? children : () => children;
usePlug(slot, id, renderer, deps, options);
return null;
};
/**
* Creates a named Slot component.
*
* @param name Name of the slot
*/
export function createSlot(name) {
const slot = (props) => React.createElement(Slot, Object.assign({ name }, props));
slot.slotName = name;
slot.Plug = createPlugComponent(name);
return slot;
}
function createPlugComponent(slotName) {
const plugComponent = (props) => React.createElement(Plug, Object.assign({ slot: slotName }, props));
plugComponent.displayName = `Slot(${slotName})`;
return plugComponent;
}
/**
* Creates a named Slot component and a Plug component bound to it.
*
* This is the recommended way to create slots.
*
* @param name Name of the slot
*/
export function createSlotAndPlug(name) {
const boundSlot = createSlot(name);
return [boundSlot, boundSlot.Plug];
}
/**
* Component that provides slot and plugs context down the component tree.
*
* This component is required for `Plug` and `Slot` components to work.
*/
export const SlotProvider = ({ children }) => {
const setPlug = (slot, id, renderer) => {
Object.assign(renderer, { id });
setSlots((prevState) => (Object.assign(Object.assign({}, prevState), { slots: Object.assign(Object.assign({}, prevState.slots), { [slot]: sortBy([...(prevState.slots[slot] || []).filter(e => e.id !== id), renderer], e => e.order || 0) }) })));
};
const removePlug = (slot, name) => {
setSlots((prevState) => (Object.assign(Object.assign({}, prevState), { slots: Object.assign(Object.assign({}, prevState.slots), { [slot]: sortBy((prevState.slots[slot] || []).filter((e) => e.id !== name), e => e.order || 0) }) })));
};
const initialSlots = {
slots: {},
setPlug,
removePlug,
};
const [slots, setSlots] = React.useState(initialSlots);
return (React.createElement(context.Provider, { value: slots }, children));
};
//# sourceMappingURL=slots.js.map