Skip to content

Instantly share code, notes, and snippets.

@delaneyj
Created March 3, 2026 11:20
Show Gist options
  • Select an option

  • Save delaneyj/fc9c1f7c52abfba25909c243794a2f00 to your computer and use it in GitHub Desktop.

Select an option

Save delaneyj/fc9c1f7c52abfba25909c243794a2f00 to your computer and use it in GitHub Desktop.
package examples
import (
"github.com/go-chi/chi/v5"
"github.com/starfederation/datastar-dev/site/auth"
. "github.com/starfederation/datastar-dev/site/shared"
"github.com/starfederation/datastar-go/datastar"
"net/http"
"regexp"
)
type passwordValidationSignals struct {
Password string `json:"password"`
}
var passwordDigitsOnlyRegex = regexp.MustCompile(`^\d+$`)
var passwordCommonRegex = regexp.MustCompile(`(?i)(password|qwerty|123456)`)
var passwordIncreasingDigitsRegex = regexp.MustCompile(`(?:01234|12345|23456|34567|45678|56789)`)
func setupPasswordValidation(examplesRouter chi.Router) {
examplesRouter.Route("/password_validation", func(passwordValidationRouter chi.Router) {
validatePassword := func(password string) []string {
var errors []string
if len(password) < 10 {
errors = append(errors, "min 10 chars")
}
if passwordDigitsOnlyRegex.MatchString(password) {
errors = append(errors, "digits only not allowed")
}
if passwordCommonRegex.MatchString(password) {
errors = append(errors, "common password")
}
if passwordIncreasingDigitsRegex.MatchString(password) {
errors = append(errors, "contains increasing digit sequence")
}
return errors
}
passwordValidationRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
u := auth.UserFromContext(r.Context())
RenderPage(passwordValidation(u), w, r)
})
passwordValidationRouter.Post("/validate", func(w http.ResponseWriter, r *http.Request) {
signals := &passwordValidationSignals{}
if err := datastar.ReadSignals(r, signals); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
sse := datastar.NewSSE(w, r)
sse.PatchElementTempl(passwordValidationPanel(validatePassword(signals.Password), false))
})
passwordValidationRouter.Post("/signup", func(w http.ResponseWriter, r *http.Request) {
signals := &passwordValidationSignals{}
if err := datastar.ReadSignals(r, signals); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
errors := validatePassword(signals.Password)
sse := datastar.NewSSE(w, r)
if len(errors) > 0 {
sse.PatchElementTempl(passwordValidationPanel(errors, false))
return
}
sse.PatchElementTempl(passwordValidationPanel(nil, true))
})
})
}
templ passwordValidation(u *auth.User) {
@page(u, "password_validation") {
@Demo() {
@passwordValidationPanel(nil, false)
}
@Heading("Explanation")
<p>
This version follows the Tao model: input is bound to a signal, validation runs only on
the server, and the server patches the UI with either an error state or success state.
</p>
<p>
The client only triggers actions. It does not compute password rules in attribute
expressions.
</p>
@Heading("HTML")
@HtmlCodeHighlight(`
<div id="demo">
<label for="password_input">Password</label>
<input
id="password_input"
type="password"
data-bind:password
data-on:input__debounce.200ms="@post('/examples/password_validation/validate')"
/>
<button data-on:click="@post('/examples/password_validation/signup')">
Sign up
</button>
</div>
`)
}
}
templ passwordValidationPanel(errors []string, submitted bool) {
<div id="demo">
<div>
<p>Create account</p>
<div>
<label for="password_input">Password</label>
<input
id="password_input"
type="password"
data-bind:password
data-on:input__debounce.200ms="@post('/examples/password_validation/validate')"
/>
</div>
<div>
if len(errors) > 0 {
<ul>
for _, err := range errors {
<li>{ err }</li>
}
</ul>
} else if submitted {
<p>Account created.</p>
} else {
<p>Use at least 10 characters.</p>
}
</div>
<div>
<button data-on:click="@post('/examples/password_validation/signup')">
Sign up
</button>
</div>
</div>
</div>
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment