Skip to content

Commit

Permalink
Create converter field abstraction (#287)
Browse files Browse the repository at this point in the history
* Created `useConverterField` hook

* Synchronize form value with text state

* Created changeset

* Added more tests

* Added validator in ConverterField

* Handled one more test case

* Set field touched=true on blur

* Created new option "ignoreFormStateUpdatesWhileFocus"

* Removed unused dependency

* Created forceSetValue function in useConverterField

* Wrap all functions with useCallback in useConverterField

* Added test case for changing format function

* Created test for changing parse function

* Fixed entrypoint config in package.json

* Clarified useConverterField tests
  • Loading branch information
AlexShukel authored Aug 27, 2023
1 parent e2159eb commit 524f4d2
Show file tree
Hide file tree
Showing 7 changed files with 448 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/four-turkeys-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@reactive-forms/x': patch
---

Created useConverterField hook in @reactive-forms/x package
13 changes: 10 additions & 3 deletions packages/x/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"description": "Advanced Reactive Forms components for rich eXperience",
"version": "0.10.2",
"main": "dist/index.js",
"module": "dist/core.esm.js",
"types": "dist/core.d.ts",
"module": "dist/x.esm.js",
"types": "dist/x.d.ts",
"bugs": "https://github.com/fracht/reactive-forms/issues",
"homepage": "https://github.com/fracht/reactive-forms#readme",
"repository": "fracht/reactive-forms.git",
Expand All @@ -27,12 +27,16 @@
"@babel/core": "7.19.6",
"@reactive-forms/core": "workspace:*",
"@reactive-tools/eslint-config": "workspace:*",
"@testing-library/react": "13.4.0",
"@types/jest": "26.0.24",
"@types/lodash": "4.14.161",
"@types/react": "18.0.23",
"aqu": "0.4.3",
"jest": "29.2.2",
"react": "18.2.0",
"rimraf": "3.0.2",
"ts-jest": "29.0.3",
"tslib": "2.3.1",
"typescript": "4.8.4"
},
"peerDependencies": {
Expand All @@ -42,5 +46,8 @@
"files": [
"dist"
],
"source": "src/index.ts"
"source": "src/index.ts",
"dependencies": {
"lodash": "4.17.21"
}
}
1 change: 1 addition & 0 deletions packages/x/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './plugin';
export * from './useConverterField';
134 changes: 134 additions & 0 deletions packages/x/src/useConverterField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { FieldConfig, FieldContext, FieldError, FieldTouched, useField, useFieldValidator } from '@reactive-forms/core';
import isObject from 'lodash/isObject';

export class ConversionError extends Error {
public constructor(errorMessage: string) {
super(errorMessage);
}
}

export type ConverterFieldConfig<T> = {
parse: (value: string) => T;
format: (value: T) => string;
} & FieldConfig<T>;

export type ConverterFieldBag<T> = {
text: string;
onTextChange: (text: string) => void;
onFocus: () => void;
onBlur: () => void;
} & FieldContext<T>;

export const useConverterField = <T>({
parse,
format,
...fieldConfig
}: ConverterFieldConfig<T>): ConverterFieldBag<T> => {
const fieldBag = useField(fieldConfig);

const {
value,
control: { setValue, setError, setTouched },
} = fieldBag;

const [isFocused, setIsFocused] = useState(false);
const [text, setText] = useState(() => format(value));
const textRef = useRef(text);
textRef.current = text;

const [hasConversionError, setHasConversionError] = useState(false);

const tryConvert = useCallback(
(text: string) => {
try {
const value = parse(text); // this could throw in case of conversion error
setValue(value);
setHasConversionError(false);
} catch (error) {
if (isObject(error) && error instanceof ConversionError) {
setHasConversionError(true);
setError({
$error: error.message,
} as FieldError<T>);
} else {
throw error;
}
}
},
[parse, setError, setValue],
);

const onTextChange = useCallback(
(newText: string) => {
textRef.current = newText;
setText(newText);
tryConvert(newText);
},
[tryConvert],
);

const onFocus = useCallback(() => {
setIsFocused(true);
}, []);

const onBlur = useCallback(() => {
setIsFocused(false);
setTouched({ $touched: true } as FieldTouched<T>);
tryConvert(text);
}, [setTouched, text, tryConvert]);

const forceSetValue = useCallback(
(value: T) => {
onTextChange(format(value));
setValue(value);
},
[format, onTextChange, setValue],
);

useFieldValidator({
name: fieldConfig.name,
validator: () => {
try {
parse(textRef.current);
} catch (error) {
if (isObject(error) && error instanceof ConversionError) {
return error.message;
}

throw error;
}

return undefined;
},
});

useEffect(() => {
if (isFocused || hasConversionError) {
return;
}

const formattedValue = format(value);
textRef.current = formattedValue;
setText(formattedValue);
}, [value, format, hasConversionError, isFocused]);

const tryConvertRef = useRef(tryConvert);

useEffect(() => {
if (tryConvertRef.current !== tryConvert) {
tryConvert(textRef.current); // Parse text again when parse function changes
}

tryConvertRef.current = tryConvert;
}, [tryConvert]);

return {
text,
onTextChange,
onFocus,
onBlur,
...fieldBag,
control: { ...fieldBag.control, setValue: forceSetValue },
};
};
Loading

0 comments on commit 524f4d2

Please sign in to comment.