@kwiz/fluentui
Version:
KWIZ common controls for FluentUI
157 lines • 6.09 kB
JavaScript
import { makeStyles } from "@fluentui/react-components";
import { isFunction, isNotEmptyArray, isNullOrEmptyString, isPrimitiveValue, jsonClone, jsonStringify, LoggerLevel, objectsEqual } from "@kwiz/common";
import { useCallback, useEffect, useRef, useState } from "react";
import { GetLogger } from "../_modules/config";
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 = GetLogger(`useStateWithTrack${isNullOrEmptyString(name) ? '' : ` ${name}`}`);
logger.setLevel(LoggerLevel.WARN);
const [value, setValueInState] = useState(initialValue);
const currentValue = useRef();
//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);
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);
function resolvePromises() {
if (isNotEmptyArray(resolveState.current)) {
let resolvers = resolveState.current.slice();
resolveState.current = []; //clear
resolvers.map(r => r(currentValue.current));
}
}
;
useEffect(() => {
resolvePromises();
}, [value]);
function getIsValueChanged(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.groupSync(result ? 'value changed' : 'value not changed', log => {
if (logger.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;
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.log(`resolved without wait`);
resolve(newState);
}
else {
resolveState.current.push(resolve);
const isChanged = isFunction(onChange.current) || options.skipUpdateIfSame
? 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 [value, setValue, currentValue];
}
/** use a ref, that can be tracked as useEffect dependency */
export function useRefWithState(initialValue, stateOptions = { skipUpdateIfSame: true, name: "useRefWithState" }) {
let asRef = useRef(initialValue);
let [asState, setState] = useStateEX(initialValue, stateOptions);
let setRef = useCallback((newValue) => {
asRef.current = newValue;
setState(newValue);
}, useEffectOnlyOnMount);
return {
/** ref object for getting latest value in handlers */
ref: asRef,
/** for useEffect dependency */
value: asState,
/** for setting on element: ref={e.set} */
set: 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