PubSub

This example uses hlivekit.PubSub to decouple a form's pieces from each other. Two input components (name and email) each validate themselves and publish input_valid or input_invalid messages, an error-summary component subscribes to those messages to show a combined list of problems, and the form publishes a form_validate message on submit to make every input re-check itself before allowing form_submit.

None of the components hold a reference to any other component — they only know about the shared PubSub instance and the message names they care about.

Live Demo

Source

package examples import ( "context" "regexp" l "github.com/SamHennessy/hlive" . "github.com/SamHennessy/hlive/hhtml" "github.com/SamHennessy/hlive/hlivekit" ) const ( pstInputInvalid = "input_invalid" pstInputValid = "input_valid" pstFormValidate = "form_validate" pstFormInvalid = "form_invalid" pstFormSubmit = "form_submit" pstFormSubmitted = "form_summited" ) // Source: https://golangcode.com/validate-an-email-address/ var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") type inputValue struct { name string value *l.NodeBox[string] error *l.NodeBox[string] } func newInputValue(name string) inputValue { return inputValue{ name: name, value: l.Box(""), error: l.Box(""), } } // PubSubDemo is a live, interactive instance of the PubSub example, served // for the "Live Demo" iframe. func PubSubDemo() *l.Page { pubSub := hlivekit.NewPubSub() page := l.NewPage() page.DOM().HTML().Add(hlivekit.InstallPubSub(pubSub)) page.DOM().Title().Add("PubSub Example") page.DOM().Head().Add(Link(Rel("stylesheet"), Href("https://cdn.simplecss.org/simple.min.css"))) page.DOM().Body().Add( Header( H1("PubSub"), P("Use the PubSub system to allow for decoupled components."), ), Main( newErrorMessages(), newUserForm( newInputName(), newInputEmail(), Button("Submit"), P("*Required"), ), newFormOutput(), ), ) return page } // // Components // // Error messages func newErrorMessages() *errorMessages { c := &errorMessages{ // Forced to a Component because errorMessages embeds *l.Component. Component: l.C("div"), inputMap: map[string]inputValue{}, errMessage: l.Box(""), } c.initDOM() return c } type errorMessages struct { *l.Component pubSub *hlivekit.PubSub errMessage *l.NodeBox[string] inputs []string inputMap map[string]inputValue } func (c *errorMessages) PubSubMount(_ context.Context, pubSub *hlivekit.PubSub) { c.pubSub = pubSub // Track input updates pubSub.Subscribe(hlivekit.NewSub(c.onInput), pstInputInvalid, pstInputValid) // Reset pubSub.Subscribe(hlivekit.NewSub(c.onFormValidate), pstFormValidate) } func (c *errorMessages) onFormValidate(_ hlivekit.QueueMessage) { c.inputs = nil c.inputMap = map[string]inputValue{} c.errMessage.Set("") c.Add(l.Attrs{"display": "none"}) } func (c *errorMessages) onInput(message hlivekit.QueueMessage) { input, ok := message.Value.(inputValue) if !ok { return } _, exists := c.inputMap[input.name] if !exists { c.inputs = append(c.inputs, input.name) } c.inputMap[input.name] = input c.formatErrMessage() } func (c *errorMessages) initDOM() { c.Add(l.ClassBool{"card": true}, l.Style{"display": "none"}, H4("Errors"), Hr(), P(c.errMessage), ) } func (c *errorMessages) formatErrMessage() { c.errMessage.Set("") for i := 0; i < len(c.inputs); i++ { if c.inputMap[c.inputs[i]].error.Get() != "" { c.errMessage.Lock(func(v string) string { return v + c.inputMap[c.inputs[i]].error.Get() + " " }) } } if c.errMessage.Get() == "" { c.Add(l.Style{"display": "none"}) } else { c.Add(l.StyleOff{"display"}) } } // User form func newUserForm(nodes ...any) *userForm { c := &userForm{ // Forced to a Component because userForm embeds *l.Component. Component: l.C("form", nodes...), } c.initDOM() return c } type userForm struct { *l.Component isInvalid bool pubSub *hlivekit.PubSub } func (c *userForm) PubSubMount(_ context.Context, pubSub *hlivekit.PubSub) { c.pubSub = pubSub // If any errors, then we can't submit pubSub.Subscribe(hlivekit.NewSub(func(message hlivekit.QueueMessage) { c.isInvalid = true }), pstInputInvalid) } func (c *userForm) initDOM() { // Revalidate the form, if invalid then submit c.Add(l.PreventDefault(), l.On("submit", c.onSubmit)) } func (c *userForm) onSubmit(_ context.Context, _ l.Event) { c.isInvalid = false // Revalidate form c.pubSub.Publish(pstFormValidate, nil) if c.isInvalid { c.pubSub.Publish(pstFormInvalid, nil) return } c.pubSub.Publish(pstFormSubmit, nil) } // Input, name func newInputName() *inputName { c := &inputName{ // Forced to a Component because inputName embeds *l.Component. Component: l.NewComponent("span"), input: newInputValue("name"), } c.initDOM() return c } type inputName struct { *l.Component pubSub *hlivekit.PubSub input inputValue firstChange bool } func (c *inputName) PubSubMount(_ context.Context, pubSub *hlivekit.PubSub) { c.pubSub = pubSub c.pubSub.Subscribe(hlivekit.NewSub(c.onFormValidate), pstFormValidate) } func (c *inputName) initDOM() { c.Add( Label("Name*"), Input( Name("name"), Placeholder("Your name"), l.AttrsLockBox{"value": c.input.value.LockBox}, l.On("input", c.onInput), l.On("change", c.onChange), ), P(l.Style{"color": "red"}, c.input.error), ) } func (c *inputName) onFormValidate(_ hlivekit.QueueMessage) { c.firstChange = true c.validate() } func (c *inputName) onChange(ctx context.Context, e l.Event) { c.firstChange = true c.onInput(ctx, e) } func (c *inputName) onInput(_ context.Context, e l.Event) { c.input.value.Set(e.Value) if c.firstChange { c.validate() } } func (c *inputName) validate() { c.input.error.Set("") if c.input.value.Get() == "" { c.input.error.Set("Name is required.") c.pubSub.Publish(pstInputInvalid, c.input) return } if len([]rune(c.input.value.Get())) < 2 { c.input.error.Set("Name is too short.") c.pubSub.Publish(pstInputInvalid, c.input) return } c.pubSub.Publish(pstInputValid, c.input) } // Input, email func newInputEmail() *inputEmail { c := &inputEmail{ // Forced to a Component because inputEmail embeds *l.Component. Component: l.NewComponent("span"), input: newInputValue("email"), } c.initDOM() return c } type inputEmail struct { *l.Component pubSub *hlivekit.PubSub input inputValue firstChange bool } func (c *inputEmail) PubSubMount(_ context.Context, pubSub *hlivekit.PubSub) { c.pubSub = pubSub c.pubSub.Subscribe(hlivekit.NewSub(c.onFormValidate), pstFormValidate) } func (c *inputEmail) initDOM() { c.Add( Label("Email"), Input( Name("email"), Placeholder("Your email address"), l.AttrsLockBox{"value": c.input.value.LockBox}, l.On("input", c.onInput), l.On("change", c.onChange), ), P(l.Style{"color": "red"}, c.input.error), ) } func (c *inputEmail) onFormValidate(_ hlivekit.QueueMessage) { c.firstChange = true c.validate() } func (c *inputEmail) onChange(ctx context.Context, e l.Event) { c.firstChange = true c.onInput(ctx, e) } func (c *inputEmail) onInput(_ context.Context, e l.Event) { c.input.value.Set(e.Value) if c.firstChange { c.validate() } } func (c *inputEmail) validate() { c.input.error.Set("") if len(c.input.value.Get()) != 0 && !emailRegex.MatchString(c.input.value.Get()) { c.input.error.Set("Email address not valid.") c.pubSub.Publish(pstInputInvalid, c.input) return } c.pubSub.Publish(pstInputValid, c.input) } // Form output func newFormOutput() *formOutput { c := &formOutput{ // Forced to a Component because formOutput embeds *l.Component. Component: l.C("table"), inputs: map[string]inputValue{}, list: hlivekit.List("tbody"), } c.Add( l.Style{"display": "none"}, Thead( Tr( Th("Key"), Th("Value"), ), ), c.list, ) return c } type formOutput struct { *l.Component list *hlivekit.ComponentList pubSub *hlivekit.PubSub inputs map[string]inputValue } func (c *formOutput) PubSubMount(_ context.Context, pubSub *hlivekit.PubSub) { c.pubSub = pubSub c.pubSub.Subscribe(hlivekit.NewSub(c.onValidInput), pstInputValid) c.pubSub.Subscribe(hlivekit.NewSub(c.onSubmitForm), pstFormSubmit) } func (c *formOutput) onValidInput(item hlivekit.QueueMessage) { if input, ok := item.Value.(inputValue); ok { c.inputs[input.name] = input } } func (c *formOutput) onSubmitForm(_ hlivekit.QueueMessage) { c.Add(l.StyleOff{"display"}) c.list.RemoveAllItems() for key, input := range c.inputs { c.list.Add( l.CM("tr", Td(key), Td(input.value), ), ) } c.pubSub.Publish(pstFormSubmitted, nil) }