From 29422638a2f5aa56f061512717155eb56a736566 Mon Sep 17 00:00:00 2001 From: Simon Boudrias Date: Sat, 7 Dec 2024 12:45:15 -0500 Subject: [PATCH] Feat(input): Add theme.validationFailureMode option to control behavior on validation failure. Fix #374 --- packages/input/README.md | 3 +++ packages/input/input.test.ts | 28 ++++++++++++++++++++++++++++ packages/input/src/index.ts | 22 +++++++++++++++++----- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/packages/input/README.md b/packages/input/README.md index e1444b083..5b2d95ba0 100644 --- a/packages/input/README.md +++ b/packages/input/README.md @@ -86,9 +86,12 @@ type Theme = { error: (text: string) => string; defaultAnswer: (text: string) => string; }; + validationFailureMode: 'keep' | 'clear'; }; ``` +`validationFailureMode` defines the behavior of the prompt when the value submitted is invalid. By default, we'll keep the value allowing the user to edit it. When the theme option is set to `clear`, we'll remove and reset to an empty string. + # License Copyright (c) 2023 Simon Boudrias (twitter: [@vaxilart](https://twitter.com/Vaxilart))
diff --git a/packages/input/input.test.ts b/packages/input/input.test.ts index 2d8616129..d954b0b8d 100644 --- a/packages/input/input.test.ts +++ b/packages/input/input.test.ts @@ -63,6 +63,34 @@ describe('input prompt', () => { await expect(answer).resolves.toEqual('2'); }); + it('can clear value when validation fail', async () => { + const { answer, events, getScreen } = await render(input, { + message: 'Answer 2 ===', + validate: (value: string) => value === '2', + theme: { + validationFailureMode: 'clear', + }, + }); + + expect(getScreen()).toMatchInlineSnapshot(`"? Answer 2 ==="`); + + events.type('1'); + expect(getScreen()).toMatchInlineSnapshot(`"? Answer 2 === 1"`); + + events.keypress('enter'); + await Promise.resolve(); + expect(getScreen()).toMatchInlineSnapshot(` + "? Answer 2 === + > You must provide a valid value" + `); + + events.type('2'); + expect(getScreen()).toMatchInlineSnapshot(`"? Answer 2 === 2"`); + + events.keypress('enter'); + await expect(answer).resolves.toEqual('2'); + }); + it('handle asynchronous validation', async () => { const { answer, events, getScreen } = await render(input, { message: 'Answer 2 ===', diff --git a/packages/input/src/index.ts b/packages/input/src/index.ts index 01fd30751..a39d1a164 100644 --- a/packages/input/src/index.ts +++ b/packages/input/src/index.ts @@ -11,18 +11,26 @@ import { } from '@inquirer/core'; import type { PartialDeep } from '@inquirer/type'; +type InputTheme = { + validationFailureMode: 'keep' | 'clear'; +}; + +const inputTheme: InputTheme = { + validationFailureMode: 'keep', +}; + type InputConfig = { message: string; default?: string; required?: boolean; transformer?: (value: string, { isFinal }: { isFinal: boolean }) => string; validate?: (value: string) => boolean | string | Promise; - theme?: PartialDeep; + theme?: PartialDeep>; }; export default createPrompt((config, done) => { const { required, validate = () => true } = config; - const theme = makeTheme(config.theme); + const theme = makeTheme(inputTheme, config.theme); const [status, setStatus] = useState('idle'); const [defaultValue = '', setDefaultValue] = useState(config.default); const [errorMsg, setError] = useState(); @@ -47,9 +55,13 @@ export default createPrompt((config, done) => { setStatus('done'); done(answer); } else { - // Reset the readline line value to the previous value. On line event, the value - // get cleared, forcing the user to re-enter the value instead of fixing it. - rl.write(value); + if (theme.validationFailureMode === 'clear') { + setValue(''); + } else { + // Reset the readline line value to the previous value. On line event, the value + // get cleared, forcing the user to re-enter the value instead of fixing it. + rl.write(value); + } setError(isValid || 'You must provide a valid value'); setStatus('idle'); }