Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/healthy-comics-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@reactive-forms/x': minor
---

Created useDecimalField hook
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ yarn-error.log*

scripts/*.mjs
coverage
**/.turbo
**/.turbo

# vscode
launch.json
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"name": "Debug Core Tests",
"type": "node",
"request": "launch",
"runtimeArgs": ["--inspect-brk", "${workspaceFolder}/node_modules/aqu/dist/aqu.js", "test", "--runInBand"],
"cwd": "${workspaceFolder}/packages/core",
"runtimeArgs": ["--inspect-brk", ".\\node_modules\\jest\\bin\\jest.js", "--watch", "useDecimalField"],
"cwd": "${workspaceFolder}/packages/x",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
Expand Down
15 changes: 15 additions & 0 deletions .vscode/launch.template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Core Tests",
"type": "node",
"request": "launch",
"runtimeArgs": ["--inspect-brk", ".\\node_modules\\jest\\bin\\jest.js", "--watch"],
"cwd": "${workspaceFolder}/packages/core",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
}
]
}
30 changes: 30 additions & 0 deletions packages/x/src/DecimalFieldI18n.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { createContext, PropsWithChildren } from 'react';
import merge from 'lodash/merge';

import { formatDecimal } from './formatDecimal';

export type DecimalFieldI18n = {
required: string;
invalidInput: string;
minValue: (value: number, precision: number) => string;
maxValue: (value: number, precision: number) => string;
};

export const defaultDecimalFieldI18n: DecimalFieldI18n = {
required: 'Field is required',
invalidInput: 'Must be decimal',
minValue: (min: number, precision: number) => `Value should not be less than ${formatDecimal(min, precision)}`,
maxValue: (max: number, precision: number) => `Value should not be greater than ${formatDecimal(max, precision)}`,
};

export const DecimalFieldI18nContext = createContext<DecimalFieldI18n>(defaultDecimalFieldI18n);

export type DecimalFieldI18nContextProviderProps = PropsWithChildren<{ i18n?: Partial<DecimalFieldI18n> }>;

export const DecimalFieldI18nContextProvider = ({ i18n, children }: DecimalFieldI18nContextProviderProps) => {
return (
<DecimalFieldI18nContext.Provider value={merge(defaultDecimalFieldI18n, i18n)}>
{children}
</DecimalFieldI18nContext.Provider>
);
};
7 changes: 7 additions & 0 deletions packages/x/src/formatDecimal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const formatDecimal = (value: number | null | undefined, precision: number) => {
if (typeof value !== 'number' || !Number.isFinite(value)) {
return '';
}

return value.toFixed(precision).toString();
};
6 changes: 4 additions & 2 deletions packages/x/src/useConverterField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ export class ConversionError extends Error {
}
}

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

export type ConverterFieldConfig<T> = ValueConverter<T> & FieldConfig<T>;

export type ConverterFieldBag<T> = {
text: string;
Expand Down
104 changes: 104 additions & 0 deletions packages/x/src/useDecimalField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useCallback, useContext } from 'react';
import { FieldConfig, useFieldValidator } from '@reactive-forms/core';

import { DecimalFieldI18nContext } from './DecimalFieldI18n';
import { formatDecimal } from './formatDecimal';
import { ConversionError, ConverterFieldBag, useConverterField, ValueConverter } from './useConverterField';

const DECIMAL_REGEX = /^\d*\.?\d*$/;
export const defaultPrecision = 2;

export type DecimalFieldConfig = FieldConfig<number | null | undefined> & {
required?: boolean;
min?: number;
max?: number;

precision?: number;
} & Partial<ValueConverter<number | null | undefined>>;

export type DecimalFieldBag = ConverterFieldBag<number | null | undefined>;

export const useDecimalField = ({
name,
validator,
schema,
required,
min,
max,
format: customFormat,
parse: customParse,
precision = defaultPrecision,
}: DecimalFieldConfig): DecimalFieldBag => {
const i18n = useContext(DecimalFieldI18nContext);

const parse = useCallback(
(text: string) => {
text = text.trim();

if (customParse) {
return customParse(text);
}

if (text.length === 0) {
return null;
}

if (!DECIMAL_REGEX.test(text)) {
throw new ConversionError(i18n.invalidInput);
}

const value = Number.parseFloat(text);

if (Number.isNaN(value)) {
throw new ConversionError(i18n.invalidInput);
}

return value;
},
[customParse, i18n.invalidInput],
);

const format = useCallback(
(value: number | null | undefined) => {
if (customFormat) {
return customFormat(value);
}

return formatDecimal(value, precision);
},
[customFormat, precision],
);

const decimalBag = useConverterField({
parse,
format,
name,
validator,
schema,
});

useFieldValidator({
name,
validator: (value) => {
if (required && typeof value !== 'number') {
return i18n.required;
}

if (typeof value !== 'number') {
return undefined;
}

if (typeof min === 'number' && value < min) {
return i18n.minValue(min, precision);
}

if (typeof max === 'number' && value > max) {
return i18n.maxValue(max, precision);
}

return undefined;
},
});

return decimalBag;
};
Loading