Compare commits
1 Commits
5362614c3e
...
v0.12.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10b4838f8d |
23
context.go
23
context.go
@@ -32,6 +32,7 @@ type Context struct {
|
|||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
ctxDisposedChan chan struct{}
|
ctxDisposedChan chan struct{}
|
||||||
reqCtx context.Context
|
reqCtx context.Context
|
||||||
|
fields []*Field
|
||||||
subscriptions []Subscription
|
subscriptions []Subscription
|
||||||
subsMu sync.Mutex
|
subsMu sync.Mutex
|
||||||
disposeOnce sync.Once
|
disposeOnce sync.Once
|
||||||
@@ -482,16 +483,28 @@ func (c *Context) unsubscribeAll() {
|
|||||||
|
|
||||||
// Field creates a signal with validation rules attached.
|
// Field creates a signal with validation rules attached.
|
||||||
// The initial value seeds both the signal and the reset target.
|
// The initial value seeds both the signal and the reset target.
|
||||||
|
// The field is tracked on the context so ValidateAll/ResetFields
|
||||||
|
// can operate on all fields by default.
|
||||||
func (c *Context) Field(initial any, rules ...Rule) *Field {
|
func (c *Context) Field(initial any, rules ...Rule) *Field {
|
||||||
return &Field{
|
f := &Field{
|
||||||
signal: c.Signal(initial),
|
signal: c.Signal(initial),
|
||||||
rules: rules,
|
rules: rules,
|
||||||
initialVal: initial,
|
initialVal: initial,
|
||||||
}
|
}
|
||||||
|
target := c
|
||||||
|
if c.isComponent() {
|
||||||
|
target = c.parentPageCtx
|
||||||
|
}
|
||||||
|
target.fields = append(target.fields, f)
|
||||||
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAll runs Validate on every field, returning true only if all pass.
|
// ValidateAll runs Validate on each field, returning true only if all pass.
|
||||||
|
// With no arguments it validates every field tracked on this context.
|
||||||
func (c *Context) ValidateAll(fields ...*Field) bool {
|
func (c *Context) ValidateAll(fields ...*Field) bool {
|
||||||
|
if len(fields) == 0 {
|
||||||
|
fields = c.fields
|
||||||
|
}
|
||||||
ok := true
|
ok := true
|
||||||
for _, f := range fields {
|
for _, f := range fields {
|
||||||
if !f.Validate() {
|
if !f.Validate() {
|
||||||
@@ -501,8 +514,12 @@ func (c *Context) ValidateAll(fields ...*Field) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetFields resets every field to its initial value and clears errors.
|
// ResetFields resets each field to its initial value and clears errors.
|
||||||
|
// With no arguments it resets every field tracked on this context.
|
||||||
func (c *Context) ResetFields(fields ...*Field) {
|
func (c *Context) ResetFields(fields ...*Field) {
|
||||||
|
if len(fields) == 0 {
|
||||||
|
fields = c.fields
|
||||||
|
}
|
||||||
for _, f := range fields {
|
for _, f := range fields {
|
||||||
f.Reset()
|
f.Reset()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,27 +96,22 @@ func TestFieldReset(t *testing.T) {
|
|||||||
|
|
||||||
func TestValidateAll(t *testing.T) {
|
func TestValidateAll(t *testing.T) {
|
||||||
v := New()
|
v := New()
|
||||||
var username, email *Field
|
|
||||||
v.Page("/", func(c *Context) {
|
v.Page("/", func(c *Context) {
|
||||||
username = c.Field("", Required(), MinLen(3))
|
c.Field("", Required(), MinLen(3))
|
||||||
email = c.Field("", Required(), Email())
|
c.Field("", Required(), Email())
|
||||||
c.View(func() h.H { return h.Div() })
|
c.View(func() h.H { return h.Div() })
|
||||||
|
|
||||||
|
// both empty → both fail
|
||||||
|
assert.False(t, c.ValidateAll())
|
||||||
})
|
})
|
||||||
|
|
||||||
// both empty → both fail
|
|
||||||
assert.False(t, false) // sanity
|
|
||||||
ok := username.Validate() && email.Validate()
|
|
||||||
assert.False(t, ok)
|
|
||||||
|
|
||||||
// simulate ValidateAll via context
|
|
||||||
v2 := New()
|
v2 := New()
|
||||||
var u2, e2 *Field
|
|
||||||
v2.Page("/", func(c *Context) {
|
v2.Page("/", func(c *Context) {
|
||||||
u2 = c.Field("joe", Required(), MinLen(3))
|
c.Field("joe", Required(), MinLen(3))
|
||||||
e2 = c.Field("joe@x.com", Required(), Email())
|
c.Field("joe@x.com", Required(), Email())
|
||||||
c.View(func() h.H { return h.Div() })
|
c.View(func() h.H { return h.Div() })
|
||||||
|
|
||||||
assert.True(t, c.ValidateAll(u2, e2))
|
assert.True(t, c.ValidateAll())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,14 +122,30 @@ func TestValidateAllPartialFailure(t *testing.T) {
|
|||||||
bad := c.Field("", Required())
|
bad := c.Field("", Required())
|
||||||
c.View(func() h.H { return h.Div() })
|
c.View(func() h.H { return h.Div() })
|
||||||
|
|
||||||
// ValidateAll must run ALL fields even if first passes
|
ok := c.ValidateAll()
|
||||||
ok := c.ValidateAll(good, bad)
|
|
||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
assert.False(t, good.HasError())
|
assert.False(t, good.HasError())
|
||||||
assert.True(t, bad.HasError())
|
assert.True(t, bad.HasError())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateAllSelectiveArgs(t *testing.T) {
|
||||||
|
v := New()
|
||||||
|
v.Page("/", func(c *Context) {
|
||||||
|
a := c.Field("", Required())
|
||||||
|
b := c.Field("ok", Required())
|
||||||
|
cField := c.Field("", Required())
|
||||||
|
c.View(func() h.H { return h.Div() })
|
||||||
|
|
||||||
|
// only validate a and b — cField should be untouched
|
||||||
|
ok := c.ValidateAll(a, b)
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.True(t, a.HasError())
|
||||||
|
assert.False(t, b.HasError())
|
||||||
|
assert.False(t, cField.HasError(), "unselected field should not be validated")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestResetFields(t *testing.T) {
|
func TestResetFields(t *testing.T) {
|
||||||
v := New()
|
v := New()
|
||||||
v.Page("/", func(c *Context) {
|
v.Page("/", func(c *Context) {
|
||||||
@@ -146,13 +157,30 @@ func TestResetFields(t *testing.T) {
|
|||||||
b.SetValue("changed-b")
|
b.SetValue("changed-b")
|
||||||
a.AddError("err")
|
a.AddError("err")
|
||||||
|
|
||||||
c.ResetFields(a, b)
|
c.ResetFields()
|
||||||
assert.Equal(t, "a", a.String())
|
assert.Equal(t, "a", a.String())
|
||||||
assert.Equal(t, "b", b.String())
|
assert.Equal(t, "b", b.String())
|
||||||
assert.False(t, a.HasError())
|
assert.False(t, a.HasError())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResetFieldsSelectiveArgs(t *testing.T) {
|
||||||
|
v := New()
|
||||||
|
v.Page("/", func(c *Context) {
|
||||||
|
a := c.Field("a")
|
||||||
|
b := c.Field("b")
|
||||||
|
c.View(func() h.H { return h.Div() })
|
||||||
|
|
||||||
|
a.SetValue("changed-a")
|
||||||
|
b.SetValue("changed-b")
|
||||||
|
|
||||||
|
// only reset a
|
||||||
|
c.ResetFields(a)
|
||||||
|
assert.Equal(t, "a", a.String())
|
||||||
|
assert.Equal(t, "changed-b", b.String(), "unselected field should not be reset")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestFieldValidateClearsPreviousErrors(t *testing.T) {
|
func TestFieldValidateClearsPreviousErrors(t *testing.T) {
|
||||||
f := newTestField("", Required())
|
f := newTestField("", Required())
|
||||||
f.Validate()
|
f.Validate()
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ func main() {
|
|||||||
email := c.Field("", via.Required(), via.Email())
|
email := c.Field("", via.Required(), via.Email())
|
||||||
age := c.Field("", via.Required(), via.Min(13), via.Max(120))
|
age := c.Field("", via.Required(), via.Min(13), via.Max(120))
|
||||||
// Optional field — only validated when non-empty
|
// Optional field — only validated when non-empty
|
||||||
website := c.Field("", via.Custom(func(val string) error { return nil }), via.Pattern(`^$|^https?://\S+$`, "Must be a valid URL"))
|
website := c.Field("", via.Pattern(`^$|^https?://\S+$`, "Must be a valid URL"))
|
||||||
|
|
||||||
var success string
|
var success string
|
||||||
|
|
||||||
signup := c.Action(func() {
|
signup := c.Action(func() {
|
||||||
success = ""
|
success = ""
|
||||||
if !c.ValidateAll(username, email, age, website) {
|
if !c.ValidateAll() {
|
||||||
c.Sync()
|
c.Sync()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -43,13 +43,13 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
success = "Account created for " + username.String() + "!"
|
success = "Account created for " + username.String() + "!"
|
||||||
c.ResetFields(username, email, age, website)
|
c.ResetFields()
|
||||||
c.Sync()
|
c.Sync()
|
||||||
})
|
})
|
||||||
|
|
||||||
reset := c.Action(func() {
|
reset := c.Action(func() {
|
||||||
success = ""
|
success = ""
|
||||||
c.ResetFields(username, email, age, website)
|
c.ResetFields()
|
||||||
c.Sync()
|
c.Sync()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
24
rule.go
24
rule.go
@@ -1,10 +1,12 @@
|
|||||||
package via
|
package via
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rule defines a single validation check for a Field.
|
// Rule defines a single validation check for a Field.
|
||||||
@@ -20,7 +22,7 @@ func Required(msg ...string) Rule {
|
|||||||
}
|
}
|
||||||
return Rule{func(val string) error {
|
return Rule{func(val string) error {
|
||||||
if strings.TrimSpace(val) == "" {
|
if strings.TrimSpace(val) == "" {
|
||||||
return fmt.Errorf("%s", m)
|
return errors.New(m)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}}
|
}}
|
||||||
@@ -33,8 +35,8 @@ func MinLen(n int, msg ...string) Rule {
|
|||||||
m = msg[0]
|
m = msg[0]
|
||||||
}
|
}
|
||||||
return Rule{func(val string) error {
|
return Rule{func(val string) error {
|
||||||
if len(val) < n {
|
if utf8.RuneCountInString(val) < n {
|
||||||
return fmt.Errorf("%s", m)
|
return errors.New(m)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}}
|
}}
|
||||||
@@ -47,8 +49,8 @@ func MaxLen(n int, msg ...string) Rule {
|
|||||||
m = msg[0]
|
m = msg[0]
|
||||||
}
|
}
|
||||||
return Rule{func(val string) error {
|
return Rule{func(val string) error {
|
||||||
if len(val) > n {
|
if utf8.RuneCountInString(val) > n {
|
||||||
return fmt.Errorf("%s", m)
|
return errors.New(m)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}}
|
}}
|
||||||
@@ -63,10 +65,10 @@ func Min(n int, msg ...string) Rule {
|
|||||||
return Rule{func(val string) error {
|
return Rule{func(val string) error {
|
||||||
v, err := strconv.Atoi(val)
|
v, err := strconv.Atoi(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s", m)
|
return errors.New("Must be a valid number")
|
||||||
}
|
}
|
||||||
if v < n {
|
if v < n {
|
||||||
return fmt.Errorf("%s", m)
|
return errors.New(m)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}}
|
}}
|
||||||
@@ -81,10 +83,10 @@ func Max(n int, msg ...string) Rule {
|
|||||||
return Rule{func(val string) error {
|
return Rule{func(val string) error {
|
||||||
v, err := strconv.Atoi(val)
|
v, err := strconv.Atoi(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s", m)
|
return errors.New("Must be a valid number")
|
||||||
}
|
}
|
||||||
if v > n {
|
if v > n {
|
||||||
return fmt.Errorf("%s", m)
|
return errors.New(m)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}}
|
}}
|
||||||
@@ -99,7 +101,7 @@ func Pattern(re string, msg ...string) Rule {
|
|||||||
compiled := regexp.MustCompile(re)
|
compiled := regexp.MustCompile(re)
|
||||||
return Rule{func(val string) error {
|
return Rule{func(val string) error {
|
||||||
if !compiled.MatchString(val) {
|
if !compiled.MatchString(val) {
|
||||||
return fmt.Errorf("%s", m)
|
return errors.New(m)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}}
|
}}
|
||||||
@@ -115,7 +117,7 @@ func Email(msg ...string) Rule {
|
|||||||
}
|
}
|
||||||
return Rule{func(val string) error {
|
return Rule{func(val string) error {
|
||||||
if !emailRegexp.MatchString(val) {
|
if !emailRegexp.MatchString(val) {
|
||||||
return fmt.Errorf("%s", m)
|
return errors.New(m)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user