Skip to content

Commit

Permalink
feat(textinput): make validation customizable
Browse files Browse the repository at this point in the history
This PR builds upon the excellent work in charmbracelet#167 and charmbracelet#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
  • Loading branch information
GabrielNagy committed Aug 18, 2022
1 parent 649f78e commit 801ac44
Showing 1 changed file with 31 additions and 9 deletions.
40 changes: 31 additions & 9 deletions textinput/textinput.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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()
Expand Down Expand Up @@ -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))
}
}

Expand Down

0 comments on commit 801ac44

Please sign in to comment.