| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513 |
- package app
- import (
- "context"
- "io"
- "reflect"
- "strings"
- "github.com/maxence-charriere/go-app/v9/pkg/errors"
- )
- // Composer is the interface that describes a customized, independent and
- // reusable UI element.
- //
- // Satisfying this interface is done by embedding app.Compo into a struct and
- // implementing the Render function.
- //
- // Example:
- //
- // type Hello struct {
- // app.Compo
- // }
- //
- // func (c *Hello) Render() app.UI {
- // return app.Text("hello")
- // }
- type Composer interface {
- UI
- // Render returns the node tree that define how the component is desplayed.
- Render() UI
- // Update update the component appearance. It should be called when a field
- // used to render the component has been modified.
- Update()
- // ResizeContent triggers OnResize() on all the component children that
- // implement the Resizer interface.
- ResizeContent()
- // ValueTo stores the value of the DOM element (if exists) that emitted an
- // event into the given value.
- //
- // The given value must be a pointer to a signed integer, unsigned integer,
- // or a float.
- //
- // It panics if the given value is not a pointer.
- ValueTo(any) EventHandler
- updateRoot() error
- dispatch(func(Context))
- }
- // PreRenderer is the interface that describes a component that performs
- // instruction when it is server-side pre-rendered.
- //
- // A pre-rendered component helps in achieving SEO friendly content.
- type PreRenderer interface {
- // The function called when the component is server-side pre-rendered.
- //
- // If pre-rendering requires blocking operations such as performing an HTTP
- // request, ensure that they are done synchronously. A good practice is to
- // avoid using goroutines during pre-rendering.
- OnPreRender(Context)
- }
- // Initializer is the interface that describes a component that performs
- // initialization instruction before being pre-rendered or mounted.
- type Initializer interface {
- Composer
- // The function called before the component is pre-rendered or mounted.
- OnInit()
- }
- // Mounter is the interface that describes a component that can perform
- // additional actions when mounted.
- type Mounter interface {
- Composer
- // The function called when the component is mounted. It is always called on
- // the UI goroutine.
- OnMount(Context)
- }
- // Dismounter is the interface that describes a component that can perform
- // additional actions when dismounted.
- type Dismounter interface {
- Composer
- // The function called when the component is dismounted. It is always called
- // on the UI goroutine.
- OnDismount()
- }
- // Navigator is the interface that describes a component that can perform
- // additional actions when navigated on.
- type Navigator interface {
- Composer
- // The function that called when the component is navigated on. It is always
- // called on the UI goroutine.
- OnNav(Context)
- }
- // Updater is the interface that describes a component that can do additional
- // instructions when one of its exported fields is modified by its nearest
- // parent component.
- type Updater interface {
- // The function called when one of the component exported fields is modified
- // by its nearest parent component. It is always called on the UI goroutine.
- OnUpdate(Context)
- }
- // AppUpdater is the interface that describes a component that is notified when
- // the application is updated.
- type AppUpdater interface {
- // The function called when the application is updated. It is always called
- // on the UI goroutine.
- OnAppUpdate(Context)
- }
- // AppInstaller is the interface that describes a component that is notified
- // when the application installation state changes.
- type AppInstaller interface {
- // The function called when the application becomes installable or
- // installed. Use Context.IsAppInstallable() or Context.IsAppInstalled to
- // check the install state. OnAppInstallChange is always called on the UI
- // goroutine.
- OnAppInstallChange(Context)
- }
- // Resizer is the interface that describes a component that is notified when the
- // app has been resized or a parent component calls the ResizeContent() method.
- type Resizer interface {
- // The function called when the application is resized or a parent component
- // called its ResizeContent() method. It is always called on the UI
- // goroutine.
- OnResize(Context)
- }
- // Component events.
- type nav struct{}
- type appUpdate struct{}
- type appInstallChange struct{}
- type resize struct{}
- // Compo represents the base struct to use in order to build a component.
- type Compo struct {
- disp Dispatcher
- ctx context.Context
- ctxCancel func()
- parentElem UI
- root UI
- this Composer
- }
- // Kind returns the ui element kind.
- func (c *Compo) Kind() Kind {
- return Component
- }
- // JSValue returns the javascript value of the component root.
- func (c *Compo) JSValue() Value {
- return c.root.JSValue()
- }
- // Mounted reports whether the component is mounted.
- func (c *Compo) Mounted() bool {
- return c.getDispatcher() != nil &&
- c.ctx != nil &&
- c.ctx.Err() == nil &&
- c.root != nil && c.root.Mounted() &&
- c.self() != nil
- }
- // Render describes the component content. This is a default implementation to
- // satisfy the app.Composer interface. It should be redefined when app.Compo is
- // embedded.
- func (c *Compo) Render() UI {
- return Div().
- DataSet("compo-type", c.name()).
- Style("border", "1px solid currentColor").
- Style("padding", "12px 0").
- Body(
- H1().Text("Component "+strings.TrimPrefix(c.name(), "*")),
- P().Body(
- Text("Change appearance by implementing: "),
- Code().
- Style("color", "deepskyblue").
- Style("margin", "0 6px").
- Text("func (c "+c.name()+") Render() app.UI"),
- ),
- )
- }
- // Update triggers a component appearance update. It should be called when a
- // field used to render the component has been modified. Updates are always
- // performed on the UI goroutine.
- func (c *Compo) Update() {
- c.dispatch(func(Context) {})
- }
- // ResizeContent triggers OnResize() on all the component children that
- // implement the Resizer interface.
- func (c *Compo) ResizeContent() {
- c.dispatch(func(Context) {
- c.root.onComponentEvent(resize{})
- })
- }
- // ValueTo stores the value of the DOM element (if exists) that emitted an event
- // into the given value.
- //
- // The given value must be a pointer to a signed integer, unsigned integer, or a
- // float.
- //
- // It panics if the given value is not a pointer.
- func (c *Compo) ValueTo(v any) EventHandler {
- return func(ctx Context, e Event) {
- value := ctx.JSSrc().Get("value")
- if err := stringTo(value.String(), v); err != nil {
- Log(errors.New("storing dom element value failed").Wrap(err))
- return
- }
- }
- }
- func (c *Compo) name() string {
- name := reflect.TypeOf(c.self()).String()
- name = strings.ReplaceAll(name, "main.", "")
- return name
- }
- func (c *Compo) self() UI {
- return c.this
- }
- func (c *Compo) setSelf(v UI) {
- if v != nil {
- c.this = v.(Composer)
- return
- }
- c.this = nil
- }
- func (c *Compo) getContext() context.Context {
- return c.ctx
- }
- func (c *Compo) getDispatcher() Dispatcher {
- return c.disp
- }
- func (c *Compo) getAttributes() attributes {
- return nil
- }
- func (c *Compo) getEventHandlers() eventHandlers {
- return nil
- }
- func (c *Compo) getParent() UI {
- return c.parentElem
- }
- func (c *Compo) setParent(p UI) {
- c.parentElem = p
- }
- func (c *Compo) getChildren() []UI {
- return []UI{c.root}
- }
- func (c *Compo) mount(d Dispatcher) error {
- if c.Mounted() {
- return errors.New("mounting component failed").
- Tag("reason", "already mounted").
- Tag("name", c.name()).
- Tag("kind", c.Kind())
- }
- if initializer, ok := c.self().(Initializer); ok && !d.isServerSide() {
- initializer.OnInit()
- }
- c.disp = d
- c.ctx, c.ctxCancel = context.WithCancel(context.Background())
- root := c.render()
- if err := mount(d, root); err != nil {
- return errors.New("mounting component failed").
- Tag("name", c.name()).
- Tag("kind", c.Kind()).
- Wrap(err)
- }
- root.setParent(c.this)
- c.root = root
- if c.getDispatcher().isServerSide() {
- return nil
- }
- if mounter, ok := c.self().(Mounter); ok {
- c.dispatch(mounter.OnMount)
- return nil
- }
- c.dispatch(nil)
- return nil
- }
- func (c *Compo) dismount() {
- c.ctxCancel()
- dismount(c.root)
- if dismounter, ok := c.this.(Dismounter); ok {
- dismounter.OnDismount()
- }
- }
- func (c *Compo) canUpdateWith(v UI) bool {
- return c.Mounted() &&
- c.Kind() == v.Kind() &&
- c.name() == v.name()
- }
- func (c *Compo) updateWith(v UI) error {
- if c.self() == v {
- return nil
- }
- if !c.canUpdateWith(v) {
- return errors.New("cannot update component with given element").
- Tag("current", reflect.TypeOf(c.self())).
- Tag("new", reflect.TypeOf(v))
- }
- aval := reflect.Indirect(reflect.ValueOf(c.self()))
- bval := reflect.Indirect(reflect.ValueOf(v))
- compotype := reflect.ValueOf(c).Elem().Type()
- haveModifiedFields := false
- for i := 0; i < aval.NumField(); i++ {
- a := aval.Field(i)
- b := bval.Field(i)
- if a.Type() == compotype {
- continue
- }
- if !a.CanSet() {
- continue
- }
- if !reflect.DeepEqual(a.Interface(), b.Interface()) {
- a.Set(b)
- haveModifiedFields = true
- }
- }
- if !haveModifiedFields {
- return nil
- }
- if err := c.updateRoot(); err != nil {
- return errors.New("updating root failed").Wrap(err)
- }
- if updater, ok := c.self().(Updater); ok {
- c.dispatch(updater.OnUpdate)
- }
- c.getDispatcher().removeComponentUpdate(c.this)
- return nil
- }
- func (c *Compo) dispatch(fn func(Context)) {
- c.getDispatcher().Dispatch(Dispatch{
- Mode: Update,
- Source: c.self(),
- Function: fn,
- })
- }
- func (c *Compo) updateRoot() error {
- a := c.root
- b := c.render()
- if canUpdate(a, b) {
- return update(a, b)
- }
- return c.replaceRoot(b)
- }
- func (c *Compo) replaceRoot(v UI) error {
- old := c.root
- new := v
- if err := mount(c.getDispatcher(), new); err != nil {
- return errors.New("replacing component root failed").
- Tag("kind", c.Kind()).
- Tag("name", c.name()).
- Tag("root-kind", old.Kind()).
- Tag("root-name", old.name()).
- Tag("new-root-kind", new.Kind()).
- Tag("new-root-name", new.name()).
- Wrap(err)
- }
- var parent UI
- for {
- parent = c.getParent()
- if parent == nil || parent.Kind() == HTML {
- break
- }
- }
- if parent == nil {
- return errors.New("replacing component root failed").
- Tag("kind", c.Kind()).
- Tag("name", c.name()).
- Tag("reason", "coponent does not have html element parents")
- }
- c.root = new
- new.setParent(c.self())
- oldjs := old.JSValue()
- newjs := v.JSValue()
- parent.JSValue().replaceChild(newjs, oldjs)
- dismount(old)
- return nil
- }
- func (c *Compo) render() UI {
- elems := FilterUIElems(c.this.Render())
- return elems[0]
- }
- func (c *Compo) preRender(p Page) {
- c.root.preRender(p)
- if initializer, ok := c.self().(Initializer); ok {
- initializer.OnInit()
- }
- if preRenderer, ok := c.self().(PreRenderer); ok {
- c.dispatch(preRenderer.OnPreRender)
- }
- }
- func (c *Compo) onComponentEvent(le any) {
- switch le := le.(type) {
- case nav:
- c.onNav(le)
- case appUpdate:
- c.onAppUpdate(le)
- case appInstallChange:
- c.onAppInstallChange(le)
- case resize:
- c.onResize(le)
- }
- c.root.onComponentEvent(le)
- }
- func (c *Compo) onNav(n nav) {
- if nav, ok := c.self().(Navigator); ok {
- c.dispatch(nav.OnNav)
- return
- }
- }
- func (c *Compo) onAppUpdate(au appUpdate) {
- if updater, ok := c.self().(AppUpdater); ok {
- c.dispatch(updater.OnAppUpdate)
- }
- }
- func (c *Compo) onAppInstallChange(ai appInstallChange) {
- if installer, ok := c.self().(AppInstaller); ok {
- c.dispatch(installer.OnAppInstallChange)
- }
- }
- func (c *Compo) onResize(r resize) {
- if resizer, ok := c.self().(Resizer); ok {
- c.dispatch(resizer.OnResize)
- return
- }
- }
- func (c *Compo) html(w io.Writer) {
- if c.root == nil {
- c.root = c.render()
- c.root.setSelf(c.root)
- }
- c.root.html(w)
- }
- func (c *Compo) htmlWithIndent(w io.Writer, indent int) {
- if c.root == nil {
- c.root = c.render()
- c.root.setSelf(c.root)
- }
- c.root.htmlWithIndent(w, indent)
- }
|