Tutorial
バックエンドとフロントエンドを通信させる
Marionetteでは、Goのハンドラが状態を更新し、htmxが必要なHTML断片だけを受け取って画面に差し替えます。 この回では、メッセージ投稿フォームを作りながら通信の流れを学びます。
Step 1
通信の全体像を理解する
基本の流れは4つです。
app.Pageが最初の画面全体を返すActionFormがフォーム送信をhtmxリクエストにするapp.Actionがバックエンドで入力を受け取り、状態を更新するRegionの中身だけが新しいHTMLで差し替わる
Step 2
バックエンドに状態を持たせる
app.SetGlobal で全ユーザー共有のアプリケーション状態を初期化し、ハンドラ内では ctx.GetGlobal と ctx.SetGlobal でその共有 state を読み書きします。
これは小さな共有デモには便利ですが、複数ユーザー前提の業務アプリでは、業務データを App global state に置かず repository の背後に置くのが基本形です。 複数ユーザーのタスク管理サンプル は、「global state を使わない業務アプリの基本形」として参照できます。
app := mb.New()
app.SetGlobal("messages", []string{}) Step 3
最初の画面を作る
app.Page は通常のGETページです。ここではフォームと、あとで差し替える Region を同じ画面に配置します。
app.Page("/", func(ctx *mb.Context) mf.Node {
return page(ctx)
}) Step 4
フォームからActionへ送る
ActionForm はHTMLの form にhtmx属性を付けます。Target には差し替えたい要素のCSSセレクタを指定します。
mf.ActionForm(mf.ActionFormProps{
Action: "/messages/create",
Target: "#messages",
Swap: "innerHTML",
}) Step 5
Actionで入力を受け取る
app.Action はPOSTされたフォーム値を読み取り、状態を更新し、差し替え用のHTML断片を返します。
app.Action("messages/create", func(ctx *mb.Context) mf.Node {
text := strings.TrimSpace(ctx.FormValue("message"))
if text != "" {
ctx.UpdateGlobal("messages", func(old any) any {
messages := cloneMessages(old)
return append([]string{text}, messages...)
})
}
messages := ctx.GetGlobalSnapshot("messages", cloneMessages).([]string)
return messageList(messages)
}) Step 6
全体のコード
main.go を次の内容に置き換えます。
package main
import (
"strings"
mb "github.com/YoshihideShirai/marionette/backend"
mf "github.com/YoshihideShirai/marionette/frontend"
)
func main() {
app := mb.New()
app.SetGlobal("messages", []string{})
app.Page("/", func(ctx *mb.Context) mf.Node {
return page(ctx)
})
app.Action("messages/create", func(ctx *mb.Context) mf.Node {
text := strings.TrimSpace(ctx.FormValue("message"))
if text != "" {
ctx.UpdateGlobal("messages", func(old any) any {
messages := cloneMessages(old)
return append([]string{text}, messages...)
})
}
messages := ctx.GetGlobalSnapshot("messages", cloneMessages).([]string)
return messageList(messages)
})
if err := app.Run("127.0.0.1:8080"); err != nil {
panic(err)
}
}
func page(ctx *mb.Context) mf.Node {
messages := ctx.GetGlobalSnapshot("messages", cloneMessages).([]string)
return mf.Container(mf.ContainerProps{
MaxWidth: "2xl",
Centered: true,
},
mf.Stack(mf.StackProps{Direction: "column", Gap: "5"},
mf.PageHeader(mf.PageHeaderProps{
Title: "Message Board",
Description: "A small app that updates one region through htmx.",
}),
mf.ActionForm(mf.ActionFormProps{
Action: "/messages/create",
Target: "#messages",
Swap: "innerHTML",
Props: mf.ComponentProps{Class: "space-y-3"},
},
mf.FormRow(mf.FormRowProps{
ID: "message",
Label: "Message",
Required: true,
Control: mf.TextField(mf.TextFieldProps{
ID: "message",
Name: "message",
Placeholder: "Write a message",
Required: true,
}),
}),
mf.SubmitButton("Post", mf.ComponentProps{}),
),
mf.Region(mf.RegionProps{ID: "messages"}, messageList(messages)),
),
)
}
func cloneMessages(old any) any {
messages, _ := old.([]string)
return append([]string(nil), messages...)
}
func messageList(messages []string) mf.Node {
if len(messages) == 0 {
return mf.EmptyState(mf.EmptyStateProps{
Title: "No messages yet",
Description: "Submit the form to render this region again.",
})
}
rows := make([]mf.TableComponentRow, 0, len(messages))
for i, message := range messages {
rows = append(rows, mf.TableRowValues(i+1, message))
}
return mf.Table(mf.TableProps{
Columns: []mf.TableColumn{{Label: "No."}, {Label: "Message"}},
Rows: rows,
})
} Step 7
起動して通信を確認する
go run . ブラウザで http://127.0.0.1:8080 を開き、メッセージを投稿します。ページ全体ではなく、#messages の中だけが更新されます。
Step 8
ここで習得したこと
Pageは初期表示、Actionはイベント処理を担当するActionFormがフロントエンドからバックエンドへの通信を作るTargetとRegionで更新範囲を限定する- Actionは画面全体ではなく、差し替えたいHTML断片を返せる
この仕組みは分析ダッシュボードにも使えます。DataQueryState をURLクエリまたはサーバー状態で共有し、
例えば地域チャートをクリックしたら同じ状態でチャートとテーブルを再描画すると、全ウィジェットを一括で絞り込めます。
次のステップ: データアプリ構築手順。
UI部品を増やしたい場合は Components Gallery を確認してください。