All files index.tsx

100% Statements 46/46
86.96% Branches 20/23
100% Functions 10/10
100% Lines 41/41

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 1161x                                   1x             1x 1x         1x 9x 8x       8x   8x       1x 34x     34x           34x 2x 2x       34x   34x     7x 7x 7x 7x   1x 25x 25x       25x 10x     15x     1x 18x 5x       7x 9x 9x 9x 9x   9x       9x 9x       1x 25x                 1x    
import * as React from 'react';
 
export interface Props {
    value?: number;
    onChange: (value?: number) => void;
}
 
export interface State {
    formattedValue: string;
}
 
export interface Options {
    decimalSeparator: string;
    thousandSeparator: string;
    precision: number;
    allowNegativeValues: boolean;
}
 
const defaults: Options = {
    decimalSeparator: ',',
    thousandSeparator: ' ',
    precision: 2,
    allowNegativeValues: false,
};
 
export function createFormattedNumberInput<ExternalProps>(InputComponent: any, Ioptions: Partial<Options> = {}): React.ComponentClass<ExternalProps | Props> {
    const opts: Options = {
        ...defaults,
        ...options,
    };
 
    const parse = (value: string) => {
        if (value) {
            const cleaned = value
                .replace(/\s/g, '')
                .replace(new RegExp(opts.decimalSeparator), '.');
 
            const number = parseFloat(cleaned);
    
            return !isNaN(number) ? number : undefined;
        }
    }
    
    const format = (value: string) => {
        value = value.replace(opts.allowNegativeValues ? /[^\d.,-]/g : /[^\d.,]/g, '');
 
        // only keep the first decimal separator
        value = value
            .replace(/[.,]/, '_')
            .replace(/[.,]/g, '')
            .replace(/_/, opts.decimalSeparator);
    
        // only keep `opts.precision` fraction digits
        if (value.indexOf(opts.decimalSeparator) !== -1) {
            const [integer, fractional] = value.split(opts.decimalSeparator);
            value = integer + opts.decimalSeparator + fractional.substr(0, opts.precision);
        }
 
        // separate thousands
        value = value.replace(/\B(?=(\d{3})+(?!\d))/g, opts.thousandSeparator);
    
        return value;
    }
 
    return class FormattedNumberInput extends React.Component<Props & ExternalProps, State> {
        private ref = React.createRef<HTMLInputElement>();
        private caretPosition: number = 0;
        state: State = { formattedValue: '' };
 
        static getDerivedStateFromProps(nextProps: Props & ExternalProps, prevState: State) {
            const formattedValue = format(String(nextProps.value));
            const prevFormattedValueWithoutSpecialCharacters = prevState.formattedValue
                .replace(new RegExp(`${opts.decimalSeparator}0$`), '')
                .replace(new RegExp(`[${opts.decimalSeparator}-]`), '');
 
            if (formattedValue !== prevFormattedValueWithoutSpecialCharacters) {
                return { formattedValue };
            }
 
            return null;
        }
 
        componentDidUpdate(prevProps: Props & ExternalProps) {
            if (this.ref.current && prevProps.value !== this.props.value) {
                this.ref.current.setSelectionRange(this.caretPosition, this.caretPosition);
            }
        }
 
        private handleChange = (event: React.FormEvent<HTMLInputElement>) => {
            const inputted = event.currentTarget.value;
            const formatted = format(inputted);
            const parsed = parse(formatted);
            const delta = formatted.length - inputted.length;
 
            this.caretPosition = this.ref.current && this.ref.current.selectionEnd
                ? Math.max(this.ref.current.selectionEnd + delta, 0)
                : 0;
 
            this.setState({ formattedValue: formatted }, () => {
                this.props.onChange(parsed);
            });
        }
 
        render() {
            return (
                <InputComponent
                    {...this.props}
                    ref={this.ref}
                    value={this.state.formattedValue}
                    onChange={this.handleChange}
                />
            );
        }
    }
};