component.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. package app
  2. import (
  3. "context"
  4. "io"
  5. "reflect"
  6. "strings"
  7. "github.com/maxence-charriere/go-app/v9/pkg/errors"
  8. )
  9. // Composer is the interface that describes a customized, independent and
  10. // reusable UI element.
  11. //
  12. // Satisfying this interface is done by embedding app.Compo into a struct and
  13. // implementing the Render function.
  14. //
  15. // Example:
  16. //
  17. // type Hello struct {
  18. // app.Compo
  19. // }
  20. //
  21. // func (c *Hello) Render() app.UI {
  22. // return app.Text("hello")
  23. // }
  24. type Composer interface {
  25. UI
  26. // Render returns the node tree that define how the component is desplayed.
  27. Render() UI
  28. // Update update the component appearance. It should be called when a field
  29. // used to render the component has been modified.
  30. Update()
  31. // ResizeContent triggers OnResize() on all the component children that
  32. // implement the Resizer interface.
  33. ResizeContent()
  34. // ValueTo stores the value of the DOM element (if exists) that emitted an
  35. // event into the given value.
  36. //
  37. // The given value must be a pointer to a signed integer, unsigned integer,
  38. // or a float.
  39. //
  40. // It panics if the given value is not a pointer.
  41. ValueTo(any) EventHandler
  42. updateRoot() error
  43. dispatch(func(Context))
  44. }
  45. // PreRenderer is the interface that describes a component that performs
  46. // instruction when it is server-side pre-rendered.
  47. //
  48. // A pre-rendered component helps in achieving SEO friendly content.
  49. type PreRenderer interface {
  50. // The function called when the component is server-side pre-rendered.
  51. //
  52. // If pre-rendering requires blocking operations such as performing an HTTP
  53. // request, ensure that they are done synchronously. A good practice is to
  54. // avoid using goroutines during pre-rendering.
  55. OnPreRender(Context)
  56. }
  57. // Initializer is the interface that describes a component that performs
  58. // initialization instruction before being pre-rendered or mounted.
  59. type Initializer interface {
  60. Composer
  61. // The function called before the component is pre-rendered or mounted.
  62. OnInit()
  63. }
  64. // Mounter is the interface that describes a component that can perform
  65. // additional actions when mounted.
  66. type Mounter interface {
  67. Composer
  68. // The function called when the component is mounted. It is always called on
  69. // the UI goroutine.
  70. OnMount(Context)
  71. }
  72. // Dismounter is the interface that describes a component that can perform
  73. // additional actions when dismounted.
  74. type Dismounter interface {
  75. Composer
  76. // The function called when the component is dismounted. It is always called
  77. // on the UI goroutine.
  78. OnDismount()
  79. }
  80. // Navigator is the interface that describes a component that can perform
  81. // additional actions when navigated on.
  82. type Navigator interface {
  83. Composer
  84. // The function that called when the component is navigated on. It is always
  85. // called on the UI goroutine.
  86. OnNav(Context)
  87. }
  88. // Updater is the interface that describes a component that can do additional
  89. // instructions when one of its exported fields is modified by its nearest
  90. // parent component.
  91. type Updater interface {
  92. // The function called when one of the component exported fields is modified
  93. // by its nearest parent component. It is always called on the UI goroutine.
  94. OnUpdate(Context)
  95. }
  96. // AppUpdater is the interface that describes a component that is notified when
  97. // the application is updated.
  98. type AppUpdater interface {
  99. // The function called when the application is updated. It is always called
  100. // on the UI goroutine.
  101. OnAppUpdate(Context)
  102. }
  103. // AppInstaller is the interface that describes a component that is notified
  104. // when the application installation state changes.
  105. type AppInstaller interface {
  106. // The function called when the application becomes installable or
  107. // installed. Use Context.IsAppInstallable() or Context.IsAppInstalled to
  108. // check the install state. OnAppInstallChange is always called on the UI
  109. // goroutine.
  110. OnAppInstallChange(Context)
  111. }
  112. // Resizer is the interface that describes a component that is notified when the
  113. // app has been resized or a parent component calls the ResizeContent() method.
  114. type Resizer interface {
  115. // The function called when the application is resized or a parent component
  116. // called its ResizeContent() method. It is always called on the UI
  117. // goroutine.
  118. OnResize(Context)
  119. }
  120. // Component events.
  121. type nav struct{}
  122. type appUpdate struct{}
  123. type appInstallChange struct{}
  124. type resize struct{}
  125. // Compo represents the base struct to use in order to build a component.
  126. type Compo struct {
  127. disp Dispatcher
  128. ctx context.Context
  129. ctxCancel func()
  130. parentElem UI
  131. root UI
  132. this Composer
  133. }
  134. // Kind returns the ui element kind.
  135. func (c *Compo) Kind() Kind {
  136. return Component
  137. }
  138. // JSValue returns the javascript value of the component root.
  139. func (c *Compo) JSValue() Value {
  140. return c.root.JSValue()
  141. }
  142. // Mounted reports whether the component is mounted.
  143. func (c *Compo) Mounted() bool {
  144. return c.getDispatcher() != nil &&
  145. c.ctx != nil &&
  146. c.ctx.Err() == nil &&
  147. c.root != nil && c.root.Mounted() &&
  148. c.self() != nil
  149. }
  150. // Render describes the component content. This is a default implementation to
  151. // satisfy the app.Composer interface. It should be redefined when app.Compo is
  152. // embedded.
  153. func (c *Compo) Render() UI {
  154. return Div().
  155. DataSet("compo-type", c.name()).
  156. Style("border", "1px solid currentColor").
  157. Style("padding", "12px 0").
  158. Body(
  159. H1().Text("Component "+strings.TrimPrefix(c.name(), "*")),
  160. P().Body(
  161. Text("Change appearance by implementing: "),
  162. Code().
  163. Style("color", "deepskyblue").
  164. Style("margin", "0 6px").
  165. Text("func (c "+c.name()+") Render() app.UI"),
  166. ),
  167. )
  168. }
  169. // Update triggers a component appearance update. It should be called when a
  170. // field used to render the component has been modified. Updates are always
  171. // performed on the UI goroutine.
  172. func (c *Compo) Update() {
  173. c.dispatch(func(Context) {})
  174. }
  175. // ResizeContent triggers OnResize() on all the component children that
  176. // implement the Resizer interface.
  177. func (c *Compo) ResizeContent() {
  178. c.dispatch(func(Context) {
  179. c.root.onComponentEvent(resize{})
  180. })
  181. }
  182. // ValueTo stores the value of the DOM element (if exists) that emitted an event
  183. // into the given value.
  184. //
  185. // The given value must be a pointer to a signed integer, unsigned integer, or a
  186. // float.
  187. //
  188. // It panics if the given value is not a pointer.
  189. func (c *Compo) ValueTo(v any) EventHandler {
  190. return func(ctx Context, e Event) {
  191. value := ctx.JSSrc().Get("value")
  192. if err := stringTo(value.String(), v); err != nil {
  193. Log(errors.New("storing dom element value failed").Wrap(err))
  194. return
  195. }
  196. }
  197. }
  198. func (c *Compo) name() string {
  199. name := reflect.TypeOf(c.self()).String()
  200. name = strings.ReplaceAll(name, "main.", "")
  201. return name
  202. }
  203. func (c *Compo) self() UI {
  204. return c.this
  205. }
  206. func (c *Compo) setSelf(v UI) {
  207. if v != nil {
  208. c.this = v.(Composer)
  209. return
  210. }
  211. c.this = nil
  212. }
  213. func (c *Compo) getContext() context.Context {
  214. return c.ctx
  215. }
  216. func (c *Compo) getDispatcher() Dispatcher {
  217. return c.disp
  218. }
  219. func (c *Compo) getAttributes() attributes {
  220. return nil
  221. }
  222. func (c *Compo) getEventHandlers() eventHandlers {
  223. return nil
  224. }
  225. func (c *Compo) getParent() UI {
  226. return c.parentElem
  227. }
  228. func (c *Compo) setParent(p UI) {
  229. c.parentElem = p
  230. }
  231. func (c *Compo) getChildren() []UI {
  232. return []UI{c.root}
  233. }
  234. func (c *Compo) mount(d Dispatcher) error {
  235. if c.Mounted() {
  236. return errors.New("mounting component failed").
  237. Tag("reason", "already mounted").
  238. Tag("name", c.name()).
  239. Tag("kind", c.Kind())
  240. }
  241. if initializer, ok := c.self().(Initializer); ok && !d.isServerSide() {
  242. initializer.OnInit()
  243. }
  244. c.disp = d
  245. c.ctx, c.ctxCancel = context.WithCancel(context.Background())
  246. root := c.render()
  247. if err := mount(d, root); err != nil {
  248. return errors.New("mounting component failed").
  249. Tag("name", c.name()).
  250. Tag("kind", c.Kind()).
  251. Wrap(err)
  252. }
  253. root.setParent(c.this)
  254. c.root = root
  255. if c.getDispatcher().isServerSide() {
  256. return nil
  257. }
  258. if mounter, ok := c.self().(Mounter); ok {
  259. c.dispatch(mounter.OnMount)
  260. return nil
  261. }
  262. c.dispatch(nil)
  263. return nil
  264. }
  265. func (c *Compo) dismount() {
  266. c.ctxCancel()
  267. dismount(c.root)
  268. if dismounter, ok := c.this.(Dismounter); ok {
  269. dismounter.OnDismount()
  270. }
  271. }
  272. func (c *Compo) canUpdateWith(v UI) bool {
  273. return c.Mounted() &&
  274. c.Kind() == v.Kind() &&
  275. c.name() == v.name()
  276. }
  277. func (c *Compo) updateWith(v UI) error {
  278. if c.self() == v {
  279. return nil
  280. }
  281. if !c.canUpdateWith(v) {
  282. return errors.New("cannot update component with given element").
  283. Tag("current", reflect.TypeOf(c.self())).
  284. Tag("new", reflect.TypeOf(v))
  285. }
  286. aval := reflect.Indirect(reflect.ValueOf(c.self()))
  287. bval := reflect.Indirect(reflect.ValueOf(v))
  288. compotype := reflect.ValueOf(c).Elem().Type()
  289. haveModifiedFields := false
  290. for i := 0; i < aval.NumField(); i++ {
  291. a := aval.Field(i)
  292. b := bval.Field(i)
  293. if a.Type() == compotype {
  294. continue
  295. }
  296. if !a.CanSet() {
  297. continue
  298. }
  299. if !reflect.DeepEqual(a.Interface(), b.Interface()) {
  300. a.Set(b)
  301. haveModifiedFields = true
  302. }
  303. }
  304. if !haveModifiedFields {
  305. return nil
  306. }
  307. if err := c.updateRoot(); err != nil {
  308. return errors.New("updating root failed").Wrap(err)
  309. }
  310. if updater, ok := c.self().(Updater); ok {
  311. c.dispatch(updater.OnUpdate)
  312. }
  313. c.getDispatcher().removeComponentUpdate(c.this)
  314. return nil
  315. }
  316. func (c *Compo) dispatch(fn func(Context)) {
  317. c.getDispatcher().Dispatch(Dispatch{
  318. Mode: Update,
  319. Source: c.self(),
  320. Function: fn,
  321. })
  322. }
  323. func (c *Compo) updateRoot() error {
  324. a := c.root
  325. b := c.render()
  326. if canUpdate(a, b) {
  327. return update(a, b)
  328. }
  329. return c.replaceRoot(b)
  330. }
  331. func (c *Compo) replaceRoot(v UI) error {
  332. old := c.root
  333. new := v
  334. if err := mount(c.getDispatcher(), new); err != nil {
  335. return errors.New("replacing component root failed").
  336. Tag("kind", c.Kind()).
  337. Tag("name", c.name()).
  338. Tag("root-kind", old.Kind()).
  339. Tag("root-name", old.name()).
  340. Tag("new-root-kind", new.Kind()).
  341. Tag("new-root-name", new.name()).
  342. Wrap(err)
  343. }
  344. var parent UI
  345. for {
  346. parent = c.getParent()
  347. if parent == nil || parent.Kind() == HTML {
  348. break
  349. }
  350. }
  351. if parent == nil {
  352. return errors.New("replacing component root failed").
  353. Tag("kind", c.Kind()).
  354. Tag("name", c.name()).
  355. Tag("reason", "coponent does not have html element parents")
  356. }
  357. c.root = new
  358. new.setParent(c.self())
  359. oldjs := old.JSValue()
  360. newjs := v.JSValue()
  361. parent.JSValue().replaceChild(newjs, oldjs)
  362. dismount(old)
  363. return nil
  364. }
  365. func (c *Compo) render() UI {
  366. elems := FilterUIElems(c.this.Render())
  367. return elems[0]
  368. }
  369. func (c *Compo) preRender(p Page) {
  370. c.root.preRender(p)
  371. if initializer, ok := c.self().(Initializer); ok {
  372. initializer.OnInit()
  373. }
  374. if preRenderer, ok := c.self().(PreRenderer); ok {
  375. c.dispatch(preRenderer.OnPreRender)
  376. }
  377. }
  378. func (c *Compo) onComponentEvent(le any) {
  379. switch le := le.(type) {
  380. case nav:
  381. c.onNav(le)
  382. case appUpdate:
  383. c.onAppUpdate(le)
  384. case appInstallChange:
  385. c.onAppInstallChange(le)
  386. case resize:
  387. c.onResize(le)
  388. }
  389. c.root.onComponentEvent(le)
  390. }
  391. func (c *Compo) onNav(n nav) {
  392. if nav, ok := c.self().(Navigator); ok {
  393. c.dispatch(nav.OnNav)
  394. return
  395. }
  396. }
  397. func (c *Compo) onAppUpdate(au appUpdate) {
  398. if updater, ok := c.self().(AppUpdater); ok {
  399. c.dispatch(updater.OnAppUpdate)
  400. }
  401. }
  402. func (c *Compo) onAppInstallChange(ai appInstallChange) {
  403. if installer, ok := c.self().(AppInstaller); ok {
  404. c.dispatch(installer.OnAppInstallChange)
  405. }
  406. }
  407. func (c *Compo) onResize(r resize) {
  408. if resizer, ok := c.self().(Resizer); ok {
  409. c.dispatch(resizer.OnResize)
  410. return
  411. }
  412. }
  413. func (c *Compo) html(w io.Writer) {
  414. if c.root == nil {
  415. c.root = c.render()
  416. c.root.setSelf(c.root)
  417. }
  418. c.root.html(w)
  419. }
  420. func (c *Compo) htmlWithIndent(w io.Writer, indent int) {
  421. if c.root == nil {
  422. c.root = c.render()
  423. c.root.setSelf(c.root)
  424. }
  425. c.root.htmlWithIndent(w, indent)
  426. }