Skip to content

Commit b0c7209

Browse files
committed
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 a breaking change to the ValidateFunc by returning an additonal bool that indicates whether or not input should 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
1 parent eda8912 commit b0c7209

File tree

1 file changed

+25
-15
lines changed

1 file changed

+25
-15
lines changed

textinput/textinput.go

+25-15
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ const (
3535
EchoNone
3636
)
3737

38-
// ValidateFunc is a function that returns an error if the input is invalid.
39-
type ValidateFunc func(string) error
38+
// ValidateFunc is a function that returns an error if the input is invalid and
39+
// a boolean indicating whether text input should be blocked.
40+
type ValidateFunc func(string) (bool, error)
4041

4142
// KeyMap is the key bindings for different actions within the textinput.
4243
type KeyMap struct {
@@ -180,19 +181,17 @@ func (m *Model) SetValue(s string) {
180181
// Clean up any special characters in the input provided by the
181182
// caller. This avoids bugs due to e.g. tab characters and whatnot.
182183
runes := m.san().Sanitize([]rune(s))
183-
m.setValueInternal(runes)
184+
blockInput, err := m.validateIfDefined(string(runes))
185+
m.setValueInternal(runes, err, blockInput)
184186
}
185187

186-
func (m *Model) setValueInternal(runes []rune) {
187-
if m.Validate != nil {
188-
if err := m.Validate(string(runes)); err != nil {
189-
m.Err = err
190-
return
191-
}
188+
func (m *Model) setValueInternal(runes []rune, err error, blockInput bool) {
189+
m.Err = err
190+
if blockInput {
191+
return
192192
}
193193

194194
empty := len(m.value) == 0
195-
m.Err = nil
196195

197196
if m.CharLimit > 0 && len(runes) > m.CharLimit {
198197
m.value = runes[:m.CharLimit]
@@ -323,9 +322,10 @@ func (m *Model) insertRunesFromUserInput(v []rune) {
323322

324323
// Put it all back together
325324
value := append(head, tail...)
326-
m.setValueInternal(value)
325+
blockInput, inputErr := m.validateIfDefined(string(value))
326+
m.setValueInternal(value, inputErr, blockInput)
327327

328-
if m.Err != nil {
328+
if blockInput {
329329
m.pos = oldPos
330330
}
331331
}
@@ -378,6 +378,7 @@ func (m *Model) handleOverflow() {
378378
// deleteBeforeCursor deletes all text before the cursor.
379379
func (m *Model) deleteBeforeCursor() {
380380
m.value = m.value[m.pos:]
381+
_, m.Err = m.validateIfDefined(string(m.value))
381382
m.offset = 0
382383
m.SetCursor(0)
383384
}
@@ -387,6 +388,7 @@ func (m *Model) deleteBeforeCursor() {
387388
// masked input.
388389
func (m *Model) deleteAfterCursor() {
389390
m.value = m.value[:m.pos]
391+
_, m.Err = m.validateIfDefined(string(m.value))
390392
m.SetCursor(len(m.value))
391393
}
392394

@@ -432,6 +434,7 @@ func (m *Model) deleteWordBackward() {
432434
} else {
433435
m.value = append(m.value[:m.pos], m.value[oldPos:]...)
434436
}
437+
_, m.Err = m.validateIfDefined(string(m.value))
435438
}
436439

437440
// deleteWordForward deletes the word right to the cursor. If input is masked
@@ -471,6 +474,7 @@ func (m *Model) deleteWordForward() {
471474
} else {
472475
m.value = append(m.value[:oldPos], m.value[m.pos:]...)
473476
}
477+
_, m.Err = m.validateIfDefined(string(m.value))
474478

475479
m.SetCursor(oldPos)
476480
}
@@ -575,12 +579,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
575579
case tea.KeyMsg:
576580
switch {
577581
case key.Matches(msg, m.KeyMap.DeleteWordBackward):
578-
m.Err = nil
579582
m.deleteWordBackward()
580583
case key.Matches(msg, m.KeyMap.DeleteCharacterBackward):
581584
m.Err = nil
582585
if len(m.value) > 0 {
583586
m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
587+
_, m.Err = m.validateIfDefined(string(m.value))
584588
if m.pos > 0 {
585589
m.SetCursor(m.pos - 1)
586590
}
@@ -597,13 +601,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
597601
if m.pos < len(m.value) {
598602
m.SetCursor(m.pos + 1)
599603
}
600-
case key.Matches(msg, m.KeyMap.DeleteWordBackward):
601-
m.deleteWordBackward()
602604
case key.Matches(msg, m.KeyMap.LineStart):
603605
m.CursorStart()
604606
case key.Matches(msg, m.KeyMap.DeleteCharacterForward):
605607
if len(m.value) > 0 && m.pos < len(m.value) {
606608
m.value = append(m.value[:m.pos], m.value[m.pos+1:]...)
609+
_, m.Err = m.validateIfDefined(string(m.value))
607610
}
608611
case key.Matches(msg, m.KeyMap.LineEnd):
609612
m.CursorEnd()
@@ -859,3 +862,10 @@ func (m *Model) previousSuggestion() {
859862
m.currentSuggestionIndex = len(m.matchedSuggestions) - 1
860863
}
861864
}
865+
866+
func (m Model) validateIfDefined(v string) (bool, error) {
867+
if m.Validate != nil {
868+
return m.Validate(v)
869+
}
870+
return false, nil
871+
}

0 commit comments

Comments
 (0)