Skip to content

Commit

Permalink
Generalisation of lowercase etc enforcements on inputs (#69)
Browse files Browse the repository at this point in the history
* Generalisation of lowercase etc enforcements

* Add own state 2 comment example

* Fix comments

* update snapshot

* -1 byte

* replace initial val

* Add test for input value replace

* remove log

* Improve readability
  • Loading branch information
istarkov authored and TrySound committed Jun 2, 2019
1 parent c19823a commit e5aa018
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 28 deletions.
26 changes: 13 additions & 13 deletions .size-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
{
"dist/rifm.umd.js": {
"bundled": 7480,
"minified": 1724,
"gzipped": 858
"bundled": 8097,
"minified": 1984,
"gzipped": 973
},
"dist/rifm.min.js": {
"bundled": 7266,
"bundled": 7276,
"minified": 1583,
"gzipped": 780
"gzipped": 783
},
"dist/rifm.cjs.js": {
"bundled": 7008,
"minified": 1650,
"gzipped": 831
"bundled": 7780,
"minified": 2016,
"gzipped": 961
},
"dist/rifm.esm.js": {
"bundled": 6937,
"minified": 1587,
"gzipped": 787,
"bundled": 7709,
"minified": 1953,
"gzipped": 917,
"treeshaked": {
"rollup": {
"code": 14,
Expand All @@ -29,9 +29,9 @@
}
},
"dist/rifm.esm.production.js": {
"bundled": 6651,
"bundled": 6655,
"minified": 1372,
"gzipped": 664,
"gzipped": 667,
"treeshaked": {
"rollup": {
"code": 14,
Expand Down
27 changes: 24 additions & 3 deletions pages/case-enforcement/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ const Example = () /*:React.Node*/ => {
const [uppercase, setUppercase] = React.useState('');
const [capitalized, setCapitalized] = React.useState('');
const [latinLetters, setLatinLetters] = React.useState('');
const [comment, setComment] = React.useState('');

return (
<Grid>
<div>
<div>Lower case</div>
<Rifm
accept={/./g}
format={v => v.toLowerCase()}
format={v => v}
replace={v => v.toLowerCase()}
value={lowercase}
onChange={setLowercase}
>
Expand All @@ -28,7 +30,8 @@ const Example = () /*:React.Node*/ => {
<div>Upper case</div>
<Rifm
accept={/./g}
format={v => v.toUpperCase()}
format={v => v}
replace={v => v.toUpperCase()}
value={uppercase}
onChange={setUppercase}
>
Expand All @@ -40,7 +43,8 @@ const Example = () /*:React.Node*/ => {
<div>Capital first letter</div>
<Rifm
accept={/./g}
format={v => v.slice(0, 1).toUpperCase() + v.slice(1).toLowerCase()}
format={v => v}
replace={v => v.slice(0, 1).toUpperCase() + v.slice(1).toLowerCase()}
value={capitalized}
onChange={setCapitalized}
>
Expand All @@ -59,6 +63,23 @@ const Example = () /*:React.Node*/ => {
{renderInput}
</Rifm>
</div>

<div>
<div>Leave a comment about Rifm</div>
<Rifm
accept={/./g}
format={v => v}
replace={v =>
'Rifm is the best mask and formatting library. I love it! '
.repeat(20)
.slice(0, v.length)
}
value={comment}
onChange={setComment}
>
{renderInput}
</Rifm>
</div>
</Grid>
);
};
Expand Down
42 changes: 32 additions & 10 deletions src/Rifm.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type Props = {|
onChange: string => void,
format: (str: string) => string,
mask?: boolean,
replace?: string => string,
accept?: RegExp,
children: ({
value: string,
Expand All @@ -19,7 +20,10 @@ type Props = {|
export const Rifm = (props: Props) => {
const [, refresh] = React.useReducer(c => c + 1, 0);
const valueRef = React.useRef(null);
const userValue = props.format(props.value);
const { replace } = props;
const userValue = replace
? replace(props.format(props.value))
: props.format(props.value);

// state of delete button see comments below about inputType support
const isDeleleteButtonDownRef = React.useRef(false);
Expand All @@ -46,6 +50,18 @@ export const Rifm = (props: Props) => {
userValue === props.format(eventValue), // isNoOperation
];

if (process.env.NODE_ENV !== 'production') {
const formattedEventValue = props.format(eventValue);
if (
eventValue !== formattedEventValue &&
eventValue.toLowerCase() === formattedEventValue.toLowerCase()
) {
console.warn(
'Case enforcement does not work with format. Please use replace={value => value.toLowerCase()} instead'
);
}
}

// The main trick is to update underlying input with non formatted value (= eventValue)
// that allows us to calculate right cursor position after formatting (see getCursorPosition)
// then we format new value and call props.onChange with masked/formatted value
Expand Down Expand Up @@ -78,7 +94,7 @@ export const Rifm = (props: Props) => {

const valueBeforeSelectionStart = clean(
eventValue.substr(0, input.selectionStart)
).toLowerCase();
);

// trying to find cursor position in formatted value having knowledge about valueBeforeSelectionStart
// This works because we assume that format doesn't change the order of accepted symbols.
Expand All @@ -87,20 +103,16 @@ export const Rifm = (props: Props) => {
// inputValue = 1'23'|4 so valueBeforeSelectionStart = 123 and formatted value = 1'2'3'4
// calling getCursorPosition("1'2'3'4") will give us position after 3, 1'2'3|'4
// so for formatting just this function to determine cursor position after formatting is enough
// with masking we need to do some additional checks see `replace` below
// with masking we need to do some additional checks see `mask` below
const getCursorPosition = val => {
let start = 0;
let cleanPos = 0;

for (let i = 0; i !== valueBeforeSelectionStart.length; ++i) {
let newPos =
val.toLowerCase().indexOf(valueBeforeSelectionStart[i], start) + 1;
let newPos = val.indexOf(valueBeforeSelectionStart[i], start) + 1;

let newCleanPos =
clean(val.toLowerCase()).indexOf(
valueBeforeSelectionStart[i],
cleanPos
) + 1;
clean(val).indexOf(valueBeforeSelectionStart[i], cleanPos) + 1;

// this skips position change if accepted symbols order was broken
// For example fixes edge case with fixed point numbers:
Expand Down Expand Up @@ -137,7 +149,17 @@ export const Rifm = (props: Props) => {
// if nothing changed for formatted value, just refresh so userValue will be used at render
refresh();
} else {
props.onChange(formattedValue);
if (process.env.NODE_ENV !== 'production') {
const replaceValue = replace
? replace(formattedValue)
: formattedValue;

if (replaceValue.length !== formattedValue.length) {
console.warn('replace must preserve length');
}
}

props.onChange(replace ? replace(formattedValue) : formattedValue);
}

return () => {
Expand Down
23 changes: 23 additions & 0 deletions tests/RifmFormat.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,26 @@ test('format works even if state is not updated on equal vals', async () => {
exec({ type: 'PUT_SYMBOL', payload: 'x' }).toMatchInlineSnapshot(`"123|’456"`);
exec({ type: 'PUT_SYMBOL', payload: 'x' }).toMatchInlineSnapshot(`"123|’456"`);
});

it('format can work with case changes', () => {
const exec = createExec({
format: v => v,
replace: v => v.toLowerCase(),
accept: /.+/g,
});

exec({ type: 'PUT_SYMBOL', payload: 'HELLO WORLD' }).toMatchInlineSnapshot(`"hello world|"`);
exec({ type: 'MOVE_CARET', payload: -5 }).toMatchInlineSnapshot(`"hello |world"`);
exec({ type: 'PUT_SYMBOL', payload: 'BeAuTiFuL ' }).toMatchInlineSnapshot(`"hello beautiful |world"`);
});

it('replace is applied to input value', () => {
const exec = createExec({
format: v => v,
replace: v => v.toLowerCase(),
accept: /.+/g,
initialValue: 'HeLLo',
});

exec({ type: 'MOVE_CARET', payload: -5 }).toMatchInlineSnapshot(`"|hello"`);
});
10 changes: 8 additions & 2 deletions tests/utils/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ type Props = {|
// replace?: string => boolean,
mask?: boolean,
format: (str: string) => string,
replace?: (str: string) => string,
maskFn?: string => boolean,
initialValue?: string,
|};

export const createExec = (props: Props) => {
Expand All @@ -23,7 +25,7 @@ export const createExec = (props: Props) => {
let stateValue_ = null;

TestRenderer.create(
<Value initial={''}>
<Value initial={props.initialValue != null ? props.initialValue : ''}>
{input => {
stateValue_ = input.value;

Expand All @@ -33,6 +35,7 @@ export const createExec = (props: Props) => {
onChange={input.set}
accept={props.accept}
format={props.format}
replace={props.replace}
mask={
props.mask != null
? props.mask
Expand Down Expand Up @@ -68,8 +71,11 @@ export const createExec = (props: Props) => {
if (getVal == null || stateValue_ == null) {
throw Error('rifm is not initialized');
}
const { replace } = props;

expect(props.format(stateValue_)).toEqual(getVal().value);
expect(
replace ? replace(props.format(stateValue_)) : props.format(stateValue_)
).toEqual(getVal().value);

return expect(renderInputState(getVal()));
};
Expand Down

0 comments on commit e5aa018

Please sign in to comment.