Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't clear HTML <input> value #447

Closed
pojntfx opened this issue Sep 14, 2020 · 7 comments
Closed

Can't clear HTML <input> value #447

pojntfx opened this issue Sep 14, 2020 · 7 comments

Comments

@pojntfx
Copy link

pojntfx commented Sep 14, 2020

Hi!

First of all, thanks for this library! I've had a lot of fun using it so far and I plan to use it for some serious frontends in the future. Sadly, I can't seem to be able to set the value of a HTML <input> element, even when calling Update() afterwards:

package components

import (
	"context"
	"log"

	"github.com/maxence-charriere/go-app/v7/pkg/app"
	proto "github.com/pojntfx/go-app-grpc-chat-frontend-web/pkg/proto/generated"
)

type App struct {
	app.Compo
	client            proto.ChatServiceClient
	receivedMessages  []proto.ChatMessage
	newMessageContent string
}

func NewApp(client proto.ChatServiceClient) *App {
	return &App{client: client, receivedMessages: []proto.ChatMessage{}, newMessageContent: ""}
}

func (c *App) HandleMessageSend(ctx app.Context, e app.Event) {
	log.Println("Sending message with content", c.newMessageContent)

	message := proto.ChatMessage{Content: c.newMessageContent}
	outMessage, err := c.client.CreateMessage(context.TODO(), &message)
	if err != nil {
		log.Println("could not send message", err)
	}

	c.receivedMessages = append(c.receivedMessages, *outMessage)

	log.Println("Received from server message", outMessage)

	c.newMessageContent = ""

	c.Update()
}

func (c *App) Render() app.UI {
	return app.Main().Body(
		app.Div().Class("container").Body(
			app.H1().Class("mt-3").Body(
				app.Text("go-app gRPC Chat Frontend"),
			),
			app.U().Class("list-group mt-3").Body(
				app.Range(c.receivedMessages).Slice(func(i int) app.UI {
					return app.Li().Class("list-group-item").Body(
						app.Text(c.receivedMessages[i].GetContent()),
					)
				}),
			),
			app.Div().Class("input-group mt-3").Body(
				app.Input().Type("text").Class("form-control").Value(c.newMessageContent).Placeholder("Message content").OnInput(func(ctx app.Context, e app.Event) {
					c.newMessageContent = e.Get("target").Get("value").String()

					c.Update()
				}).OnChange(c.HandleMessageSend),
				app.Div().Class("input-group-append").Body(
					app.Button().Class("btn btn-primary").Body(app.Text("Send Message")).OnClick(c.HandleMessageSend),
				),
			),
		),
	)
}

I'd expect to the be able to clear the input in the OnChange handler, but nothing happens. newMessageContent gets set, but the input still shows that last value. Any ideas?

@maxence-charriere
Copy link
Owner

Look like you are setting back the value by the previous content

c.newMessageContent = e.Get("target").Get("value").String()

@pojntfx
Copy link
Author

pojntfx commented Sep 18, 2020

@maxence-charriere Yeah sure, in the OnInput handler; in the OnChange handler (c.HandleMessageSend) however I'm clearing the value (when I "send a message"). No matter what I do, even if I call c.HandleMessageSend from the button, the text field isn't being cleared 🤷

@pojntfx
Copy link
Author

pojntfx commented Sep 18, 2020

Just in case you're still looking into this; I've found a solution after a few hours of troubleshooting. I'll post it here; the gist of it is that there is a difference between the HTML value (for a text input) and the DOM value - noticed this when working with checkboxes. Using component-local state I currently create the equivalent of a React ref and update the DOM attributes manually on every render ;)

@pojntfx
Copy link
Author

pojntfx commented Sep 18, 2020

Code repo with the implementation: https://github.com/pojntfx/liwasc-frontend-web

The implementation (syncing the checked DOM property with the go-app state):

package components

import (
	"github.com/maxence-charriere/go-app/v7/pkg/app"
)

type OnOffSwitchComponent struct {
	app.Compo
	On       bool
	OnToggle func(ctx app.Context, e app.Event)

	ref app.HTMLInput
}

func (c *OnOffSwitchComponent) Render() app.UI {
	c.Sync()

	return c.ref
}

func (c *OnOffSwitchComponent) OnMount(ctx app.Context) {
	c.Sync()
}

func (c *OnOffSwitchComponent) Sync() {
	if c.ref == nil {
		c.ref = app.Input().Type("checkbox").Checked(c.On).OnChange(c.OnToggle)
	} else {
		c.ref.JSValue().Set("checked", c.On)
	}
}

And a small example of the DOM Sync (unidirectional dataflow; there is no two-way bindings magic here) as a video:

ezgif-3-3f3d06163b23

@maxence-charriere If you don't mind I'd add this to the documentation wiki ;)

@pojntfx
Copy link
Author

pojntfx commented Sep 19, 2020

Just one more small issue: With this approach, I can't pass components as "props" anymore because they won't be updated. I tried to implement the behaviour like so:

package components

import (
	"fmt"
	"log"

	"github.com/maxence-charriere/go-app/v7/pkg/app"
)

type ExpandableSectionComponent struct {
	app.Compo
	Open     bool
	OnToggle func(ctx app.Context, e app.Event)
	Title    string
	Content  app.UI
}

func (c *ExpandableSectionComponent) Render() app.UI {
	ref := app.Div().Class("pf-c-expandable-section__content").Hidden(!c.Open).Body(
		c.Content,
	)

	app.Dispatch(func() {
		if ref.JSValue() != nil {
			log.Println("Setting hidden")

			ref.JSValue().Set("hidden", !c.Open)
		}
	})

	return app.Div().Class(fmt.Sprintf("pf-c-expandable-section pf-u-mb-md %v", func() string {
		if c.Open {
			return "pf-m-expanded"
		}

		return ""
	}())).Body(
		app.Button().Class("pf-c-expandable-section__toggle").Body(
			app.Span().Class("pf-c-expandable-section__toggle-icon").Body(
				app.I().Class("fas fa-angle-right"),
			),
			app.Span().Class("pf-c-expandable-section__toggle-text").Body(
				app.Text(c.Title),
			),
		).OnClick(c.OnToggle),
		ref,
	)
}

I would expect the code in app.Dispatch to be called after the render, and thus I'd expect ref.JSValue to be !nil - however it is. Is there some way I could access the JSValue of an element that I'm rendering in the Render function? I need to access the JSValue of a child component. I'd use the OnUpdate callback but that seems to have been removed in v7 ...

@pojntfx
Copy link
Author

pojntfx commented Sep 20, 2020

Alright, one night later I actually got this to work. Even forked the repo to add a OnPostRender callback, only to find out that it's actually possible in an elegant way with the current implementation ;) The issue with my code above is that ref, even once I tried to access it's JSValue in my OnPostRender callback, might not be rendered - it's a child component after all, so that makes sense; this led to the following solution:

package components

import (
	"fmt"

	"github.com/maxence-charriere/go-app/v7/pkg/app"
)

type ExpandableSectionComponent struct {
	app.Compo

	Open     bool
	OnToggle func(ctx app.Context, e app.Event)
	Title    string
	Content  app.UI
}

func (c *ExpandableSectionComponent) Render() app.UI {
	return app.Div().Class(fmt.Sprintf("pf-c-expandable-section pf-u-mb-md %v", func() string {
		if c.Open {
			return "pf-m-expanded"
		}

		return ""
	}())).Body(
		app.Button().Class("pf-c-expandable-section__toggle").Body(
			app.Span().Class("pf-c-expandable-section__toggle-icon").Body(
				app.I().Class("fas fa-angle-right"),
			),
			app.Span().Class("pf-c-expandable-section__toggle-text").Body(
				app.Text(c.Title),
			),
		).OnClick(c.OnToggle),
		&ExpandableSectionComponentContent{Content: c.Content, Open: c.Open},
	)
}

type ExpandableSectionComponentContent struct {
	app.Compo

	Content app.UI
	Open    bool
}

func (c *ExpandableSectionComponentContent) Render() app.UI {
	app.Dispatch(func() {
		c.JSValue().Set("hidden", !c.Open)
	})

	return app.Div().Class("pf-c-expandable-section__content").Hidden(!c.Open).Body(
		c.Content,
	)
}

Pretty simple actually. app.Dispatch is actually called right after the component of the Render func - not the child component, so I simply created a nested component with the div which's JSValue I want to access as the root component and access it in with the standard JSValue func of app.Compo. Now, using this approach, it is possible to modify JS attributes and take child components without anything going out of sync ;)

@maxence-charriere Would it be possible to add this to the Wiki, in an article like "Syncing DOM properties"? The wiki isn't editable directly but I could write the article and send it to you. Or maybe even adopt the defaultValue and defaultChecked prop conventions of React (which change the HTML attributes value and checked) and use the Value and Checked functions to edit the DOM properties instead? Using the latter, go-app would be a bit more intuitive; however I might be biased as I work with React on the daily ;) I could create a PR if you'd like me to.

@pojntfx pojntfx closed this as completed Sep 20, 2020
@maxence-charriere
Copy link
Owner

Inm gonna takena look to make the wiki editable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants