initial port of the UI kit
This commit is contained in:
147
go/webui/validation.go
Normal file
147
go/webui/validation.go
Normal file
@@ -0,0 +1,147 @@
|
||||
// Port of web/kit/Validation.ts.
|
||||
package webui
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// validationNonDigit matches every non-digit rune (the JS /\D/g).
|
||||
var validationNonDigit = regexp.MustCompile(`\D`)
|
||||
|
||||
// validationDigits strips every non-digit character.
|
||||
func validationDigits(s string) string {
|
||||
return validationNonDigit.ReplaceAllString(s, "")
|
||||
}
|
||||
|
||||
// Validation describes a single form field's validation inputs. Solid's reactive
|
||||
// Accessor<T> values collapse to plain values here: the caller reads its signals
|
||||
// at the call site and passes the current values. FieldBlur is optional (nil
|
||||
// falls back to Field), as is IsValidFunc.
|
||||
type Validation struct {
|
||||
ID string // snake_case identifier that is "touched"
|
||||
Name string // human-readable field name, used in messages
|
||||
Required bool // whether an empty value is an error
|
||||
Touched map[string]bool // which field IDs the user has interacted with
|
||||
Field string // the live field value
|
||||
FieldBlur *string // the value at last blur; nil uses Field
|
||||
IsValidFunc func(string) bool
|
||||
InvalidMsg string // custom "is invalid" message; empty uses a default
|
||||
}
|
||||
|
||||
// CreateValidation computes the validation error message for a field, or "" when
|
||||
// valid. This is the collapsed (non-reactive) form of the Solid createMemo: the
|
||||
// caller re-invokes it whenever the underlying signals change.
|
||||
func CreateValidation(v Validation) string {
|
||||
field := strings.TrimSpace(v.Field)
|
||||
// When no blur value is provided, fall back to the live value so the
|
||||
// "has value but not blurred yet" guard is always false and validation
|
||||
// runs against the live value instead.
|
||||
fieldBlur := field
|
||||
if v.FieldBlur != nil {
|
||||
fieldBlur = strings.TrimSpace(*v.FieldBlur)
|
||||
}
|
||||
|
||||
if !v.Touched[v.ID] || (field != "" && fieldBlur == "") || (v.IsValidFunc != nil && v.IsValidFunc(field)) {
|
||||
return ""
|
||||
}
|
||||
if v.Required && field == "" {
|
||||
return v.Name + " is required"
|
||||
}
|
||||
if v.IsValidFunc != nil && !v.IsValidFunc(fieldBlur) {
|
||||
if v.InvalidMsg != "" {
|
||||
return v.InvalidMsg
|
||||
}
|
||||
return v.Name + " is invalid"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsPhoneNumberValid reports whether phoneNumber has exactly 10 digits.
|
||||
func IsPhoneNumberValid(phoneNumber string) bool {
|
||||
return len(validationDigits(phoneNumber)) == 10
|
||||
}
|
||||
|
||||
var validationEmailRegex = regexp.MustCompile("^[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~](\\.?[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\\.?[a-zA-Z0-9])*\\.[a-zA-Z](-?[a-zA-Z0-9])+$")
|
||||
|
||||
// IsEmailValid reports whether email is a syntactically valid email address.
|
||||
func IsEmailValid(email string) bool {
|
||||
if email == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
emailParts := strings.Split(email, "@")
|
||||
if len(emailParts) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
account := emailParts[0]
|
||||
address := emailParts[1]
|
||||
|
||||
if len(account) > 64 {
|
||||
return false
|
||||
} else if len(address) > 255 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, part := range strings.Split(address, ".") {
|
||||
if len(part) > 63 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return validationEmailRegex.MatchString(email)
|
||||
}
|
||||
|
||||
var validationURLRegex = regexp.MustCompile("[(http(s)?)://(www\\.)?a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)")
|
||||
|
||||
// IsURLValid reports whether url contains a substring that looks like a URL.
|
||||
func IsURLValid(url string) bool {
|
||||
return validationURLRegex.MatchString(url)
|
||||
}
|
||||
|
||||
// IsZipCodeValid reports whether zip has exactly 5 or 9 digits.
|
||||
func IsZipCodeValid(zip string) bool {
|
||||
rawZip := validationDigits(zip)
|
||||
return len(rawZip) == 5 || len(rawZip) == 9
|
||||
}
|
||||
|
||||
// IsTaxIDValid reports whether id has exactly 9 digits.
|
||||
func IsTaxIDValid(id string) bool {
|
||||
return len(validationDigits(id)) == 9
|
||||
}
|
||||
|
||||
// IsAtLeastMinChars reports whether input is at least minLen characters long.
|
||||
func IsAtLeastMinChars(input string, minLen int) bool {
|
||||
return utf8.RuneCountInString(input) >= minLen
|
||||
}
|
||||
|
||||
// IsWithinMaxChars reports whether input is at most maxLen characters long.
|
||||
func IsWithinMaxChars(input string, maxLen int) bool {
|
||||
return utf8.RuneCountInString(input) <= maxLen
|
||||
}
|
||||
|
||||
var validationNameRegex = regexp.MustCompile(`^[\p{L}]*[\p{L} '\-]*[\p{L}]$`)
|
||||
|
||||
// IsNameValid reports whether name consists only of letters, spaces, hyphens,
|
||||
// and apostrophes (and begins/ends with a letter).
|
||||
func IsNameValid(name string) bool {
|
||||
return validationNameRegex.MatchString(name)
|
||||
}
|
||||
|
||||
var validationUsernameRegex = regexp.MustCompile(`^[A-Za-z0-9]*$`)
|
||||
|
||||
// IsUsernameValid reports an error message when the username is invalid (5-50
|
||||
// alphanumeric characters), or "" when valid.
|
||||
func IsUsernameValid(username string) string {
|
||||
if n := utf8.RuneCountInString(username); n < 5 || n > 50 {
|
||||
return "Username must have 5-50 characters"
|
||||
}
|
||||
|
||||
if !validationUsernameRegex.MatchString(username) {
|
||||
return "Username must only contain alphanumeric characters"
|
||||
}
|
||||
|
||||
return "" // Valid
|
||||
}
|
||||
Reference in New Issue
Block a user