UNPKG

@workday/canvas-kit-react

Version:

The parent module that contains all Workday Canvas Kit React components

272 lines (271 loc) • 12.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createModelHook = exports.createEvents = exports.useEventMap = exports.createEventMap = void 0; /* eslint-disable react-hooks/rules-of-hooks */ const react_1 = __importDefault(require("react")); /** * Convenience factory function that extracts type information and encodes it for use with model * config and `useEventMap`. Under the hood, it returns the config that was passed in. The real * magic is in type extraction and encoding which reduces boilerplate. * * `createEventMap` is a function that takes an `Events` generic and will return a function that * takes in a config object to configure all guards and callbacks. The empty function is used because * Typescript does not allow partial specification of generics (either you specify all generics or * none of them). Since `Events` cannot be inferred, it is passed to the first function. * * @example * type Events = { * open(data: { eventData: string }): void * } * * const eventMap = createEventMap<Events>()({ * guards: { * shouldOpen: 'open' * }, * callbacks: { * onOpen: 'open' * } * }) * * @deprecated `createModelHook` uses Template Literal Types to create event map types */ const createEventMap = () => (config) => { // Instruct Typescript that all valid guards and callbacks exist return config; }; exports.createEventMap = createEventMap; // small wrapper to get `keyof T` instead of `string | number | symbol` const keys = (input) => Object.keys(input); /** * This hook creates a stable reference events object to be used in a model. The reference is stable * by the use of `React.Memo` and uses React Refs to make sure there are no stale closure values. It * takes in an event map, state, model config, and an events object. It will map over each event and * add guards and callbacks to the event as configured in the event map. * * @param eventMap * @param state * @param config * @param events * * @example * const useDiscloseModel = (config: ModelConfig = {}): DiscloseModel => { * const events = useEventMap(eventMap, state, config, { * open() { * // do something * } * } * }) * @deprecated Use `createModelHook` instead */ const useEventMap = (eventMap, state, config, events) => { // use refs so we can memoize the returned `events` object const eventMapRef = react_1.default.useRef(eventMap); const stateRef = react_1.default.useRef(state); const configRef = react_1.default.useRef(config); const eventsRef = react_1.default.useRef(events); // update all the refs with current values eventMapRef.current = eventMap; stateRef.current = state; configRef.current = config; eventsRef.current = events; return react_1.default.useMemo(() => { return keys(eventsRef.current).reduce((result, key) => { result[key] = (data => { var _a, _b; // Invoke the configured guard if there is one if (!eventsRef.current[key]._wrapped) { const guardFn = keys(eventMapRef.current.guards || {}).find(k => { return (eventMapRef.current.guards || {})[k] === key; }); if (guardFn && ((_a = configRef.current) === null || _a === void 0 ? void 0 : _a[guardFn]) && //@ts-ignore Typescript doesn't like that the call signatures are different !configRef.current[guardFn]({ data, state: stateRef.current })) { return; } } // call the event (setter) eventsRef.current[key](data); if (!eventsRef.current[key]._wrapped) { // Invoke the configured callback if there is one const callbackFn = keys(eventMapRef.current.callbacks || {}).find(k => { return (eventMapRef.current.callbacks || {})[k] === key; }); if (callbackFn && ((_b = configRef.current) === null || _b === void 0 ? void 0 : _b[callbackFn])) { //@ts-ignore Typescript doesn't like that the call signatures are different configRef.current[callbackFn]({ data, prevState: stateRef.current }); } } }); // this cast keeps Typescript happy // Mark this function has been wrapped so we can detect wrapped events and not call guards and callbacks multiple times result[key]._wrapped = true; return result; }, {}); }, []); }; exports.useEventMap = useEventMap; // use this? const createEvents = (events) => (config) => { return { events, eventMap: config }; }; exports.createEvents = createEvents; function capitalize(string) { return string[0].toUpperCase() + string.substring(1); } function getGuardName(eventName) { return `should${capitalize(eventName)}`; } function getCallbackName(eventName) { return `on${capitalize(eventName)}`; } // Merges config together in a way that is type safe and works at runtime with correct rules and order applied // to guards and callbacks. function mergeConfig(sourceConfig, newConfig) { const result = { ...sourceConfig }; for (const key in newConfig) { if (typeof newConfig[key] === 'function' && /(on)[A-Z]/.test(key) && typeof sourceConfig[key] === 'function') { // merge callbacks and ignore Typescript's errors. We've already tested call signatures // @ts-ignore result[key] = (data, state) => { // @ts-ignore sourceConfig[key](data, state); // @ts-ignore newConfig[key](data, state); }; } else if (newConfig[key] && /(should)[A-Z]/.test(key) && sourceConfig[key]) { // merge guards and ignore Typescript's errors. We've already tested call signatures // @ts-ignore result[key] = (data, state) => { // @ts-ignore return sourceConfig[key](data, state) && newConfig[key](data, state); }; } else { // @ts-ignore result[key] = newConfig[key]; } } return result; } /** * Factory function to create typed models with minimal Typescript required. It is a function that * takes `defaultConfig` and `requiredConfig` and returns a function that will become the definition * of the model. The config objects will be used to generate a `getElemProps` function that is used * to separate model config an element props. * * Typescript will infer all config from the returned `state`, `events`, and the `defaultConfig` and * the `requiredConfig`. `defaultConfig` serves the additional purpose of being a default value if * no config value was provided. `requiredConfig` is a little odd in that config values are ignored * and only used to extract types. * * @example * const useModel = createModelHook({ * defaultConfig: { * optional: 'right' as 'left' | 'right', // optional type casting * }, * requiredConfig: { * size: 1, // values are only used for types and are ignored at runtime * } * })(config => { * // config is pre-typed for you based on `defaultConfig` * }) */ const createModelHook = (options) => { const { defaultConfig = {}, requiredConfig = {}, defaultContext, contextOverride } = options; // create a bunch of refs so we can define the `wrappedModel` function once. const fnRef = { current: null }; const callbacksRef = { current: [] }; const guardsRef = { current: [] }; const Context = contextOverride || react_1.default.createContext(defaultContext || { state: {}, events: {} }); const getElemProps = (props) => { // if (eventsRef.current === null) { // throw Error( // `useModel.getElemProps() must be called after useModel(). getElemProps needs to determine the events returned by the model to function correctly.\nExample:\nconst model = useModel();\nconst elemProps = useModel.getElemProps(props);` // ); // } const elemProps = {}; for (const key in props) { if ((!defaultConfig.hasOwnProperty(key) && !requiredConfig.hasOwnProperty(key) && !callbacksRef.current.includes(key) && !guardsRef.current.includes(key)) || key === 'id') { // @ts-ignore Typescript complains about index signatures and this type is never exposed in definitions, so suppress the error elemProps[key] = props[key]; } } return elemProps; }; // we use `any` here because we don't know what the type is here and it is internal. No use // slowing down Typescript to bother type checking function wrappedModelHook(config) { const stateRef = react_1.default.useRef({}); const configRef = react_1.default.useRef({}); const eventsRef = react_1.default.useRef({}); // We want to apply defaults, but if we merge config together using a spread, a value of // `undefined` will override which will cause issues when config values are not expected to be // undefined const finalConfig = { ...defaultConfig }; for (const key in config || {}) { if (config[key] !== undefined || // @ts-ignore if `defaultConfig` has a property of `key`, it has `key` in the index signature. Come on, TypeScript (defaultConfig.hasOwnProperty(key) && defaultConfig[key] === undefined)) { finalConfig[key] = config[key]; } } const { state, events, ...rest } = fnRef.current(finalConfig); // update all the refs with current values stateRef.current = state; configRef.current = config || {}; eventsRef.current = events; if (!callbacksRef.current.length) { callbacksRef.current = keys(events).map(getCallbackName); } if (!guardsRef.current.length) { guardsRef.current = keys(events).map(getGuardName); } const wrappedEvents = react_1.default.useMemo(() => { return keys(eventsRef.current).reduce((result, key) => { result[key] = (data) => { // Invoke the configured guard if there is one if (!eventsRef.current[key]._wrapped) { const guardFnName = getGuardName(key); if (configRef.current[guardFnName] && !configRef.current[guardFnName](data, stateRef.current)) { return; } } // call the event (setter) eventsRef.current[key](data); const callbackFnName = getCallbackName(key); if (!eventsRef.current[key]._wrapped) { if (configRef.current[callbackFnName]) { configRef.current[callbackFnName](data, stateRef.current); } } }; // Mark this function has been wrapped so we can detect wrapped events and not call guards and callbacks multiple times result[key]._wrapped = true; return result; }, {}); }, []); // The model context is private and should never be used return { state, events: wrappedEvents, __UNSTABLE_modelContext: Context, ...rest }; } wrappedModelHook.getElemProps = getElemProps; wrappedModelHook.defaultConfig = defaultConfig; wrappedModelHook.requiredConfig = requiredConfig; wrappedModelHook.mergeConfig = mergeConfig; wrappedModelHook.Context = Context; return (fn) => { fnRef.current = fn; return wrappedModelHook; // Typescript complains about the `fn` not matching the signature. It's okay Typescript. It works at runtime }; }; exports.createModelHook = createModelHook;