@workday/canvas-kit-react
Version:
The parent module that contains all Workday Canvas Kit React components
272 lines (271 loc) • 12.4 kB
JavaScript
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;
;