masc

package module
v0.0.0-...-a634514 Latest Latest
Warning

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

Go to latest
Published: Mar 8, 2024 License: BSD-3-Clause, MIT Imports: 10 Imported by: 4

README

masc

Masc combines the state management of Bubble Tea with the Vecty view rendering model. The result is a library for building browser applications in Go using The Elm Architecture.

Vecty components are stateless, or, at least, agnostic about how state is managed.

Bubble Tea models are stateful, or, at least, opinionated about how state should be managed.

Masc models look just like Bubble Tea models, except that they return HTML or other components rather than strings when being rendered. This is just like vecty. The vecty rendering engine is used to update the browser.

Masc components look just like Vecty components, except the Render function takes a func(Msg) parameter. This function, called send by convention, is used to send messages to the running program to update its state.

Stateless components implement the Component interface, i.e. have a Render(func(Msg) ComponentOrHTML function.

Models are Components that also implement the Model interface, i.e. have Init() Cmd and Update(Msg) (Model, Cmd) functions.

That is, models are stateful components.

Example

Here's a basic Hello World example.

package main

import (
	"github.com/octoberswimmer/masc"
	"github.com/octoberswimmer/masc/elem"
)

func main() {
	masc.SetTitle("Hello Vecty!")
	m := &PageView{}
	pgm := masc.NewProgram(m)
	_, err := pgm.Run()
	if err != nil {
		panic(err)
	}
}

// PageView is our main page component.
type PageView struct {
	masc.Core
}

func (p *PageView) Init() masc.Cmd {
	return nil
}

func (p *PageView) Update(msg masc.Msg) (masc.Model, masc.Cmd) {
	return p, nil
}

// Render implements the masc.Component interface.
func (p *PageView) Render(send func(masc.Msg)) masc.ComponentOrHTML {
	return elem.Body(
		masc.Text("Hello Vecty!"),
	)
}

Additional examples, including a todo app, are in the example directory. These can be run using wasmserve.

Documentation

Overview

Package masc provides a framework for building browser applications based on the paradigms of The Elm Architecture. It combines the state management of Bubble Tea with the Vecty view rendering model.

Example programs can be found at https://github.com/octoberswimmer/masc/tree/main/example

Index

Constants

This section is empty.

Variables

View Source
var ErrProgramKilled = errors.New("program was killed")

ErrProgramKilled is returned by Program.Run when the program got killed.

Functions

func AddStylesheet

func AddStylesheet(url string)

AddStylesheet adds an external stylesheet to the document.

func RenderBody

func RenderBody(body Component, send func(Msg))

RenderBody renders the given component as the document body. The given Component's Render method must return a "body" element or a panic will occur.

This function blocks forever in order to prevent the program from exiting, which would prevent components from rerendering themselves in the future.

It is a short-handed form for writing:

err := masc.RenderInto("body", body, send)
if err !== nil {
	panic(err)
}
select{} // run Go forever

func RenderInto

func RenderInto(selector string, c Component, send func(Msg)) error

RenderInto renders the given component into the existing HTML element found by the CSS selector (e.g. "#id", ".class-name") by replacing it.

If there is more than one element found, the first is used. If no element is found, an error of type InvalidTargetError is returned.

If the Component's Render method does not return an element of the same type, an error of type ElementMismatchError is returned.

func RenderIntoNode

func RenderIntoNode(node SyscallJSValue, c Component, send func(Msg)) error

RenderIntoNode renders the given component into the existing HTML element by replacing it.

If the Component's Render method does not return an element of the same type, an error of type ElementMismatchError is returned.

func SetTitle

func SetTitle(title string)

SetTitle sets the title of the document.

Types

type Applyer

type Applyer interface {
	// Apply applies the markup to the given HTML element or text node.
	Apply(h *HTML)
}

Applyer represents some type of markup (a style, property, data, etc) which can be applied to a given HTML element or text node.

func Attribute

func Attribute(key string, value interface{}) Applyer

Attribute returns an Applyer which applies the given attribute to an element.

In most situations, you should use Property function, or the prop subpackage (which is type-safe) instead. There are only a few attributes (aria-*, role, etc) which do not have equivalent properties. Always opt for the property first, before relying on an attribute.

func Class

func Class(class ...string) Applyer

Class returns an Applyer which applies the provided classes. Subsequent calls to this function will append additional classes. To toggle classes, use ClassMap instead. Each class name must be passed as a separate argument.

func Data

func Data(key, value string) Applyer

Data returns an Applyer which applies the given data attribute.

func ElementKey

func ElementKey(key interface{}) Applyer

Key returns an Applyer that uniquely identifies the HTML element amongst its siblings. When used, all other sibling elements and components must also be keyed.

func MarkupIf

func MarkupIf(cond bool, markup ...Applyer) Applyer

MarkupIf returns nil if cond is false, otherwise it returns the given markup.

func Namespace

func Namespace(uri string) Applyer

Namespace is Applyer which sets the namespace URI to associate with the created element. This is primarily used when working with, e.g., SVG.

See https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS#Valid Namespace URIs

func Property

func Property(key string, value interface{}) Applyer

Property returns an Applyer which applies the given JavaScript property to an HTML element or text node. Generally, this function is not used directly but rather the prop and style subpackages (which are type safe) should be used instead.

To set style, use style package or Style. Property panics if key is "style".

func Style

func Style(key, value string) Applyer

Style returns an Applyer which applies the given CSS style. Generally, this function is not used directly but rather the style subpackage (which is type safe) should be used instead.

func UnsafeHTML

func UnsafeHTML(html string) Applyer

UnsafeHTML is Applyer which unsafely sets the inner HTML of an HTML element.

It is entirely up to the caller to ensure the input HTML is properly sanitized.

It is akin to innerHTML in standard JavaScript and dangerouslySetInnerHTML in React, and is said to be unsafe because Vecty makes no effort to validate or ensure the HTML is safe for insertion in the DOM. If the HTML came from a user, for example, it would create a cross-site-scripting (XSS) exploit in the application.

The returned Applyer can only be applied to HTML, not masc.Text, or else a panic will occur.

type BatchMsg

type BatchMsg []Cmd

BatchMsg is a message used to perform a bunch of commands concurrently with no ordering guarantees. You can send a BatchMsg with Batch.

type ClassMap

type ClassMap map[string]bool

ClassMap is markup that specifies classes to be applied to an element if their boolean value are true.

func (ClassMap) Apply

func (m ClassMap) Apply(h *HTML)

Apply implements the Applyer interface.

type Cmd

type Cmd func() Msg

Cmd is an IO operation that returns a message when it's complete. If it's nil it's considered a no-op. Use it for things like HTTP requests, timers, saving and loading from disk, and so on.

Note that there's almost never a reason to use a command to send a message to another part of your program. That can almost always be done in the update function.

func Batch

func Batch(cmds ...Cmd) Cmd

Batch performs a bunch of commands concurrently with no ordering guarantees about the results. Use a Batch to return several commands.

Example:

    func (m model) Init() Cmd {
	       return tea.Batch(someCommand, someOtherCommand)
    }

func Every

func Every(duration time.Duration, fn func(time.Time) Msg) Cmd

Every is a command that ticks in sync with the system clock. So, if you wanted to tick with the system clock every second, minute or hour you could use this. It's also handy for having different things tick in sync.

Because we're ticking with the system clock the tick will likely not run for the entire specified duration. For example, if we're ticking for one minute and the clock is at 12:34:20 then the next tick will happen at 12:35:00, 40 seconds later.

To produce the command, pass a duration and a function which returns a message containing the time at which the tick occurred.

type TickMsg time.Time

cmd := Every(time.Second, func(t time.Time) Msg {
   return TickMsg(t)
})

Beginners' note: Every sends a single message and won't automatically dispatch messages at an interval. To do that, you'll want to return another Every command after receiving your tick message. For example:

type TickMsg time.Time

// Send a message every second.
func tickEvery() Cmd {
    return Every(time.Second, func(t time.Time) Msg {
        return TickMsg(t)
    })
}

func (m model) Init() Cmd {
    // Start ticking.
    return tickEvery()
}

func (m model) Update(msg Msg) (Model, Cmd) {
    switch msg.(type) {
    case TickMsg:
        // Return your Every command again to loop.
        return m, tickEvery()
    }
    return m, nil
}

Every is analogous to Tick in the Elm Architecture.

func Sequence

func Sequence(cmds ...Cmd) Cmd

Sequence runs the given commands one at a time, in order. Contrast this with Batch, which runs commands concurrently.

func Sequentially deprecated

func Sequentially(cmds ...Cmd) Cmd

Sequentially produces a command that sequentially executes the given commands. The Msg returned is the first non-nil message returned by a Cmd.

func saveStateCmd() Msg {
   if err := save(); err != nil {
       return errMsg{err}
   }
   return nil
}

cmd := Sequentially(saveStateCmd, Quit)

Deprecated: use Sequence instead.

func SetWindowTitle

func SetWindowTitle(title string) Cmd

SetWindowTitle produces a command that sets the terminal title.

For example:

func (m model) Init() Cmd {
    // Set title.
    return tea.SetWindowTitle("My App")
}

func Tick

func Tick(d time.Duration, fn func(time.Time) Msg) Cmd

Tick produces a command at an interval independent of the system clock at the given duration. That is, the timer begins precisely when invoked, and runs for its entire duration.

To produce the command, pass a duration and a function which returns a message containing the time at which the tick occurred.

type TickMsg time.Time

cmd := Tick(time.Second, func(t time.Time) Msg {
   return TickMsg(t)
})

Beginners' note: Tick sends a single message and won't automatically dispatch messages at an interval. To do that, you'll want to return another Tick command after receiving your tick message. For example:

type TickMsg time.Time

func doTick() Cmd {
    return Tick(time.Second, func(t time.Time) Msg {
        return TickMsg(t)
    })
}

func (m model) Init() Cmd {
    // Start ticking.
    return doTick()
}

func (m model) Update(msg Msg) (Model, Cmd) {
    switch msg.(type) {
    case TickMsg:
        // Return your Tick command again to loop.
        return m, doTick()
    }
    return m, nil
}

type Component

type Component interface {
	// Render is responsible for building HTML which represents the component.
	//
	// If Render returns nil, the component will render as nothing (in reality,
	// a noscript tag, which has no display or action, and is compatible with
	// Vecty's diffing algorithm).
	Render(send func(Msg)) ComponentOrHTML

	// Context returns the components context, which is used internally by
	// Vecty in order to store the previous component render for diffing.
	Context() *Core
	// contains filtered or unexported methods
}

Component represents a single visual component within an application. To define a new component simply implement the Render method and embed the Core struct:

type MyComponent struct {
	masc.Core
	... additional component fields (state or properties) ...
}

func (c *MyComponent) Render() masc.ComponentOrHTML {
	... rendering ...
}

type ComponentOrHTML

type ComponentOrHTML interface {
	// contains filtered or unexported methods
}

ComponentOrHTML represents one of:

Component
*HTML
List
KeyedList
nil

An unexported method on this interface ensures at compile time that the underlying value must be one of these types.

type Copier

type Copier interface {
	// Copy returns a copy of the component.
	Copy() Component
}

Copier is an optional interface that a Component can implement in order to copy itself. Vecty must internally copy components, and it does so by either invoking the Copy method of the Component or, if the component does not implement the Copier interface, a shallow copy is performed.

TinyGo: If compiling your Vecty application using the experimental TinyGo support (https://github.com/hexops/vecty/pull/243) then all components must implement at least a shallow-copy Copier interface (this is not required otherwise):

func (c *MyComponent) Copy() masc.Component {
	cpy := *c
	return &cpy
}

type Core

type Core struct {
	// contains filtered or unexported fields
}

Core implements the Context method of the Component interface, and is the core/central struct which all Component implementations should embed.

func (*Core) Context

func (c *Core) Context() *Core

Context implements the Component interface.

type ElementMismatchError

type ElementMismatchError struct {
	// contains filtered or unexported fields
}

ElementMismatchError is returned when the element returned by a component does not match what is required for rendering.

func (ElementMismatchError) Error

func (e ElementMismatchError) Error() string

type Event

type Event struct {
	Value  SyscallJSValue
	Target SyscallJSValue
}

Event represents a DOM event.

type EventListener

type EventListener struct {
	Name     string
	Listener func(*Event)
	// contains filtered or unexported fields
}

EventListener is markup that specifies a callback function to be invoked when the named DOM event is fired.

func (*EventListener) Apply

func (l *EventListener) Apply(h *HTML)

Apply implements the Applyer interface.

func (*EventListener) PreventDefault

func (l *EventListener) PreventDefault() *EventListener

PreventDefault prevents the default behavior of the event from occurring.

See https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault.

func (*EventListener) StopPropagation

func (l *EventListener) StopPropagation() *EventListener

StopPropagation prevents further propagation of the current event in the capturing and bubbling phases.

See https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation.

type HTML

type HTML struct {
	// contains filtered or unexported fields
}

HTML represents some form of HTML: an element with a specific tag, or some literal text (a TextNode).

func Tag

func Tag(tag string, m ...MarkupOrChild) *HTML

Tag returns an HTML element with the given tag name. Generally, this function is not used directly but rather the elem subpackage (which is type safe) is used instead.

func Text

func Text(text string, m ...MarkupOrChild) *HTML

Text returns a TextNode with the given literal text. Because the returned HTML represents a TextNode, the text does not have to be escaped (arbitrary user input fed into this function will always be safely rendered).

func (*HTML) Key

func (h *HTML) Key() interface{}

Key implements the Keyer interface.

func (*HTML) Node

func (h *HTML) Node() SyscallJSValue

Node returns the underlying JavaScript Element or TextNode.

It panics if it is called before the DOM node has been attached, i.e. before the associated component's Mounter interface would be invoked.

type InvalidTargetError

type InvalidTargetError struct {
	// contains filtered or unexported fields
}

InvalidTargetError is returned when the element targeted by a render is invalid because it is null or undefined.

func (InvalidTargetError) Error

func (e InvalidTargetError) Error() string

type KeyedList

type KeyedList struct {
	// contains filtered or unexported fields
}

KeyedList is produced by calling List.WithKey. It has no public behaviour, and List members are no longer accessible once wrapped in this stucture.

func (KeyedList) Key

func (l KeyedList) Key() interface{}

Key implements the Keyer interface

type Keyer

type Keyer interface {
	// Key returns a value that uniquely identifies the component amongst its
	// siblings. The returned type must be a valid map key, or rendering will
	// panic.
	Key() interface{}
}

Keyer is an optional interface that a Component can implement in order to uniquely identify the component amongst its siblings. If implemented, all siblings, both components and HTML, must also be keyed.

Implementing this interface allows siblings to be removed or re-ordered whilst retaining state, and improving render efficiency.

type List

type List []ComponentOrHTML

List represents a list of components or HTML.

func (List) WithKey

func (l List) WithKey(key interface{}) KeyedList

WithKey wraps the List in a Keyer using the given key. List members are inaccessible within the returned value.

type MarkupList

type MarkupList struct {
	// contains filtered or unexported fields
}

MarkupList represents a list of Applyer which is individually applied to an HTML element or text node.

It may only be created through the Markup function.

func Markup

func Markup(m ...Applyer) MarkupList

Markup wraps a list of Applyer which is individually applied to an HTML element or text node.

func (MarkupList) Apply

func (m MarkupList) Apply(h *HTML)

Apply implements the Applyer interface.

type MarkupOrChild

type MarkupOrChild interface {
	// contains filtered or unexported methods
}

MarkupOrChild represents one of:

Component
*HTML
List
KeyedList
nil
MarkupList

An unexported method on this interface ensures at compile time that the underlying value must be one of these types.

func If

func If(cond bool, children ...ComponentOrHTML) MarkupOrChild

If returns nil if cond is false, otherwise it returns the given children.

type Model

type Model interface {
	Component
	// Init is the first function that will be called. It returns an optional
	// initial command. To not perform an initial command return nil.
	Init() Cmd

	// Update is called when a message is received. Use it to inspect messages
	// and, in response, update the model and/or send a command.
	Update(Msg) (Model, Cmd)
}

Model contains the program's state as well as its core functions.

type Mounter

type Mounter interface {
	// Mount is called after the component has been mounted, after the DOM node
	// has been attached.
	Mount()
}

Mounter is an optional interface that a Component can implement in order to receive component mount events.

type Msg

type Msg interface{}

Msg contain data from the result of a IO operation. Msgs trigger the update function and, henceforth, the UI.

func Quit

func Quit() Msg

Quit is a special command that tells the Bubble Tea program to exit.

type Program

type Program struct {
	// contains filtered or unexported fields
}

Program is a terminal user interface.

func NewProgram

func NewProgram(model Model, opts ...ProgramOption) *Program

NewProgram creates a new Program.

func (*Program) Kill

func (p *Program) Kill()

Kill stops the program immediately and restores the former terminal state. The final render that you would normally see when quitting will be skipped. [program.Run] returns a ErrProgramKilled error.

func (*Program) Quit

func (p *Program) Quit()

Quit is a convenience function for quitting Bubble Tea programs. Use it when you need to shut down a Bubble Tea program from the outside.

If you wish to quit from within a Bubble Tea program use the Quit command.

If the program is not running this will be a no-op, so it's safe to call if the program is unstarted or has already exited.

func (*Program) Run

func (p *Program) Run() (Model, error)

Run initializes the program and runs its event loops, blocking until it gets terminated by either Program.Quit, Program.Kill, or its signal handler. Returns the final model.

func (*Program) Send

func (p *Program) Send(msg Msg)

Send sends a message to the main update function, effectively allowing messages to be injected from outside the program for interoperability purposes.

If the program hasn't started yet this will be a blocking operation. If the program has already been terminated this will be a no-op, so it's safe to send messages after the program has exited.

func (*Program) Start deprecated

func (p *Program) Start() error

Start initializes the program and runs its event loops, blocking until it gets terminated by either Program.Quit, Program.Kill, or its signal handler.

Deprecated: please use Program.Run instead.

func (*Program) StartReturningModel deprecated

func (p *Program) StartReturningModel() (Model, error)

StartReturningModel initializes the program and runs its event loops, blocking until it gets terminated by either Program.Quit, Program.Kill, or its signal handler. Returns the final model.

Deprecated: please use Program.Run instead.

func (*Program) Wait

func (p *Program) Wait()

Wait waits/blocks until the underlying Program finished shutting down.

type ProgramOption

type ProgramOption func(*Program)

ProgramOption is used to set options when initializing a Program. Program can accept a variable number of options.

Example usage:

p := NewProgram(model, WithInput(someInput), WithOutput(someOutput))

func RenderTo

func RenderTo(rootNode SyscallJSValue) ProgramOption

RenderTo configures the renderer to render the model to the passed DOM node.

func WithContext

func WithContext(ctx context.Context) ProgramOption

WithContext lets you specify a context in which to run the Program. This is useful if you want to cancel the execution from outside. When a Program gets cancelled it will exit with an error ErrProgramKilled.

func WithFilter

func WithFilter(filter func(Model, Msg) Msg) ProgramOption

WithFilter supplies an event filter that will be invoked before Bubble Tea processes a tea.Msg. The event filter can return any tea.Msg which will then get handled by Bubble Tea instead of the original event. If the event filter returns nil, the event will be ignored and Bubble Tea will not process it.

As an example, this could be used to prevent a program from shutting down if there are unsaved changes.

Example:

func filter(m tea.Model, msg tea.Msg) tea.Msg {
	if _, ok := msg.(tea.QuitMsg); !ok {
		return msg
	}

	model := m.(myModel)
	if model.hasChanges {
		return nil
	}

	return msg
}

p := tea.NewProgram(Model{}, tea.WithFilter(filter));

if _,err := p.Run(); err != nil {
	fmt.Println("Error running program:", err)
	os.Exit(1)
}

func WithoutCatchPanics

func WithoutCatchPanics() ProgramOption

WithoutCatchPanics disables the panic catching that Bubble Tea does by default. If panic catching is disabled the terminal will be in a fairly unusable state after a panic because Bubble Tea will not perform its usual cleanup on exit.

func WithoutRenderer

func WithoutRenderer() ProgramOption

WithoutRenderer disables the renderer. When this is set output and log statements will be plainly sent to stdout (or another output if one is set) without any rendering and redrawing logic. In other words, printing and logging will behave the same way it would in a non-TUI commandline tool. This can be useful if you want to use the Bubble Tea framework for a non-TUI application, or to provide an additional non-TUI mode to your Bubble Tea programs. For example, your program could behave like a daemon if output is not a TTY.

func WithoutSignalHandler

func WithoutSignalHandler() ProgramOption

WithoutSignalHandler disables the signal handler that Bubble Tea sets up for Programs. This is useful if you want to handle signals yourself.

func WithoutSignals

func WithoutSignals() ProgramOption

WithoutSignals will ignore OS signals. This is mainly useful for testing.

type QuitMsg

type QuitMsg struct{}

QuitMsg signals that the program should quit. You can send a QuitMsg with Quit.

type RenderSkipper

type RenderSkipper interface {
	// SkipRender is called with a copy of the Component made the last time its
	// Render method was invoked. If it returns true, rendering of the
	// component will be skipped.
	//
	// The previous component may be of a different type than this
	// RenderSkipper itself, thus a type assertion should be used and no action
	// taken if the type does not match.
	SkipRender(prev Component) bool
}

RenderSkipper is an optional interface that Component's can implement in order to short-circuit the reconciliation of a Component's rendered body.

This is purely an optimization, and does not need to be implemented by Components for correctness. Without implementing this interface, only the difference between renders will be applied to the browser DOM. This interface allows components to bypass calculating the difference altogether and quickly state "nothing has changed, do not re-render".

type SyscallJSValue

type SyscallJSValue jsObject

SyscallJSValue is an actual syscall/js.Value type under WebAssembly compilation.

It is declared here just for purposes of testing Vecty under native 'go test', linting, and serving documentation under godoc.org.

type Unmounter

type Unmounter interface {
	// Unmount is called before the component has been unmounted, before the
	// DOM node has been removed.
	Unmount()
}

Unmounter is an optional interface that a Component can implement in order to receive component unmount events.

Directories

Path Synopsis
Package elem defines markup to create DOM elements.
Package elem defines markup to create DOM elements.
Package event defines markup to bind DOM events.
Package event defines markup to bind DOM events.
example module

Jump to

Keyboard shortcuts

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