go-app icon indicating copy to clipboard operation
go-app copied to clipboard

Dinamically update component?

Open yosoymau opened this issue 3 years ago • 22 comments

Hello, i just started to use this go-app tool.

Basically my problem is that i tried to build a component that dynamically changes it's contents with the data received from an http request. But this looks like it's a dumb approach because it only seems to update the contents when i restart the server.

Maybe someone can tell me how could this be done or where to look for a solution to what i'm trying to accomplish.

Thanks for all!

yosoymau avatar Apr 10 '22 22:04 yosoymau

May I ask when you expect the changed content to be retrieved?

oderwat avatar Apr 10 '22 22:04 oderwat

I was under the assumption that the component function would get called every time the component needed to be rendered. That is what i was trying to accomplish. This doesn't seem to work this way, and because of this, the data is only retrieved at the startup of the server.

I think my problem is that i have a fundamental misunderstanding on how this all works, If you could share some insight on how could this be done i'd be grateful.

Basically what i want to get is this:

  1. User hits refresh page
  2. Server make http request to get updated data
  3. Place that data into the component structure.
  4. Render component to the page.

yosoymau avatar Apr 11 '22 00:04 yosoymau

Your point 2 does not make sense to me. Why would the "server" request updated data? When the user presses refresh. the browser will refresh the page by loading it (again) from the server. In the case of go-app and you are not using "OnPreRender()" the server will not do anything regarding your components. They are handled in the front end (by "the client").

To actually have different data on the same "URL" (without recompiling your program) you need to get the data "in the client" and then "dynamically render it".

One usually does not want the user to hit refresh for that (actually this is the last thing I want my users to do when I implement my own routing in the front end).

I think you should read:

  1. https://go-app.dev/architecture
  2. https://go-app.dev/seo
  3. https://go-app.dev/components

You could also try to read and understand: https://github.com/maxence-charriere/go-app/tree/42eb629a51fb842c0da7abfa7e65b5bd1956ed1c/docs/src which loads most of its data as markdown files from the client and converts it to HTML to render the pages.

BTW: From what you wrote so far I am not sure if you actually "need" go-app (a PWA framework) but maybe should look at one of the normal HTML server frameworks (there are plenty).

oderwat avatar Apr 11 '22 01:04 oderwat

@yosoymau maybe be a code snippet of why you try to do would help understand where is the struggle.

maxence-charriere avatar Apr 11 '22 03:04 maxence-charriere

Thank you for your answers! I've now come to the realization that i don't really have a clue of what it is that i am doing and that i should read more about front end development because i am starting to understand how my questions are, as you've said, kinda non sensical lol, because of this i think we should close this issue.

Sorry for the inconvenience and thanks a lot for your answers, they've been very useful to shine some light into my clueless mind lol.

yosoymau avatar Apr 13 '22 05:04 yosoymau

I just try to implement a simple clock, how do I make the time reflected on GUI every seconds?

func (c *Clock) OnMount(ctx app.Context, a app.Action) {
	for range time.Tick(time.Second) {
		c.Time = time.Now().Format(time.RFC3339)
		c.Update()
	}
}

hauntedness avatar Apr 30 '22 13:04 hauntedness

c.Time

make that lowercase (c.time) and try again. You may also not use c.Update() but call ctx.Dispatch(func(ctx app.Context) { c.Time = time.Now().Format(time.RFC3339) })

oderwat avatar Apr 30 '22 15:04 oderwat

func (c *Clock) OnMount(ctx app.Context, a app.Action) looks wrong too (it does not sound as this is an action handler). I suggest you add var _ app.Mounter = (*Clock)(nil) to validate your implementation fits the interface.

oderwat avatar Apr 30 '22 16:04 oderwat

And of course, you need to make that timer loop asynchronously:

package clock

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

type Clock struct {
	app.Compo
	time string
}

var _ app.Mounter = (*Clock)(nil)

func (c *Clock) OnMount(ctx app.Context) {
	ctx.Async(func() {
		for range time.Tick(time.Second) {
			ctx.Dispatch(func(ctx app.Context) {
				c.time = time.Now().Format(time.RFC3339)
			})
		}
	})
}

func (c *Clock) Render() app.UI {
	return app.Div().Class("clock").Text(c.time)
}

oderwat avatar Apr 30 '22 16:04 oderwat

I was curious how to make it useable for real:

package clock

import (
	"github.com/maxence-charriere/go-app/v9/pkg/app"
	"github.com/metatexx/go-app-pkgs/dbg"
	"time"
)

type Clock struct {
	app.Compo
	Format  string
	Class   string
	time    string
	running bool
}

var _ app.Mounter = (*Clock)(nil)
var _ app.Dismounter = (*Clock)(nil)

func (c *Clock) OnMount(ctx app.Context) {
	ctx.Async(func() {
		c.running = true
		for range time.Tick(time.Second) {
			if !c.running {
				dbg.Log("stopped")
				break
			}
			ctx.Dispatch(func(ctx app.Context) {
				c.updateTime()
			})
		}
		dbg.Log("exits")
	})
	c.updateTime()
}

func (c *Clock) updateTime() {
	c.time = time.Now().Format(c.Format)

}

func (c *Clock) Render() app.UI {
	return app.Div().Class(c.Class).Text(c.time)
}

func (c *Clock) OnDismount() {
	dbg.Log("stopping")
	c.running = false
}

usage like&clock.Clock{Format: "15:04:05 02.01.2006", Class: "text-gray-300"}. German Date/Time + TailwindCSS in that example.

oderwat avatar Apr 30 '22 16:04 oderwat

Also curious, slightly different implementation for timer cleanup

Edit: Updated with some changes to make sure the for loop exits on dismount

package clock

import (
	"time"

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

type Clock struct {
	app.Compo
	time   string
	done   chan bool
	ticker *time.Ticker
}

var _ app.Mounter = (*Clock)(nil)
var _ app.Dismounter = (*Clock)(nil)

func (c *Clock) OnMount(ctx app.Context) {
	c.done = make(chan bool, 1)
	c.ticker = time.NewTicker(time.Second * 1)
	c.updateTime(ctx)
	ctx.Async(func() { c.timerLoop(ctx) })
}

func (c *Clock) timerLoop(ctx app.Context) {
	for {
		select {
		case <-c.ticker.C:
			ctx.Dispatch(c.updateTime)
		case <-c.done:
			c.ticker.Stop()
			return
		}
	}
}

func (c *Clock) updateTime(_ app.Context) {
	c.time = time.Now().Format(time.RFC3339)
}

func (c *Clock) Render() app.UI {
	return app.Div().Class("clock").Text(c.time)
}

func (c *Clock) OnDismount() {
	c.done <- true
}

mlctrez avatar Apr 30 '22 16:04 mlctrez

@mlctrez It seems like your timer code is better as my one still leaks the timer.

oderwat avatar May 02 '22 08:05 oderwat

But the problem with the way @mlctrez stops the timer (for me) is, that it does not seem to run the part of the function after the for{} so I do not get to see the dbg.Log("exits") from my example if I use his code. I do not understand why that is the case? I even changed the dbg.Log("exits") with creating a panic, because I thought I might just not get output.

I ended up with:

package clock

import (
	"github.com/maxence-charriere/go-app/v9/pkg/app"
	"github.com/metatexx/go-app-pkgs/dbg"
	"time"
)

type Clock struct {
	app.Compo
	Format  string
	Class   string
	time    string
	running bool
	ticker  *time.Ticker
}

var _ app.Mounter = (*Clock)(nil)
var _ app.Dismounter = (*Clock)(nil)

func (c *Clock) OnMount(ctx app.Context) {
	ctx.Async(func() {
		c.ticker = time.NewTicker(time.Second * 1)
		defer func() {
			c.ticker.Stop()
			dbg.Log("ticker stopped")
		}()
		c.running = true
		for range c.ticker.C {
			if !c.running {
				dbg.Log("stopped")
				break
			}
			ctx.Dispatch(func(ctx app.Context) {
				c.updateTime()
			})
		}
		dbg.Log("exits")
	})
	c.updateTime()
}

func (c *Clock) updateTime() {
	c.time = time.Now().Format(c.Format)

}

func (c *Clock) Render() app.UI {
	return app.Div().Class(c.Class).Text(c.time)
}

func (c *Clock) OnDismount() {
	dbg.Log("stopping")
	c.running = false
	c.ticker.Reset(1)
}

This results in:

pkg/components/clock.(*Clock).OnDismount: stopping
wasm_exec.js:349 pkg/components/sortabletable.(*SortableTable).OnDismount
wasm_exec.js:349 pkg/components/clock.(*Clock).OnMount.func1: stopped
wasm_exec.js:349 pkg/components/clock.(*Clock).OnMount.func1: exits
wasm_exec.js:349 pkg/components/clock.(*Clock).OnMount.func1.1: ticker stopped

This code also renders the clock as soon as possible (and not just after the first tick).

Edit: The c.ticker.Reset(1) was added later and makes it stop immediately (before that it stopped only after the next tick).

oderwat avatar May 02 '22 14:05 oderwat

@oderwat I totally missed that time.Ticker.Stop() does not close the channel. That's why the for loop never exits.

mlctrez avatar May 02 '22 16:05 mlctrez

@oderwat I totally missed that time.Ticker.Stop() does not close the channel. That's why the for loop never exits.

Oh, yes, so it just stops, and the loop freezes. Makes sense.

oderwat avatar May 02 '22 17:05 oderwat

@oderwat @mlctrez really cool and exactly what I what. many many thanks

hauntedness avatar May 04 '22 06:05 hauntedness

Just started to use go-app .. i am kinda in love but to the point. something lite and dirty like this will work? It will benefit if you have an interface TaskManager in the app

package tasks

import (
	"sync"
	"time"

	"github.com/google/uuid"
	"github.com/maxence-charriere/go-app/v9/pkg/app"
)

var ImmediateTask time.Duration = 0 * time.Second

type Task struct {
	ID   uuid.UUID
	Name string

	ctx app.Context

	every time.Duration

	ticker *time.Ticker
	mux    sync.Mutex

	cFunc func(ctx app.Context)

	running int
}

func NewTask(ctx app.Context, name string, every time.Duration, clientfunc func(ctx app.Context)) *Task {
	tsk := new(Task)

	tsk.Name = name
	tsk.ctx = ctx

	tsk.running = 0
	tsk.ID = uuid.New()

	tsk.every = every

       if tsk.every > 0 {
	 tsk.ticker = time.NewTicker(tsk.every)
       }
	tsk.cFunc = clientfunc

	return tsk
}

func (t *Task) Run() {
	t.ctx.Async(func() {
		t.running = 1
		defer func() {
			t.ticker.Stop()
			app.Log("ticker stopped")
		}()

		if t.every > 0 && t.ticker != nil {
			for range t.ticker.C {
				if t.running == 0 {
					app.Log("stopped")
					break
				}
				t.ctx.Dispatch(func(ctx app.Context) {
					t.mux.Lock()
					defer t.mux.Unlock()

					t.cFunc(ctx)

				})
			}

		} else {
			t.ctx.Dispatch(func(ctx app.Context) {
				t.mux.Lock()
				defer t.mux.Unlock()

				t.cFunc(ctx)

			})

		}
		app.Log("exits")

	})

	if t.every > 0 {
		t.mux.Lock()
		defer t.mux.Unlock()
		t.cFunc(t.ctx)
	}
}

func (t *Task) Unlock() {
	t.mux.Lock()
}

func (t *Task) Lock() {
	t.mux.Lock()
}

func (t *Task) Stop() {
	t.mux.Lock()
	defer t.mux.Unlock()

	t.running = 0

	if t.ticker != nil {
		t.ticker.Reset(t.every)

	}
}

func (t *Task) Restart() {
	t.running = 1
	t.Run()
}

func (t *Task) Remove() {
	t.Stop()
	t = nil

}

func (t *Task) Running() bool {
	return t.running == 1
}

func (t *Task) Stopped() bool {
	return t.running == 0
}
`

callevo avatar Feb 05 '23 20:02 callevo

I guess you should check your comment and correct the usage of the ``` code block.

What I can see, from what I can see, it seems that you use a mutex in dispatched code. I think that is not necessarily needed because all code that you dispatch will run sequential in the UI go routine: https://go-app.dev/concurrency#ui-goroutine

oderwat avatar Feb 05 '23 20:02 oderwat

@oderwat thanks for the answer (``` code block done , my apologies), but if you put something like a read or a rest call and you change something outside of the task , something maybe like this in another piece of the code that you also change in the task. But understood the ui-goroutine

		c.tsk.Lock()
		err := ctx.LocalStorage().Get("authUser", &loginUser)
		if err != nil {
			app.Log("Error reading")
		}
		c.tsk.Unlock()

callevo avatar Feb 05 '23 20:02 callevo

I did not check, but can you even reliably use ctx.LocalStorage() outside the UI go routine? I would not try to do that. You need to have ctx for that, which most likely is inside a handler or OnWhatever() and they all run inside the ui-goroutine anyway.

For our projects, we stay with all component modification code inside the ui-goroutine and the same for accessing local storage or basically anything DOM/JavaScript. All concurrent code ends up with some kind of dispatching that result.

oderwat avatar Feb 05 '23 21:02 oderwat

Local storage is goroutine safe https://github.com/maxence-charriere/go-app/blob/master/pkg/app/storage.go#L106

maxence-charriere avatar Feb 06 '23 02:02 maxence-charriere

Thanks @oderwat and @maxence-charriere , so we can safely remove the mutex. We are scheduling the tasks in the OnMount and they work beautifully. Thanks for your help

callevo avatar Feb 06 '23 15:02 callevo