From 801ac4493f36d38ad132520184e01e1f88682527 Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Tue, 5 Jul 2022 17:00:23 +0300 Subject: [PATCH] feat(textinput): make validation customizable This PR builds upon the excellent work in #167 and #114 and adds a bit more customizability to the feature. Currently, the validation API will completely block text input if the Validate function returns an error. This commit makes this behavior configurable by introducing a ValidateAction which decides how to proceed in case a validation error happens. To preserve backwards compatibility, the default behavior remains unchanged. If ValidateAction is set to AllowInput, the text input error will still be set if needed, but actual input will not be blocked. This is helpful for cases where the user is requested to type an existing system path, and the Validate function keeps asserting the existence of the path. With the current implementation such a validation is not possible. For example: > / Err: nil > /t Err: /t: No such file or directory > /tm Err: /tm: No such file or directory > /tmp Err: nil --- textinput/textinput.go | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/textinput/textinput.go b/textinput/textinput.go index 9692ed7c..98af83d4 100644 --- a/textinput/textinput.go +++ b/textinput/textinput.go @@ -81,6 +81,15 @@ const ( CursorHide ) +// ValidateAction describes the behavior on input validation. +type ValidateAction int + +// Available validate actions. +const ( + BlockInput ValidateAction = iota + AllowInput +) + // String returns a the cursor mode in a human-readable format. This method is // provisional and for informational purposes only. func (c CursorMode) String() string { @@ -159,6 +168,10 @@ type Model struct { // error returned by the function. If the function is not defined, all // input is considered valid. Validate ValidateFunc + + // ValidateAction determines what action to take if the last input was + // invalid. + ValidateAction ValidateAction } // New creates a new model with default settings. @@ -191,14 +204,12 @@ var NewModel = New // SetValue sets the value of the text input. func (m *Model) SetValue(s string) { if m.Validate != nil { - if err := m.Validate(s); err != nil { - m.Err = err + m.Err = m.Validate(s) + if m.Err != nil && m.ValidateAction == BlockInput { return } } - m.Err = nil - runes := []rune(s) if m.CharLimit > 0 && len(runes) > m.CharLimit { m.value = runes[:m.CharLimit] @@ -362,7 +373,9 @@ func (m *Model) handlePaste(v string) bool { value := append(head, tail...) m.SetValue(string(value)) - if m.Err != nil { + // If validation failed and we're blocking input, revert to the previous + // cursor position + if m.Validate != nil && m.ValidateAction == BlockInput && m.Err != nil { m.pos = oldPos } @@ -612,8 +625,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { case tea.KeyMsg: switch msg.Type { case tea.KeyBackspace, tea.KeyCtrlH: // delete character before cursor - m.Err = nil - if msg.Alt { resetBlink = m.deleteWordLeft() } else { @@ -624,6 +635,13 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } } } + + if m.Validate != nil { + m.Err = nil + if m.ValidateAction == AllowInput { + m.Err = m.Validate(string(m.value)) + } + } case tea.KeyLeft, tea.KeyCtrlB: if msg.Alt { // alt+left arrow, back one word resetBlink = m.wordLeft() @@ -680,9 +698,13 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { copy(value, m.value) value = append(value[:m.pos], append(runes, value[m.pos:]...)...) m.SetValue(string(value)) - if m.Err == nil { - resetBlink = m.setCursor(m.pos + len(runes)) + + // If validation failed and we don't allow input, avoid + // resetting the cursor blink. + if m.Validate != nil && m.ValidateAction == BlockInput && m.Err != nil { + break } + resetBlink = m.setCursor(m.pos + len(runes)) } }