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

How to make a cute CRUD table component #926

Closed
mgazzin opened this issue Feb 21, 2024 · 17 comments
Closed

How to make a cute CRUD table component #926

mgazzin opened this issue Feb 21, 2024 · 17 comments

Comments

@mgazzin
Copy link

mgazzin commented Feb 21, 2024

Hello,
beautiful project, congrats!
Do you have few minutes to give me some hints on how to build a beautiful CRUD table component?
I am testing the following code:

package main

import (
	"fmt"
	"time"
	"os"
	"log/slog"
	"net/http"
	"github.com/jszwec/csvutil"
	"github.com/maxence-charriere/go-app/v9/pkg/app"
)

type Student struct {
	Name      string `csv:"name"`
	Age       int    `csv:"age,omitempty"`
	CreatedAt time.Time
}

type StudentCompo struct {
	app.Compo
	Students []Student
}

func (sc *StudentCompo) Render() app.UI {
	slog.Info("StudentCompo Render called")
	l := len(sc.Students)
	slog.Info("len(sc.Students)", l)
	for i:=0; i<len(sc.Students); i++ {
		slog.Info("Name", sc.Students[i].Name)
		slog.Info("Age", sc.Students[i].Age)
		slog.Info("CreatedAt", sc.Students[i].CreatedAt)
	}
	return app.Div().Body(
		app.Table().Body(
			app.Tr().Body(
				app.Th().Text("Name"),
				app.Th().Text("Age"),
				app.Th().Text("CreatedAt"),
			),
			/*
			app.Tr().Body(
				app.Td().Text("jacek"),
				app.Td().Text("23"),
				app.Td().Text("2012-04-01T15:00:00Z"),
			),
			app.Tr().Body(
				app.Td().Text("john"),
				app.Td().Text("21"),
				app.Td().Text("2001-05-21T16:57:00Z"),
			),
			*/
			app.Range(sc.Students).Slice(func(i int) app.UI {
				return app.Tr().Body(app.Td().Text(sc.Students[i].Name),
					app.Td().Text(fmt.Sprintf("%d", sc.Students[i].Age)),
					app.Td().Text(sc.Students[i].CreatedAt.Format(time.RFC3339)),
				)	
			}),
		),
	)
}


func createStudents() []Student {
	slog.Info("createStudents called")
	var csvInput = []byte(`
name,age,CreatedAt
jacek,23,2012-04-01T15:00:00Z
john,21,2001-05-21T16:57:00Z`,
	)

	var students []Student
	if err := csvutil.Unmarshal(csvInput, &students); err != nil {
		fmt.Println("error:", err)
	}

	return students
}


func main() {
	logLevel := new(slog.LevelVar)
	logLevel.Set(slog.LevelInfo)
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel}))
	slog.SetDefault(logger)
	slog.Info("Hello, World!")

	sl := createStudents()

	app.Route("/", &StudentCompo{
        		Students: sl,
        	})

	app.RunWhenOnBrowser()
	http.Handle("/", &app.Handler{
		Name: "Student",
	})

	if err := http.ListenAndServe(":3000", nil); err != nil {
		slog.Info("ListenAndServe:", err)
	}
}

But unfortunately it only prints the header:

immagine

The logger prints this:

./students 
time=2024-02-21T10:50:40.142+01:00 level=INFO msg="Hello, World!"
time=2024-02-21T10:50:40.142+01:00 level=INFO msg="createStudents called"
time=2024-02-21T10:50:44.905+01:00 level=INFO msg="StudentCompo Render called"
time=2024-02-21T10:50:44.905+01:00 level=INFO msg=len(sc.Students) !BADKEY=0
time=2024-02-21T10:50:44.905+01:00 level=INFO msg="StudentCompo Render called"
time=2024-02-21T10:50:44.905+01:00 level=INFO msg=len(sc.Students) !BADKEY=0

Do you have any suggestion on how to build this component?

@oderwat
Copy link
Sponsor Contributor

oderwat commented Feb 21, 2024

Hi, cool that you try go-app :)

The problem with your code is that you missed that app.Route() gets an example of the components type and not an initialized component. You need to add your data inside of the components OnMount() or OnInit().

@mgazzin
Copy link
Author

mgazzin commented Feb 21, 2024

Thanks @oderwat

Now I am getting the expected result:

immagine

I have changed my code as below, but I still can't understand why I am getting the following log message while entering Render function:

msg=len(sc.Students) !BADKEY=0

it seems the Compo is not initialized yet.
code:

package main

import (
	"fmt"
	"time"
	"os"
	"log/slog"
	"net/http"
	"github.com/jszwec/csvutil"
	"github.com/maxence-charriere/go-app/v9/pkg/app"
)

type Student struct {
	Name      string `csv:"name"`
	Age       int    `csv:"age,omitempty"`
	CreatedAt time.Time
}

type StudentCompo struct {
	app.Compo
	Students []Student
}

func (sc *StudentCompo) Render() app.UI {
	slog.Info("StudentCompo Render called")
	l := len(sc.Students)
	slog.Info("len(sc.Students)", l)
	for i:=0; i<len(sc.Students); i++ {
		slog.Info("Name", sc.Students[i].Name)
		slog.Info("Age", sc.Students[i].Age)
		slog.Info("CreatedAt", sc.Students[i].CreatedAt)
	}
	return app.Div().Body(
		app.Table().Body(
			app.Tr().Body(
				app.Th().Text("Name"),
				app.Th().Text("Age"),
				app.Th().Text("CreatedAt"),
			),
			app.Range(sc.Students).Slice(func(i int) app.UI {
				return app.Tr().Body(app.Td().Text(sc.Students[i].Name),
					app.Td().Text(fmt.Sprintf("%d", sc.Students[i].Age)),
					app.Td().Text(sc.Students[i].CreatedAt.Format(time.RFC3339)),
				)	
			}),
		),
	)
}

func (sc *StudentCompo) OnMount(ctx app.Context) {
	slog.Info("StudentCompo OnMount called")
	sc.Students = createStudents()
}

func createStudents() []Student {
	slog.Info("createStudents called")
	var csvInput = []byte(`
name,age,CreatedAt
jacek,23,2012-04-01T15:00:00Z
john,21,2001-05-21T16:57:00Z`,
	)

	var students []Student
	if err := csvutil.Unmarshal(csvInput, &students); err != nil {
		fmt.Println("error:", err)
	}

	return students
}


func main() {
	logLevel := new(slog.LevelVar)
	logLevel.Set(slog.LevelInfo)
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel}))
	slog.SetDefault(logger)
	slog.Info("Student component starting")

	app.Route("/", &StudentCompo{})

	app.RunWhenOnBrowser()
	http.Handle("/", &app.Handler{
		Name: "Student",
	})

	if err := http.ListenAndServe(":3000", nil); err != nil {
		slog.Info("ListenAndServe:", err)
	}
}

Do you have any plan to extend your library with more standard components?

@oderwat
Copy link
Sponsor Contributor

oderwat commented Feb 21, 2024

Render() is called before OnMount() because the "mounting" creates the node so if you need to initialize a structure you should use OnInit() or make Render() work with the non initialized state. if !sc.Mounted() would be a universal way to do that. I prefer to check my data and react properly on it's state though.

I see you use slog and that is great, but I think for the front end you should Go with app.Log() or use what we use all the time dbg.Log() (from https://github.com/metatexx/go-app-pkgs/) because this shows you source and line for the callers, which makes it much easier to debug in the frontend as there is not yet a lot of other debugging help available. dbg.Logc() can also help a lot because it lets you easily dump JSValue in a way that you can use the developer tools to browse through the dump. app.Logf("%v") / dbg.Logf("%v") will only print the Go wrapper types instead.

@mgazzin
Copy link
Author

mgazzin commented Feb 24, 2024

It works @oderwat .
Thanks .
I am still reading the documentation but it is not clear to me if Render() can be nested. Suppose I have a bar-menu a top-menu and body, how should I declare these objects?

@oderwat
Copy link
Sponsor Contributor

oderwat commented Feb 24, 2024

You place one component into another one. The only limitation with that is, that each component must have a root component. So if you had one that are table rows, you need to have the '' as their parent to make it a component that then can co into a <table><thead><tr><td>A</td><td>B</td></tr></head>{component:<body><tr><td>A</td><td>B</td></tr></thbody>}</table> construct (using {} to illustrate the component, and it's Render() output).
We usually place each component in its own package to avoid directly shared data.

@mgazzin
Copy link
Author

mgazzin commented Feb 28, 2024

Thanks @oderwat .
I am not very confident with WASM. How debug is possible in SPA? I can't see the effect of my changes in the source page so is there any other way to check if my code is correct?

@oderwat
Copy link
Sponsor Contributor

oderwat commented Feb 28, 2024

You can write tests (see go-app test sources). You can actually debug in the browser, but that is not expressive, but I think they work in it. But currently you mostly need to do "app.Log()" or "dbg.Log()" (our package) print debugging.

For the problem with recompilation and reloading I wrote an internal tool that for all our go applications including the PWAs handles restarts of services and apps with support for go.work files and other stuff. But that comes down to simply checking for changes and restarting the app server and have a short time update checker in the web application and have that reload. There one "can" store state in local storage and let the app jump into this, so that you do not have to click through all the functionality. You could also write short individual applications that test / demos components. We do this by the same tool which also does the watcher functionality as this can create an app skeleton by just giving it a name.

Besides all of this I also experimented in automatic testing through headless chrome witch also works quite well, but we do not utilize that in production yet. I have an older public repo with a proof of concept. We (again internally) have a larger testing framework build on this idea. https://github.com/oderwat/rodgoapp

@mgazzin
Copy link
Author

mgazzin commented Feb 28, 2024

Could you please make an example of using app.Log() or dbg.Log() ?
The tool you mentioned, do you have the intention to keep it internally?
Thanks @oderwat

@mgazzin
Copy link
Author

mgazzin commented Feb 28, 2024

Also PreRender can be useful for debugging?

@oderwat
Copy link
Sponsor Contributor

oderwat commented Feb 28, 2024

Could you please make an example of using app.Log() or dbg.Log() ?

I would go get github.com/metatexx/go-app-pkgs/dbg and then import it and use dbg.Logf("name: %q", "oderwat") which will output the caller file and line. You can also set your internal package path so that it is shortened using: dbg.TrimPrefixes=[]string{"yourpackagebasepath"} and disable the logging with dbg.Enabled=false.

I normally just do it like this:

func (sc *StudentCompo) OnMount(ctx app.Context) {
	dbg.Log()
	sc.Students = createStudents()
}

or

func (sc *StudentCompo) OnMount(ctx app.Context) {
	dbg.Log("start")
        defer dbg.Log("end")
	sc.Students = createStudents()
}

Which will automatically show the package and function without writing it out all the time.

You also can use: dbg.Logc(component.JSValue()) to get an interactive variant of the DOM node that is the root of your component in the dev tools console.

The tool you mentioned, do you have the intention to keep it internally? Thanks @oderwat

This is not planned. First of: Releasing anything usually means that you need to support and fully document it. We are too small to handle that. Second: This is magic and the tools name is even magx and therefor it is also dangerous to use. It even contains the line "Preventing you from imploding the universe" as one of its error messages.

You should maybe check out "mage" which was used by us in the past and where I wrote the first automatic re-compiler for. I think https://github.com/oderwat/go-nats-app could be of interest for you (maybe also for the front-/backend communication for which we use NATS nearly exclusively). The actual watcher code could be a variant of run which starts the program in a go routine and then polls the glob patterns for changes, if they occur you kill the process and start over again. Of course there are multiple other things to be taken care of. I once used some other go based file watcher but I don't remember which one and why it was not "good enough" in the end.

@oderwat
Copy link
Sponsor Contributor

oderwat commented Feb 28, 2024

Also PreRender can be useful for debugging?

We actually never use PreRender() at all. Because of the code separation of WASM and server code there is not even any of the component in the server and therefor there are other routes and there is just one empty component. You can see this in https://github.com/oderwat/go-nats-app/blob/master/backend-entry.go

@oderwat
Copy link
Sponsor Contributor

oderwat commented Mar 1, 2024

For debugging the actual WASM I found some good information here:

https://www.youtube.com/watch?v=DcEcRfauvuw

https://github.com/WebAssembly/wabt

@JulienLecoq
Copy link

Render() is called before OnMount() because the "mounting" creates the node so if you need to initialize a structure you should use OnInit() or make Render() work with the non initialized state. if !sc.Mounted() would be a universal way to do that. I prefer to check my data and react properly on it's state though.

I see you use slog and that is great, but I think for the front end you should Go with app.Log() or use what we use all the time dbg.Log() (from https://github.com/metatexx/go-app-pkgs/) because this shows you source and line for the callers, which makes it much easier to debug in the frontend as there is not yet a lot of other debugging help available. dbg.Logc() can also help a lot because it lets you easily dump JSValue in a way that you can use the developer tools to browse through the dump. app.Logf("%v") / dbg.Logf("%v") will only print the Go wrapper types instead.

Mhh, OnInit() is a thing? I don't see it being referenced there: https://go-app.dev/components#lifecycle-events

@oderwat
Copy link
Sponsor Contributor

oderwat commented Mar 12, 2024

Mhh, OnInit() is a thing? I don't see it being referenced there: https://go-app.dev/components#lifecycle-events

I think that whole diagram is a bit outdated or does not contain the information that is needed (imho). OnInit() was proposed by me quite some time ago and the graphics also does not mention render at all. I guess that the v10 release also makes changes to order and details of the execution and that @maxence-charriere updates the documentation eventually.

@maxence-charriere
Copy link
Owner

Documentation will be revamped. At the begining v10 changed some stuff in terms of ordering but is has been reverted back since you folks were using those things.

@JulienLecoq
Copy link

Ok 👌🏻

@JulienLecoq
Copy link

I'm really struggling to deploy my website too, I hope the new doc will improve this part. I tried the static way showed in the current doc but half of my website is broken doing this way. I'm still a bit confused what goes into the "static" part.

Everything works fine if I launch a web server on "/" but doing this way I struggle to deploy it on services such as fly.io.

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

4 participants