Skip to content

Commit d661a68

Browse files
authored
Merge pull request #8 from partial-coffee/flow-test
flow addition
2 parents dd8229c + 4c78376 commit d661a68

File tree

7 files changed

+771
-13
lines changed

7 files changed

+771
-13
lines changed

example/main.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"net/http"
7+
"sync"
8+
9+
"github.com/partial-coffee/go-partial"
10+
)
11+
12+
// In-memory session store for demonstration
13+
var (
14+
sessionStore = make(map[string]*partial.FlowSessionData)
15+
sessionMu sync.Mutex
16+
)
17+
18+
func getSession(r *http.Request) *partial.FlowSessionData {
19+
// For demo, use a fixed session key
20+
sessionMu.Lock()
21+
defer sessionMu.Unlock()
22+
if sessionStore["demo"] == nil {
23+
sessionStore["demo"] = &partial.FlowSessionData{}
24+
}
25+
return sessionStore["demo"]
26+
}
27+
28+
func main() {
29+
fsys := &partial.InMemoryFS{
30+
Files: map[string]string{
31+
"templates/layout.html": `<html><head><title>Flow Example</title></head><body>{{ child "content" }}</body></html>`,
32+
"templates/welcome.html": `<h1>Welcome</h1><a href='/?step=info'>Next</a>`,
33+
"templates/info.html": `<h1>Info</h1><a href='/?step=welcome'>Back</a> <a href='/?step=form'>Next</a>`,
34+
"templates/form.html": `<h1>Form</h1><form method='post'><input name='field' placeholder='Type something'><button type='submit'>Submit</button></form>{{ if .Data.Error }}<div style='color:red'>{{ .Data.Error }}</div>{{ end }}<a href='/?step=info'>Back</a>`,
35+
"templates/confirm.html": `<h1>Confirm</h1><div>Done: true</div><a href='/?step=welcome'>Restart</a>`,
36+
},
37+
}
38+
39+
steps := []partial.FlowStep{
40+
{
41+
Name: "welcome",
42+
Partial: partial.New("templates/welcome.html").ID("content"),
43+
Validate: nil,
44+
},
45+
{
46+
Name: "info",
47+
Partial: partial.New("templates/info.html").ID("content"),
48+
Validate: nil,
49+
},
50+
{
51+
Name: "form",
52+
Partial: partial.New("templates/form.html").ID("content"),
53+
Validate: func(r *http.Request, data map[string]any) error {
54+
if r.Method == http.MethodPost {
55+
if r.FormValue("field") == "" {
56+
return fmt.Errorf("field required")
57+
}
58+
}
59+
return nil
60+
},
61+
},
62+
{
63+
Name: "confirm",
64+
Partial: partial.New("templates/confirm.html").ID("content"),
65+
Validate: nil,
66+
},
67+
}
68+
flow := partial.NewPageFlow(steps)
69+
70+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
71+
session := getSession(r)
72+
stepName := r.URL.Query().Get("step")
73+
if stepName == "" {
74+
stepName = steps[0].Name
75+
}
76+
idx := flow.FindStep(stepName)
77+
if idx == -1 {
78+
http.Error(w, "Step not found", http.StatusNotFound)
79+
return
80+
}
81+
flow.Current = idx
82+
curStep := flow.GetCurrentStep()
83+
84+
var renderError string
85+
if r.Method == http.MethodPost && curStep.Validate != nil {
86+
err := curStep.Validate(r, nil)
87+
if err != nil {
88+
renderError = err.Error()
89+
} else {
90+
session.SetStepValidated(curStep.Name, true)
91+
if flow.Next() {
92+
http.Redirect(w, r, fmt.Sprintf("/?step=%s", flow.GetCurrentStep().Name), http.StatusSeeOther)
93+
return
94+
}
95+
}
96+
}
97+
98+
// Prepare the layout and content partials
99+
layout := partial.New("templates/layout.html").ID("root")
100+
layout.With(curStep.Partial.SetData(map[string]any{"Error": renderError}))
101+
102+
service := partial.NewService(&partial.Config{})
103+
out, err := service.NewLayout().FS(fsys).Set(layout).RenderWithRequest(r.Context(), r)
104+
if err != nil {
105+
http.Error(w, err.Error(), http.StatusInternalServerError)
106+
return
107+
}
108+
_, _ = w.Write([]byte(out))
109+
})
110+
111+
log.Println("Example flow running at http://localhost:8123/")
112+
log.Fatal(http.ListenAndServe(":8123", nil))
113+
}

flow.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package partial
2+
3+
import (
4+
"net/http"
5+
)
6+
7+
// FlowStep represents a single step in a pageflow.
8+
type FlowStep struct {
9+
Name string
10+
Partial *Partial
11+
Validate func(r *http.Request, data map[string]any) error // nil for info-only steps
12+
}
13+
14+
// PageFlow manages a multi-step flow.
15+
type PageFlow struct {
16+
Steps []FlowStep
17+
Current int
18+
}
19+
20+
// FlowSessionData holds all data and validation info for a flow, to be stored in session.
21+
type FlowSessionData struct {
22+
StepData map[string]map[string]any // stepName -> data
23+
Validated map[string]bool // stepName -> validated
24+
Current string // current step name
25+
}
26+
27+
// NewPageFlow creates a new PageFlow with the given steps.
28+
func NewPageFlow(steps []FlowStep) *PageFlow {
29+
return &PageFlow{
30+
Steps: steps,
31+
Current: 0,
32+
}
33+
}
34+
35+
// GetCurrentStep returns the current FlowStep.
36+
func (f *PageFlow) GetCurrentStep() *FlowStep {
37+
if f.Current < 0 || f.Current >= len(f.Steps) {
38+
return nil
39+
}
40+
return &f.Steps[f.Current]
41+
}
42+
43+
// Next advances to the next step if possible.
44+
func (f *PageFlow) Next() bool {
45+
if f.Current < len(f.Steps)-1 {
46+
f.Current++
47+
return true
48+
}
49+
return false
50+
}
51+
52+
// Prev goes back to the previous step if possible.
53+
func (f *PageFlow) Prev() bool {
54+
if f.Current > 0 {
55+
f.Current--
56+
return true
57+
}
58+
return false
59+
}
60+
61+
// FindStep returns the index of a step by name, or -1 if not found.
62+
func (f *PageFlow) FindStep(name string) int {
63+
for i, step := range f.Steps {
64+
if step.Name == name {
65+
return i
66+
}
67+
}
68+
return -1
69+
}
70+
71+
// AllPreviousValidated checks if all previous steps are validated.
72+
func (f *PageFlow) AllPreviousValidated(session *FlowSessionData) bool {
73+
curStep := f.GetCurrentStep()
74+
if curStep == nil {
75+
return false
76+
}
77+
curIdx := f.FindStep(curStep.Name)
78+
for i := 0; i < curIdx; i++ {
79+
if !session.Validated[f.Steps[i].Name] {
80+
return false
81+
}
82+
}
83+
return true
84+
}
85+
86+
// SetStepValidated marks a step as validated in the session.
87+
func (session *FlowSessionData) SetStepValidated(stepName string, valid bool) {
88+
if session.Validated == nil {
89+
session.Validated = make(map[string]bool)
90+
}
91+
session.Validated[stepName] = valid
92+
}
93+
94+
// SetStepData sets the data for a step in the session.
95+
func (session *FlowSessionData) SetStepData(stepName string, data map[string]any) {
96+
if session.StepData == nil {
97+
session.StepData = make(map[string]map[string]any)
98+
}
99+
session.StepData[stepName] = data
100+
}
101+
102+
// GetStepData gets the data for a step from the session.
103+
func (session *FlowSessionData) GetStepData(stepName string) map[string]any {
104+
if session.StepData == nil {
105+
return nil
106+
}
107+
return session.StepData[stepName]
108+
}
109+
110+
// GetAllData returns all step data as a single merged map.
111+
func (session *FlowSessionData) GetAllData() map[string]any {
112+
merged := make(map[string]any)
113+
for _, data := range session.StepData {
114+
for k, v := range data {
115+
merged[k] = v
116+
}
117+
}
118+
return merged
119+
}

0 commit comments

Comments
 (0)