reactea

package module
v0.4.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 7, 2022 License: MIT Imports: 3 Imported by: 15

README

Reactea

Latest build Codecov Go Reference Go Report Card

Rather simple Bubble Tea companion for handling hierarchy and support for lifting state up.
It Reactifies Bubble Tea philosophy and makes it especially easy to work with in bigger projects.

For me, personally - It's a must in project with multiple pages and component communication

Check our quickstart right here or other examples here!

Installation

go get -u github.com/londek/reactea

General info

The goal is to create components which are

  • dimensions-aware (especially unify all setSize conventions)
  • propful
  • easy to lift the state up
  • able to communicate with parent without importing it (I spent too many hours solving import cycles hehe)
  • easier to code
  • all of that without code duplication

The extreme performance is not main goal of this package, because either way Bubble Tea
refresh rate is only 60hz and 50 allocations in entire runtime won't really hurt anyone.
Most info is currently in source code so I suggest checking it out

Always return reactea.Destroy instead of tea.Quit in order to follow our convention

As of now Go doesn't support type aliases for generics, so Renderer[TProps] has to be explicitly casted.
It's planned for Go 1.20

Quickstart

Reactea unlike Bubble Tea implements two-way communication, very React-like communication.
If you have experience with React you are gonna love Reactea straight away!

While it may look in following tutorial that Reactea overcomplicates things, trust me, for major projects it's a lifesaver!

In this tutorial we are going to make application that consists of 2 pages.

  • The input (aka index, in reactea default) page for inputting your name
  • The displayname page for displaying your name
Lifecycle

More detailed docs about component lifecycle can be found here, we are only gonna go through basics.

Reactea component lifecycle consists of 6 methods (while Bubble Tea only 3)

Method Purpose
Init(TProps) tea.Cmd It's called first. All critical stuff should happen here. It also supports IO through tea.Cmd
Update(tea.Msg) tea.Cmd It reacts to Bubble Tea IO and should update state accordingly
AfterUpdate() tea.Cmd It's called after root component finishes Update(). Components should queue themselves
Render(int, int) string It renders the UI. The two arguments are width and height, they should be calculated by parent
Destroy() It's called whenever Component is about to be GC-ed. Please note that it's parent's responsibility to call Destroy()
UpdateProps(TProps) Derives state from given properties. Usually called from Init()

Your first application should consist only of Update and Render, all other methods will be implemented by reactea.BasicComponent and reactea.BasicPropfulComponent.

Let's get to work!

The input page

/pages/input/input.go

type Component struct {
    reactea.BasicComponent                // It implements AfterUpdate() for us, so we don't have to care!
    reactea.BasicPropfulComponent[Props]  // It implements props backend - UpdateProps() and Props()

    textinput textinput.Model             // Input for inputting name
}

type Props struct {
    SetText func(string)  // SetText function for lifting state up
}

func New() *Component {
    return &Component{textinput: textinput.New()}
}

func (c *Component) Init(props Props) tea.Cmd {
    // Always derive props in Init()! If you are not replacing Init(),
    // reactea.BasicPropfulComponent will take care of it
    c.UpdateProps(props)
    
    return c.textinput.Focus()
}

func (c *Component) Update(msg tea.Msg) tea.Cmd {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        if msg.Type == tea.KeyEnter {
            // Lifted state power! Woohooo
            c.Props().SetText(c.textinput.Value())

            // Navigate to displayname, please
            reactea.SetCurrentRoute("displayname")
            return nil
        }
    }

    var cmd tea.Cmd
    c.textinput, cmd = c.textinput.Update(msg)
    return cmd
}

// Here we are not using width and height, but you can!
func (c *Component) Render(int, int) string {
    return fmt.Sprintf("Enter your name: %s\nAnd press [ Enter ]", c.textinput.View())
}
The displayname page

/pages/displayname/displayname.go

import (
 "fmt"
)

// Our prop(s) is a string itself!
type Props = string

// Stateless components?!?!
func Renderer(text Props, width, height int) string {
    return fmt.Sprintf("OMG! Hello %s!", text)
}
Main component

/app/app.go

type Component struct {
    reactea.BasicComponent                          // It implements AfterUpdate()
    reactea.BasicPropfulComponent[reactea.NoProps]  // It implements props backend - UpdateProps() and Props()

    mainRouter reactea.Component[router.Props]      // Our router

    text string // The name
}

func New() *Component {
    return &Component{
        mainRouter: router.New(),
    }
}

func (c *Component) Init(reactea.NoProps) tea.Cmd {
    // Does it remind you of something? react-router!
    return c.mainRouter.Init(map[string]router.RouteInitializer{
        "default": func(router.Params) (reactea.SomeComponent, tea.Cmd) {
            component := input.New()

            return component, component.Init(input.Props{
                SetText: c.setText, // Can also use "lambdas" (function can be created here)
            })
        },
        "displayname": func(router.Params) (reactea.SomeComponent, tea.Cmd) {
            // RouteInitializer wants SomeComponent so we have to convert
            // Stateless component (renderer) to Component
            component := reactea.Componentify[string](displayname.Renderer)

            return component, component.Init(c.text)
        },
    })
}

func (c *Component) Update(msg tea.Msg) tea.Cmd {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        // ctrl+c support 
        if msg.String() == "ctrl+c" {
            return reactea.Destroy
        }
    }

    return c.mainRouter.Update(msg)
}

func (c *Component) Render(width, height int) string {
    return c.mainRouter.Render(width, height)
}

func (c *Component) setText(text string) {
    c.text = text
}
Main

main.go

// reactea.NewProgram initializes program with
// "translation layer", so Reactea components work
program := reactea.NewProgram(app.New())

if err := program.Start(); err != nil {
    panic(err)
}

Component lifecycle

Component lifecycle image

Reactea component lifecycle consists of 6 methods (while Bubble Tea only 3)

Method Purpose
Init(TProps) tea.Cmd It's called first. All critical stuff should happen here. It also supports IO through tea.Cmd
Update(tea.Msg) tea.Cmd It reacts to Bubble Tea IO and should update state accordingly
AfterUpdate() tea.Cmd It's called after root component finishes Update(). Components should queue themselves
Render(int, int) string It renders the UI. The two arguments are width and height, they should be calculated by parent
Destroy() It's called whenever Component is about to be GC-ed. Please note that it's parent's responsibility to call Destroy()
UpdateProps(TProps) Derives state from given properties. Usually called from Init()

Reactea takes pointer approach for components making state modifiable in any lifecycle method
There are also 2 additional lifecycle methods: AfterUpdate() and UpdateProps()

AfterUpdate()

AfterUpdate() is the only lifecycle method that is not controlled by parent. It's called right after root component finishes Update(). Components should queue itself with reactea.AfterUpdate(component) in Update()

UpdateProps()

UpdateProps() is a lifecycle method that derives state from props, It can happen anytime during lifecycle. Usually called by Init()

Notes

Update() IS NOT guaranteed to be called on first-run, Init() for most part is, and critical logic should be there

Lifecycle is (almost, see AfterUpdate()) fully controlled by parent component making graph above fully theoretical and possibly invalid for third-party components

Stateless components

Stateless components are represented by following function types

Renderer[TProps any] ProplessRenderer DumbRenderer
Properties
Dimensions
Arguments TProps, int, int int, int

There are many utility functions for transforming stateless into stateful components or for rendering any component without knowing its type (reactea.RenderAny)

Routes API

Routes API allows developers for easy development of multi-page apps. They are kind of substitute for window.Location inside Bubble Tea

reactea.CurrentRoute() Route

Returns current route

reactea.LastRoute() Route

Returns last route

reactea.WasRouteChanged() bool

returns LastRoute() != CurrentRoute()

Reactea Routes now support params

Params have been introduced in order to allow routes like: teams/123/player/4

Params have to follow regex ^:.*$
^ being beginning of current path level (/^level/)
$being end of current path level (/level$/)

Note that params support wildcards with single :, like teams/:/player. teams/123/player, teams/456/player etc will be matched no matter what and param will be ignored in param map.

Router Component

Router Component is basic implementation of how routing could look in your application. It doesn't support wildcards yet or relative pathing. All data is provided from within props

router.Props

router.Props is a map of route initializers keyed by routes

What is RouteInitializer?

RouteInitializer is function that initializes the current route component

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AfterUpdate

func AfterUpdate(afterUpdater AfterUpdater)

Queue component for AfterUpdate event

func CurrentRoute

func CurrentRoute() string

func Destroy

func Destroy() tea.Msg

Destroys app before quiting

func New

func New(root Component[NoProps]) model

func NewProgram added in v0.3.0

func NewProgram(root Component[NoProps], options ...tea.ProgramOption) *tea.Program

It simplifies tea.NewProgram(reactea.New(root), opts...) to reactea.NewProgram(root, opts...)

Note: Return type is *tea.Program, Reactea doesn't have it's own wrapper (reactea.Program) type, yet (?)

func RenderAny

func RenderAny[TProps any, TRenderer AnyRenderer[TProps]](renderer TRenderer, props TProps, width, height int) (result string)

Renders all AnyRenderers in one function

Note: If you are using ProplessRenderer/DumbRenderer just pass reactea.NoProps{} or struct{}{}

Note: Using named return type for 100% coverage

func RouteMatchesPlaceholder added in v0.4.0

func RouteMatchesPlaceholder(route string, placeholder string) (map[string]string, bool)

Checks whether route (e.g. teams/123/12) matches placeholder (e.g teams/:teamId/:playerId) Returns map of found params and if it matches Params have to follow regex ^:.*$ ^ being beginning of current path level (/^level/) $ being end of current path level (/level$/)

Note: Entire matched route can be accessed with key "$" Note: Placeholders can be optional => foo/?:/?: will match foo/bar and foo and foo/bar/baz Note: The most outside placeholders can be optional recursive => foo/+?: will match foo/bar and foo and foo/bar/baz Note: It allows for defining wildcards with foo/:/bar Note: Duplicate params will result in overwrite of first param

func SetCurrentRoute

func SetCurrentRoute(newRoute string)

func WasRouteChanged

func WasRouteChanged() bool

func WithRoute added in v0.3.0

func WithRoute(route string) func(*tea.Program)

func WithoutInput added in v0.3.1

func WithoutInput() func(*tea.Program)

Useful for testing on Github Actions, by default Bubble Tea would try reading from /dev/tty, but on Github Actions it's restricted resulting in error

Types

type AfterUpdater

type AfterUpdater interface {
	AfterUpdate() tea.Cmd
}

type AnyProplessRenderer added in v0.2.0

type AnyProplessRenderer interface {
	ProplessRenderer | DumbRenderer
}

type AnyRenderer added in v0.2.0

type AnyRenderer[TProps any] interface {
	func(TProps, int, int) string | AnyProplessRenderer
}

Why not Renderer[TProps]? It would have to be type alias there are no type aliases yet for generics, but they are planned for Go 1.20. Something to keep in mind for future

type BasicComponent

type BasicComponent struct{}

The most basic form of Reactea Component It implements all not required methods so you don't have to

func (*BasicComponent) AfterUpdate

func (c *BasicComponent) AfterUpdate() tea.Cmd

func (*BasicComponent) Destroy

func (c *BasicComponent) Destroy()

func (*BasicComponent) Update

func (c *BasicComponent) Update(msg tea.Msg) tea.Cmd

type BasicPropfulComponent

type BasicPropfulComponent[TProps any] struct {
	// contains filtered or unexported fields
}

Stores props in struct. If you want to derive state from UpdateProps() you're probably looking at wrong thing

func (*BasicPropfulComponent[TProps]) Init

func (c *BasicPropfulComponent[TProps]) Init(props TProps) tea.Cmd

func (*BasicPropfulComponent[TProps]) Props

func (c *BasicPropfulComponent[TProps]) Props() TProps

func (*BasicPropfulComponent[TProps]) UpdateProps

func (c *BasicPropfulComponent[TProps]) UpdateProps(props TProps)

type Component

type Component[TProps any] interface {

	// You always have to initialize component with some kind of
	// props - it can even be zero value
	// Init() Is meant to both initialize subcomponents and run
	// long IO operations through tea.Cmd
	Init(TProps) tea.Cmd

	// It's called when component is about to be destroyed
	//
	// Note: It's parent component's job to call it so
	// relying on it outside of Reactea builtins is
	// not reliable
	Destroy()

	// Typical tea.Model Update(), we handle all IO events here
	Update(tea.Msg) tea.Cmd

	// Callee already knows at which size should it render at
	Render(int, int) string

	// It's an Update() but for props, the state derive stage
	// happens here
	UpdateProps(TProps)

	// AfterUpdate is stage useful for components like routers
	// to prepare content. Saying that you will probably never
	// need to use it
	AfterUpdate() tea.Cmd
}

func Componentify added in v0.2.0

func Componentify[TProps any, TRenderer AnyRenderer[TProps]](renderer TRenderer) Component[TProps]

Componentifies AnyRenderer Returns uninitialized component with renderer taking care of .Render()

type DumbRenderer

type DumbRenderer = func() string

Doesn't have state, props, even scalling for target dimensions = DumbRenderer, or Stringer

type InvisibleComponent

type InvisibleComponent struct{}

Utility component for displaying empty string on Render()

func (*InvisibleComponent) Render

func (c *InvisibleComponent) Render(int, int) string

type NoProps

type NoProps = struct{}

type ProplessRenderer

type ProplessRenderer = func(int, int) string

SUPEEEEEER shorthand for components

func PropfulToLess

func PropfulToLess[TProps any](renderer Renderer[TProps], props TProps) ProplessRenderer

Wraps propful into propless renderer

type Renderer

type Renderer[TProps any] func(TProps, int, int) string

Ultra shorthand for components = just renderer One could say it's a stateless component Also note that it doesn't handle any IO by itself

TODO: Change to type alias after type aliases for generics support is released (planned Go 1.20). For now explicit type conversion is required

type SomeComponent

type SomeComponent interface {
	Destroy()
	Update(tea.Msg) tea.Cmd
	Render(int, int) string
	AfterUpdate() tea.Cmd
}

Interface which is basically reactea.Component just without the props part, use it if you don't care about generics

Directories

Path Synopsis
example module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL