@kwiz/fluentui
Version:
KWIZ common controls for FluentUI
174 lines • 7.25 kB
JavaScript
import { makeStyles } from "@fluentui/react-components";
import { CommonLogger, isFunction, isNotEmptyArray, isNullOrEmptyString, isNullOrUndefined, isPrimitiveValue, jsonClone, jsonStringify, LoggerLevel, objectsEqual } from "@kwiz/common";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { mixins } from "../styles/styles";
/** Empty array ensures that effect is only run on mount */
export const useEffectOnlyOnMount = [];
function extractStringValue(e) {
try {
if (e instanceof HTMLElement)
return e.outerHTML;
}
catch (e) { }
try {
let json = jsonStringify(e);
if (json === "{}")
return Object.keys(e).join(); //maybe just object with functions, no members or values
else
return json;
}
catch (e) { }
try {
return e.toString();
}
catch (e) { }
return '';
}
/** set state on steroids. provide promise callback after render, onChange transformer and automatic skip-set when value not changed */
export function useStateEX(initialValue, options) {
options = options || {};
const name = options.name || '';
let logger = useMemo(() => new CommonLogger(`useStateWithTrack${isNullOrEmptyString(name) ? '' : ` ${name}`}`), [name]);
logger.i.setLevel(LoggerLevel.WARN);
const [value, setValueInState] = useState(initialValue);
//you can't flip those options between renders
const needToTrackChanges = useMemo(() => options.skipUpdateIfSame || isFunction(options.onChange), []);
const currentValue = useRef(initialValue); //must set initial value don't rely on useEffect to do it since some dependents might try to access it on first load
//json clone complex/ref values so we can compare if value changed, in case caller makes chagnes on the value object directly.
const currentValueForChecks = useRef();
useEffect(() => {
updateCurrentRef(initialValue);
}, useEffectOnlyOnMount);
//keep a ref to onChange so the caller's latet state is accessible
const onChange = useRef(options.onChange);
//keep it in sync
onChange.current = options.onChange;
/** make this a collection in case several callers are awaiting the same propr update */
const resolveState = useRef([]);
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, useEffectOnlyOnMount);
const resolvePromises = useCallback(() => {
if (isNotEmptyArray(resolveState.current)) {
let resolvers = resolveState.current.slice();
resolveState.current = []; //clear
resolvers.map(r => r(currentValue.current));
}
}, []);
useEffect(() => {
resolvePromises();
}, [value]);
const getIsValueChanged = useCallback((newValue) => {
let error = null;
let result;
try {
if (!objectsEqual(newValue, currentValueForChecks.current)) {
result = true;
}
else {
result = false;
}
}
catch (e) {
error = e;
result = true;
}
return logger.i.groupSync(result ? 'value changed' : 'value not changed', log => {
if (logger.i.getLevel() === LoggerLevel.VERBOSE) {
log('old: ' + extractStringValue(currentValueForChecks.current));
log('new: ' + extractStringValue(newValue));
if (error)
log({ label: "Error", value: error });
}
return result;
});
}, []);
function updateCurrentRef(newValue) {
currentValue.current = newValue; //always update this ref- it is being used by callers, its fast and not expensive
//this json clone on html elements for useRefWithState was killing our app
//we don't need to clone currentValueForChecks if we don't have to
if (!needToTrackChanges)
return;
currentValueForChecks.current = isPrimitiveValue(newValue) || isFunction(newValue)
? newValue
//fix skipUpdateIfSame for complex objects
//if we don't clone it, currentValue.current will be a ref to the value in the owner
//and will be treated as unchanged object, and it will be out of sync
//this leads to skipUpdateIfSame failing after just 1 unchanged update
: jsonClone(newValue);
}
const setValue = useCallback((newState) => new Promise(resolve => {
if (!isMounted.current) {
//unmounted may never resolve
logger.i.log(`resolved without wait`);
resolve(newState);
}
else {
resolveState.current.push(resolve);
const isChanged = needToTrackChanges
? getIsValueChanged(newState) //don't call this if there is no onChange handler and if we don't need to monitor skipUpdateIfSame
: true;
if (isFunction(onChange.current))
newState = onChange.current(newState, isChanged);
//keep current value ref up to date
updateCurrentRef(newState);
//set state
if (!options.skipUpdateIfSame || isChanged)
setValueInState(newState);
else //don't set in state - just resolve pending promises, UI will not be updated.
resolvePromises();
}
}), useEffectOnlyOnMount);
return useMemo(() => [value, setValue, currentValue], [value, setValue, currentValue]);
}
/** use a ref, that can be tracked as useEffect dependency */
export function useRefWithState(initialValue, stateOptions = { skipUpdateIfSame: true, name: "useRefWithState" },
/** if used in a control that also needs a forwardRef, set this to keep them in sync */
forwardRef) {
let asRef = useRef(initialValue);
let [asState, setState] = useStateEX(initialValue, stateOptions);
//incorrect to use useCallback with no dependencies, broke licensing vertical tab list
function setRef(newValue) {
asRef.current = newValue;
setState(newValue);
}
;
useEffect(() => {
//setting the forwardRef
if (!isNullOrUndefined(forwardRef)) {
if (isFunction(forwardRef))
forwardRef(asRef.current);
else
forwardRef.current = asRef.current;
}
}, [asState]);
return useMemo(() => ({
/** ref object for getting latest value in handlers */
ref: asRef,
/** for useEffect dependency */
value: asState,
/** for setting on element: ref={e.set} */
set: setRef
}), [asRef, asState, setRef]);
}
const useStyles = makeStyles({
clickable: mixins.clickable,
});
/** return props to make div appear as clickable, and accept enter key as click */
export function useClickableDiv() {
const cssNames = useStyles();
const props = {
className: cssNames.clickable,
tabIndex: 0,
onKeyDown: e => {
if (e.key === "Enter")
e.target.click();
}
};
return props;
}
//# sourceMappingURL=hooks.js.map