in uui-core/src/data/forms/useForm.ts [32:393]
export function useForm<T>(props: UseFormProps<T>): IFormApi<T> {
const context: UuiContexts = useUuiContext();
const initialForm = useRef<FormState<T>>({
isChanged: false,
isInProgress: false,
form: props.value,
validationState: { isInvalid: false },
serverValidationState: undefined,
formHistory: [props.value],
historyIndex: 0,
isInSaveMode: false,
});
const propsRef = useRef(props);
propsRef.current = props;
const getMetadata = (value: T) =>
propsRef.current.getMetadata ? propsRef.current.getMetadata(value) : {};
const prevFormValue = useRef<T>(props.value);
const formState = useRef(initialForm.current);
const forceUpdate = useForceUpdate();
const updateFormState = (
update: (current: FormState<T>) => FormState<T>,
) => {
const newState = update(formState.current);
formState.current = newState;
forceUpdate();
};
const handleSave = useCallback((isSavedBeforeLeave?: boolean) => {
let savePromise: any;
updateFormState((currentState) => {
let newState = { ...currentState, isInSaveMode: true };
newState.isInSaveMode = true;
newState = updateValidationStates(newState);
if (!newState.validationState.isInvalid) {
newState.isInProgress = true;
savePromise = propsRef.current
.onSave(formState.current.form)
.then((response) =>
handleSaveResponse(response, isSavedBeforeLeave))
.catch((err) => handleError(err));
} else {
savePromise = Promise.reject();
}
return newState;
});
return savePromise;
}, []);
const removeUnsavedChanges = useCallback(() => {
context.uuiUserSettings.set(props.settingsKey, null);
}, [context.uuiUserSettings, props.settingsKey]);
const handleLeave = useCallback(async (nextLocation?: Link, currentLocation?: Link) => {
if (props.beforeLeave) {
const res = await props.beforeLeave(nextLocation, currentLocation);
if (res === true) {
return handleSave(true);
}
if (res === false) {
removeUnsavedChanges();
return Promise.resolve();
}
if (res === 'remain') {
return Promise.resolve('remain');
}
}
return Promise.resolve();
}, [
props.beforeLeave,
handleSave,
removeUnsavedChanges,
]);
const { isLocked, block, unblock } = useLock({ handleLeave });
const getMergedValidationState = () => {
const {
form, lastSentForm, serverValidationState, validationState,
} = formState.current;
if (serverValidationState) {
const serverValidation = validateServerErrorState(form, lastSentForm, serverValidationState);
return mergeValidation(validationState, serverValidation);
}
return validationState;
};
const lens = useMemo(
() =>
new LensBuilder<T, T>({
get: () => formState.current.form,
set: (_, small: T) => {
handleFormUpdate(() => small);
return small;
},
getValidationState: getMergedValidationState,
getMetadata: () => getMetadata(formState.current.form),
}),
[],
);
useEffect(() => {
const unsavedChanges = getUnsavedChanges();
if (!unsavedChanges || !props.loadUnsavedChanges || isEqual(unsavedChanges, initialForm.current.form)) return;
props
.loadUnsavedChanges()
.then(() => handleFormUpdate(() => unsavedChanges))
.catch(() => null);
}, []);
useEffect(() => {
if (!isEqual(props.value, prevFormValue.current)) {
resetForm({
...formState.current,
form: props.value,
formHistory: formState.current.isChanged ? formState.current.formHistory : [props.value],
});
prevFormValue.current = props.value;
}
}, [props.value]);
const getUnsavedChanges = (): T => {
return context.uuiUserSettings.get<T>(props.settingsKey);
};
const handleFormUpdate = (update: (current: T) => T, options?: { addCheckpoint?: boolean }) =>
updateFormState((currentState) => {
options = options ?? {};
options.addCheckpoint = options.addCheckpoint ?? true;
const newForm = update(currentState.form);
let { historyIndex, formHistory } = currentState;
// Determine if change is significant and we need to create new checkpoint.
// If false - we'll just update the latest checkpoint.
// We need to always create a checkpoint at the first change, to save initial form state.
const needCheckpoint = historyIndex === 0 || shouldCreateUndoCheckpoint(formHistory[historyIndex - 1], formHistory[historyIndex], newForm);
if (options.addCheckpoint && needCheckpoint) {
historyIndex++;
}
formHistory = formHistory.slice(0, historyIndex).concat(newForm);
if (options.addCheckpoint || context.uuiUserSettings.get(props.settingsKey)) {
context.uuiUserSettings.set(props.settingsKey, newForm);
}
const isChanged = !isEqual(initialForm.current.form, newForm);
if (isChanged === true) {
block();
} else {
unblock();
}
let newState = {
...currentState,
form: newForm,
isChanged: isChanged,
historyIndex,
formHistory,
};
if (currentState.isInSaveMode || props.validationOn === 'change') {
newState = updateValidationStates(newState);
}
return newState;
});
const resetForm = (withNewState: FormState<T>) =>
updateFormState((currentState) => {
const newFormState = { ...currentState, ...withNewState };
if (newFormState !== currentState) {
initialForm.current = newFormState;
return newFormState;
}
});
const updateValidationStates = (state: FormState<T>): FormState<T> => {
const valueToValidate = state.form;
const metadata = getMetadata(valueToValidate);
const isInSaveMode = state.isInSaveMode;
const validationMode = isInSaveMode || !props.validationOn ? 'save' : props.validationOn;
const validationState = uuiValidate(valueToValidate, metadata, initialForm.current.form, validationMode);
const newState = { ...state, validationState };
if (!validationState.isInvalid) {
// When form became valid, we switch inSaveMode to false
newState.isInSaveMode = false;
}
return newState;
};
const handleError = (err?: any) => {
updateFormState((currentValue) => ({
...currentValue,
isInProgress: false,
}));
propsRef.current.onError?.(err);
};
const handleSaveResponse = (response: FormSaveResponse<T> | void, isSavedBeforeLeave?: boolean) => {
const newFormValue = (response && response.form) || formState.current.form;
const newState: FormState<T> = {
...formState.current,
historyIndex: 0,
formHistory: [newFormValue],
isChanged: response && response.validation?.isInvalid ? formState.current.isChanged : false,
form: newFormValue,
isInProgress: false,
serverValidationState: (response && response.validation) || formState.current.serverValidationState,
lastSentForm: response && response.validation?.isInvalid ? response.form || formState.current.form : formState.current.lastSentForm,
};
if (response && response.validation) {
flushSync(() => {
updateFormState(() => newState);
});
return;
}
flushSync(() => {
resetForm(newState);
removeUnsavedChanges();
unblock();
});
if (propsRef.current.onSuccess && response) {
propsRef.current.onSuccess(response.form, isSavedBeforeLeave);
}
};
const handleUndo = useCallback(
() =>
updateFormState((currentState) => {
const { formHistory, historyIndex } = currentState;
const previousIndex = historyIndex - 1;
if (previousIndex >= 0) {
const previousItem = formHistory[previousIndex];
let newState = {
...currentState,
isChanged: previousIndex !== 0,
form: previousItem,
historyIndex: previousIndex,
};
if (currentState.validationState.isInvalid) {
newState = updateValidationStates(newState);
}
return newState;
} else {
return currentState;
}
}),
[],
);
const handleRedo = useCallback(
() =>
updateFormState((currentState) => {
const { formHistory, historyIndex } = currentState;
const nextIndex = historyIndex + 1;
if (nextIndex < currentState.formHistory.length) {
const nextItem = formHistory[nextIndex];
let newState = {
...currentState, form: nextItem, historyIndex: nextIndex, isChanged: true,
};
if (currentState.validationState.isInvalid) {
newState = updateValidationStates(newState);
}
return newState;
} else {
return currentState;
}
}),
[],
);
const validate = useCallback(() => {
const formSate = { ...formState.current, isInSaveMode: true };
const newState = updateValidationStates(formSate);
updateFormState(() => newState);
return newState.validationState;
}, []);
const handleRevert = useCallback(() => {
resetForm(initialForm.current);
}, [props.value]);
const handleValueChange = useCallback((newValue: T) => {
handleFormUpdate(() => newValue);
}, []);
const handleSetValue = useCallback((value: React.SetStateAction<T>) => {
handleFormUpdate((currentValue) => {
const newValue: T = value instanceof Function ? value(currentValue) : value;
return newValue;
});
}, []);
const handleReplaceValue = useCallback((value: React.SetStateAction<T>) => {
updateFormState((currentValue) => {
const newFormValue = value instanceof Function ? value(currentValue.form) : value;
return {
...currentValue,
form: newFormValue,
};
});
}, []);
const saveCallback = useCallback(() => {
handleSave().catch(() => {});
}, [handleSave]);
const handleClose = useCallback(() => {
return isLocked ? handleLeave() : Promise.resolve();
}, [isLocked]);
const setServerValidationState = useCallback((value: React.SetStateAction<ValidationState>) => {
updateFormState((currentValue) => {
const newValue = value instanceof Function ? value(currentValue.serverValidationState) : value;
return {
...currentValue,
serverValidationState: newValue,
};
});
}, []);
const mergedValidationState = getMergedValidationState();
return {
setValue: handleSetValue,
replaceValue: handleReplaceValue,
isChanged: formState.current.isChanged,
close: handleClose,
lens,
save: saveCallback,
undo: handleUndo,
redo: handleRedo,
revert: handleRevert,
validate,
canUndo: formState.current.historyIndex > 0,
canRedo: formState.current.historyIndex < formState.current.formHistory.length - 1,
canRevert: formState.current.form !== props.value,
value: formState.current.form,
onValueChange: handleValueChange,
isInvalid: mergedValidationState.isInvalid,
validationMessage: mergedValidationState.validationMessage,
validationProps: mergedValidationState.validationProps,
serverValidationState: formState.current.serverValidationState,
setServerValidationState,
isInProgress: formState.current.isInProgress,
};
}