* Move to templ from Gostar
* data-scroll-in-view remove self after application
* throttle stopped double calls (start and end of throttle) by default
* new data: vt {true|false} for turning on/off view transitions per update. During the update things aren't clickable, making it a pain for real-time apps. Default is on still
* Add rawKey to ctx, allows easier cleanup of keys
* fix issues with data-fetch-indicator is is-fetching
* move site to daisyui/tailwind for better consistency in docs and examples

---------

Co-authored-by: Patrick Marchand <mail@patrickmarchand.com>
This commit is contained in:
Delaney 2024-06-04 19:36:44 -07:00 committed by GitHub
parent f754c6e13f
commit ccce8472eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
147 changed files with 15252 additions and 5274 deletions

3
.gitignore vendored
View File

@ -2,4 +2,5 @@ debug_bin*
node_modules
site_bin
.task
.task
*_templ.go

25
.vscode/settings.json vendored
View File

@ -8,28 +8,5 @@
"sudodevnull",
"TLDR",
"Websockets"
],
"playwright.workspaceSettings": {
"configs": [
{
"relativeConfigFile": "playwright/playwright.config.ts",
"selected": true,
"enabled": true,
"projects": [
{
"name": "chromium",
"enabled": true
},
{
"name": "firefox",
"enabled": true
},
{
"name": "webkit",
"enabled": false
}
]
}
]
}
]
}

View File

@ -11,6 +11,11 @@ vars:
sh: cat library/package.json| jq -r .version
tasks:
tools:
cmds:
- go install github.com/a-h/templ/cmd/templ@latest
- go install github.com/go-task/task/v3/cmd/task@latest
version:
cmds:
- echo {{.VERSION}}
@ -40,27 +45,42 @@ tasks:
# - echo "{{.BACKEND_STATIC_DIR}}"
- pnpm i
# - pnpm vitest --watch=false
- pnpm prettier src -w --log-level silent
- pnpm prettier -w .
- pnpm build
- rsync -av dist/ {{.BACKEND_STATIC_DIR}}
- rsync -av package.json {{.BACKEND_STATIC_DIR}}
css:
vars:
BACKEND_STATIC_DIR: "../backends/go/{{.NAME}}/static/css"
dir: library
dir: backends/go/site/css
generates:
- "{{.BACKEND_STATIC_DIR}}"
- "../static/css/site.css"
sources:
- "**/*.md"
- "../backends/go/site/**/*.go"
- "../**/*.md"
# - "../**/*.go"
- "../**/*.templ"
cmds:
- pnpm unocss
- pnpm tailwindcss build -o ../static/css/site.css
templ:
env:
TEMPL_EXPERIMENT: rawgo
generates:
- "**/*_templ.go"
sources:
- "**/*.templ"
cmds:
- templ generate .
kill:
cmds:
- killall -q {{.BIN_NAME}} || echo "Process was not running."
tests:
dir: playwright
cmds:
- pnpm i
- pnpm playwright
hot:
desc: Server hot reload
dir: backends/go
@ -73,6 +93,7 @@ tasks:
deps:
- library
- kill
- templ
# - css
cmds:

View File

@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2
}

View File

@ -0,0 +1,25 @@
{
"name": "datastar-css",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"autoprefixer": "^10.4.19",
"daisyui": "^4.11.1",
"postcss": "^8.4.38",
"prettier": "^3.3.0",
"tailwind-scrollbar": "^3.1.0",
"tailwindcss": "^3.4.3",
"vite": "^5.2.12"
},
"dependencies": {
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.13"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
module.exports = {
plugins: [require('tailwindcss'), require('autoprefixer')],
}

View File

@ -0,0 +1,43 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['../**/*.md', '../**/*.templ'],
theme: {
extend: {
colors: {
discord: '#5865F2',
github: '#333',
},
},
fontFamily: {
sans: 'Inter',
mono: 'Fira Code',
brand: 'Orbitron',
},
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/container-queries'),
require('daisyui'),
require('tailwind-scrollbar'),
],
daisyui: {
themes: [
{
datastar: {
primary: '#c9a75f',
secondary: '#bfdbfe',
accent: '#7dd3fc',
neutral: '#444',
'neutral-content': '#fff',
'base-100': '#0b1325',
'base-200': '#1e304a',
'base-300': '#3a506b',
info: '#0369a1',
success: '#69c383',
warning: '#facc15',
error: '#e11d48',
},
},
],
},
}

View File

@ -9,19 +9,17 @@ import (
"io"
"net/http"
"github.com/a-h/templ"
"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/formatters/html"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
"github.com/benbjohnson/hashfs"
"github.com/delaneyj/datastar"
"github.com/delaneyj/toolbelt"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/gomarkdown/markdown/ast"
mdhtml "github.com/gomarkdown/markdown/html"
. "github.com/delaneyj/gostar/elements"
)
//go:embed static/*
@ -29,7 +27,7 @@ var staticFS embed.FS
var (
staticSys = hashfs.NewFS(staticFS)
highlightCSS *STYLEElement
highlightCSS templ.Component
mdRenderer func() *mdhtml.Renderer
)
@ -78,11 +76,11 @@ func RunBlocking(port int) toolbelt.CtxErrFunc {
func setupRoutes(router chi.Router) error {
defer router.Handle("/static/*", hashfs.FileServer(staticSys))
defer router.Get("/hotreload", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
<-r.Context().Done()
sse.Send("reload", datastar.WithSSERetry(250))
})
// defer router.Get("/hotreload", func(w http.ResponseWriter, r *http.Request) {
// sse := datastar.NewSSE(w, r)
// <-r.Context().Done()
// sse.Send("reload", datastar.WithSSERetry(250))
// })
htmlFormatter := html.New(html.WithClasses(true), html.TabWidth(2))
if htmlFormatter == nil {
@ -97,7 +95,10 @@ func setupRoutes(router chi.Router) error {
if err := htmlFormatter.WriteCSS(highlightCSSBuffer, highlightStyle); err != nil {
return fmt.Errorf("error writing highlight css: %w", err)
}
highlightCSS = STYLE().Text(highlightCSSBuffer.String())
highlightCSS = templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
_, err := io.WriteString(w, fmt.Sprintf(`<style>%s</style>`, highlightCSSBuffer.String()))
return err
})
mdRenderer = func() *mdhtml.Renderer {
return mdhtml.NewRenderer(mdhtml.RendererOptions{
@ -141,9 +142,8 @@ func setupRoutes(router chi.Router) error {
}
if err := errors.Join(
setupAPI(router),
setupHome(router),
setupDocs(router),
setupGuide(router),
setupReferenceRoutes(router),
setupExamples(router),
setupEssays(router),
@ -153,42 +153,3 @@ func setupRoutes(router chi.Router) error {
return nil
}
func buttonLink(isAccent ...bool) *AElement {
bg := "bg-primary-600 hover:bg-primary-700"
if len(isAccent) > 0 && isAccent[0] {
bg = "bg-accent-600 hover:bg-accent-700"
}
return A().
CLASS(
"font-brand font-bold p-4 cursor-pointer text-primary-50 rounded-md text-center flex gap-2 items-center justify-center",
bg,
)
}
func link(href, text string, isHighlighted bool) *AElement {
return A().
IfCLASS(isHighlighted, "text-accent-200 hover:text-accent-100 underline decoration-primary-300").
IfCLASS(!isHighlighted, "text-primary-300 hover:text-primary-200 no-underline").
CLASS("font-bold").
HREF(href).
Text(text)
}
func linkChild(href string, children ...ElementRenderer) *AElement {
return A(children...).CLASS("hover:bg-accent-500 rounded-full p-2").HREF(href)
}
func datastarLogo() *SVGSVGElement {
return SVG_SVG().
VIEW_BOX("0 0 128 128").
Children(
SVG_PATH().
FILL("currentColor").
D("M124.317 3.683c-.538-.515-1.268-.912-1.897-1.01a27.833 27.833 0 0 0-3.451-.304c-.985-.029-2.582.033-3.564.112-17.149 1.385-33.377 8.61-47.923 17.546-.98.601-2.551 1.604-3.503 2.249-8.577 5.806-16.27 12.957-22.41 21.308-.583.794-1.835 1.339-2.817 1.259-9.968-.814-20.434 2.617-26.808 10.5-.723.893-1.783 2.429-2.407 3.394-3.68 5.69-6.321 12.243-7.146 18.953-.14 1.141.237 3.022 1.255 3.513 1.46.703 3.4.235 5.226-.06 1.132-.184 2.998-.288 4.141-.414l14.352-1.575c.98-.108 1.927.592 2.005 1.574.47 5.897-2.002 11.47-3.318 17.187-.571 2.481 1.301 4.663 4.034 4.034 5.716-1.316 11.288-3.79 17.185-3.32.982.079 1.682 1.026 1.574 2.005l-1.574 14.354c-.125 1.142-.23 3.007-.414 4.14-.295 1.825-.764 3.766-.06 5.225.49 1.018 2.373 1.397 3.514 1.257 6.71-.825 13.263-3.466 18.953-7.146.965-.624 2.5-1.684 3.394-2.407 7.883-6.374 11.312-16.842 10.499-26.81-.08-.982.466-2.232 1.26-2.815 8.351-6.14 15.5-13.835 21.306-22.412.645-.952 1.648-2.524 2.25-3.503 8.935-14.546 16.16-30.774 17.545-47.923.08-.982.141-2.579.112-3.564a27.814 27.814 0 0 0-.303-3.45c-.098-.628-.495-1.36-1.01-1.897zM88.447 31.37c3.95 0 8.38 4.43 8.38 8.38 0 .054-3.604 3.84-8.867 7.502-2.924 2.144-9.243 2.604-8.672 5.944.575 3.37 6.625 1.883 9.938 3.118 5.484 1.898 10.716 4.28 10.718 4.287 1.377 3.703-1.699 9.3-5.456 10.523-.098.032-4.726-2.361-9.744-6.041-2.858-2.079-5.209-7.83-8.282-6.333-3.04 1.48.227 6.917.292 10.62-.17 2.81.169 8.835-.975 11.303-3.162 2.414-9.343 1.286-11.79-1.851-.189-.243.856-5.697 2.729-11.108 1.234-3.443 5.96-7.486 3.605-9.841-2.382-2.382-6.496 2.273-9.939 3.507-5.573 1.93-10.937 2.665-11.205 2.437-2.979-2.54-4.255-8.506-1.852-11.596 3.697-.695 7.772-.911 11.596-.877 3.572.065 8.914 3.316 10.426.292 1.534-3.067-4.223-5.521-6.431-8.38-1.038-1.857-5.834-8.027-5.944-9.84 1.164-3.791 6.912-6.762 10.62-5.36.066.025 2.451 5.791 4.19 10.816 1.235 3.443-.123 9.56 3.216 10.036 3.354.48 3.768-5.781 5.846-8.77 3.701-5.046 7.53-8.768 7.6-8.768z"),
SVG_PATH().
FILL("currentColor").
D("M26.794 83.953C19.57 83.732 15.15 93.67 13.47 99.89c-1.422 5.67-7.77 15.862-4.497 19.135 3.273 3.273 13.464-3.075 19.136-4.497 6.22-1.68 16.157-6.1 15.936-13.322-5.283.223-12.989 5.243-19.165 3.279a2.212 2.212 0 0 1-1.365-1.366c-1.965-6.176 3.054-13.883 3.278-19.166z"),
)
}

View File

@ -1,95 +0,0 @@
package site
import (
"math/rand"
"net/http"
"sync/atomic"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/toolbelt"
"github.com/go-chi/chi/v5"
)
func setupAPI(router chi.Router) error {
var globalCount = new(int32)
c := int32(toolbelt.Fit(rand.Float32(), 0, 1, -100, 100))
globalCount = &c
type Store struct {
Count int32 `json:"count"`
}
globalCountExample := func() ElementRenderer {
store := &Store{
Count: atomic.LoadInt32(globalCount),
}
return DIV().
ID("global-count-example").
CLASS("flex flex-col gap-2").
DATASTAR_STORE(store).
Children(
DIV().
CLASS("flex gap-2 justify-between items-center").
Children(
buttonLink(true).
CLASS("text-xs p-1").
DATASTAR_ON("click", "$count++").
Text("Increment Global State +"),
buttonLink(true).
CLASS("text-xs p-1").
DATASTAR_ON("click", "$count--").
Text("Decrement Global State -"),
DIV().
CLASS("flex flex-col gap-2").
Children(
DIV().DATASTAR_TEXT("`Count is ${$count % 2 === 0 ? 'even' : 'odd'}`"),
INPUT().
CLASS("shadow appearance-none border border-accent-500 rounded w-full py-2 px-3 bg-accent-700 text-accent-200 leading-tight focus:outline-none focus:shadow-outline").
TYPE("number").
NAME("count").
CustomData("model", "count").
CustomData("testid", "globalcount_input"),
),
),
DIV().
CLASS("flex gap-4").
Children(
buttonLink(true).
CLASS("flex-1").
DATASTAR_ON("click", datastar.GET("/api/globalCount")).
Text("Load global count"),
buttonLink(true).
CLASS("flex-1").
DATASTAR_ON("click", datastar.POST("/api/globalCount")).
Text("Store global count"),
),
)
}
router.Route("/api", func(apiRouter chi.Router) {
apiRouter.Route("/globalCount", func(globalCountRouter chi.Router) {
globalCountRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(sse, globalCountExample())
})
globalCountRouter.Post("/", func(w http.ResponseWriter, r *http.Request) {
store := &Store{}
if err := datastar.BodyUnmarshal(r, store); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
atomic.StoreInt32(globalCount, store.Count)
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(sse, globalCountExample())
})
})
})
return nil
}

View File

@ -1,80 +0,0 @@
package site
import (
"net/http"
"strings"
. "github.com/delaneyj/gostar/elements"
"github.com/go-chi/chi/v5"
"github.com/samber/lo"
)
var docNames = []string{
"getting_started",
"von_deepa",
"howl",
"batteries_included",
"streaming_backend",
}
func setupDocs(router chi.Router) error {
mdElementRenderers, mdAnchors, err := markdownRenders("docs")
if err != nil {
return err
}
docLabels := lo.Map(docNames, func(name string, i int) string {
return strings.ToUpper(strings.ReplaceAll(name, "_", " "))
})
router.Route("/docs", func(docsRouter chi.Router) {
docsRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/docs/"+docNames[0], http.StatusFound)
})
sidebarContents := func(r *http.Request) ElementRenderer {
return Group(lo.Map(docNames, func(name string, i int) ElementRenderer {
return link("/docs/"+name, docLabels[i], strings.HasSuffix(r.URL.Path, name)).CLASS("uppercase font-brand")
})...)
}
docsRouter.Get("/{docName}", func(w http.ResponseWriter, r *http.Request) {
docName := chi.URLParam(r, "docName")
contents, ok := mdElementRenderers[docName]
if !ok {
http.Error(w, "not found", http.StatusNotFound)
return
}
docIdx := lo.IndexOf(docNames, docName)
contentGroup := []ElementRenderer{}
if docIdx > 0 {
contentGroup = append(contentGroup,
buttonLink().
CLASS("w-full no-underline").
HREF("/docs/"+docNames[docIdx-1]).
Text("Back to "+docLabels[docIdx-1]),
)
}
contentGroup = append(contentGroup, contents)
if docIdx < len(docNames)-1 {
contentGroup = append(contentGroup,
buttonLink().
CLASS("w-full no-underline").
HREF("/docs/"+docNames[docIdx+1]).
Text("Next "+docLabels[docIdx+1]),
)
}
prosePage(
r,
sidebarContents(r),
Group(contentGroup...),
mdAnchors[docName],
).Render(w)
})
})
return nil
}

View File

@ -4,75 +4,82 @@ import (
"net/http"
"strings"
. "github.com/delaneyj/gostar/elements"
"github.com/a-h/templ"
"github.com/go-chi/chi/v5"
"github.com/samber/lo"
)
func setupEssays(router chi.Router) error {
mdElementRenderers, mdAnchors, err := markdownRenders("essays")
mdElementRenderers, _, err := markdownRenders("essays")
if err != nil {
return err
}
essayNames := []string{
"why_another_framework",
"yes_you_want_a_build_step",
"haikus",
"grugs_around_fire",
"i_am_a_teapot",
"event_streams_all_the_way_down",
"another_dependency",
sidebarGroups := []*SidebarGroup{
{
Label: "2024",
Links: []*SidebarLink{
{ID: "another_dependency"},
{ID: "event_streams_all_the_way_down"},
},
},
{
Label: "2023",
Links: []*SidebarLink{
{ID: "i_am_a_teapot"},
{ID: "grugs_around_fire"},
{ID: "haikus"},
{ID: "yes_you_want_a_build_step"},
{ID: "why_another_framework"},
},
},
}
lo.ForEach(sidebarGroups, func(group *SidebarGroup, grpIdx int) {
lo.ForEach(group.Links, func(link *SidebarLink, linkIdx int) {
link.URL = templ.SafeURL("/essays/" + link.ID)
link.Label = strings.ToUpper(strings.ReplaceAll(link.ID, "_", " "))
essayLabels := lo.Map(essayNames, func(name string, i int) string {
return strings.ToUpper(strings.ReplaceAll(name, "_", " "))
if linkIdx > 0 {
link.Prev = group.Links[linkIdx-1]
} else if grpIdx > 0 {
prvGrp := sidebarGroups[grpIdx-1]
link.Prev = prvGrp.Links[len(prvGrp.Links)-1]
}
if linkIdx < len(group.Links)-1 {
link.Next = group.Links[linkIdx+1]
} else if grpIdx < len(sidebarGroups)-1 {
nxtGrp := sidebarGroups[grpIdx+1]
link.Next = nxtGrp.Links[0]
}
})
})
router.Route("/essays", func(essaysRouter chi.Router) {
essaysRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/essays/"+essayNames[0], http.StatusFound)
http.Redirect(w, r, string(sidebarGroups[0].Links[0].URL), http.StatusFound)
})
sidebarContents := func(r *http.Request) ElementRenderer {
return Group(lo.Map(essayNames, func(name string, i int) ElementRenderer {
return link("/essays/"+name, essayLabels[i], strings.HasSuffix(r.URL.Path, name)).CLASS("uppercase font-brand")
})...)
}
essaysRouter.Get("/{docName}", func(w http.ResponseWriter, r *http.Request) {
docName := chi.URLParam(r, "docName")
contents, ok := mdElementRenderers[docName]
essaysRouter.Get("/{name}", func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
contents, ok := mdElementRenderers[name]
if !ok {
http.Error(w, "not found", http.StatusNotFound)
return
}
docIdx := lo.IndexOf(essayNames, docName)
contentGroup := []ElementRenderer{}
if docIdx > 0 {
contentGroup = append(contentGroup,
buttonLink().
CLASS("w-full").
HREF("/essays/"+essayNames[docIdx-1]).
Text("Back to "+essayLabels[docIdx-1]).
CLASS("flex flex-col justify-center items-center no-underline"))
}
contentGroup = append(contentGroup, contents)
if docIdx < len(essayNames)-1 {
contentGroup = append(contentGroup,
buttonLink().
CLASS("w-full").
HREF("/essays/"+essayNames[docIdx+1]).
Text("Next "+essayLabels[docIdx+1]).
CLASS("flex flex-col justify-center items-center no-underline"))
var currentLink *SidebarLink
for _, group := range sidebarGroups {
for _, link := range group.Links {
if link.ID == name {
currentLink = link
break
}
}
}
anchors := mdAnchors[docName]
prosePage(r, sidebarContents(r), Group(contentGroup...), anchors).Render(w)
SidebarPage(r, sidebarGroups, currentLink, contents).Render(r.Context(), w)
})
})

View File

@ -4,9 +4,9 @@ import (
"errors"
"fmt"
"net/http"
"strings"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/toolbelt"
"github.com/a-h/templ"
"github.com/go-chi/chi/v5"
"github.com/go-sanitize/sanitize"
"github.com/gorilla/sessions"
@ -14,184 +14,287 @@ import (
)
var (
sanitizer *sanitize.Sanitizer
SignalStore = Group(
H4(Text("Signal Store")),
PRE().DATASTAR_TEXT("JSON.stringify(ctx.store())"),
)
sanitizer *sanitize.Sanitizer
)
func setupExamples(router chi.Router) (err error) {
sanitizer, err = sanitize.New()
if err != nil {
return fmt.Errorf("error creating sanitizer: %w", err)
}
mdElementRenderers, _, err := markdownRenders("examples")
if err != nil {
return err
}
type Example struct {
URL string
Label string
Description string
Prev, Next *Example
sanitizer, err = sanitize.New()
if err != nil {
return fmt.Errorf("error creating sanitizer: %w", err)
}
type ExampleGroup struct {
Label string
Examples []*Example
}
var (
prevExample *Example
examplesByURL = map[string]*Example{}
)
examples := lo.Map([]ExampleGroup{
sidebarGroups := []*SidebarGroup{
{
Label: "Ported HTMX Examples",
Examples: []*Example{
{Label: "Click to Edit", Description: "inline editing of a data object"},
{Label: "Bulk Update", Description: "bulk updating of multiple rows of data"},
{Label: "Click to Load", Description: "loading data on demand"},
{Label: "Delete Row", Description: "row deletion in a table"},
{Label: "Edit Row", Description: "how to edit rows in a table"},
{Label: "Lazy Load", Description: "how to lazy load content"},
{Label: "Fetch Indicator", Description: "show a loading indicator when fetching data"},
{Label: "Is Loading Identifier", Description: "use an isLoading set of identifiers in a signal to reflect when an element is fetching"},
{Label: "Inline Validation", Description: "how to do inline field validation"},
{Label: "Infinite Scroll", Description: "infinite scrolling of a page"},
{Label: "Active Search", Description: "the active search box pattern"},
{Label: "Progress Bar", Description: "a job-runner like progress bar"},
{Label: "Value Select", Description: "making the values of a select dependent on another select"},
{Label: "Animations", Description: "various animation techniques"},
{Label: "File Upload", Description: "how to upload a file via ajax with a progress bar"},
{Label: "Dialogs Browser", Description: "the prompt and confirm dialogs"},
{Label: "Lazy Tabs", Description: "how to lazy load tabs"},
Links: []*SidebarLink{
{ID: "click_to_edit"},
{ID: "bulk_update"},
{ID: "click_to_load"},
{ID: "delete_row"},
{ID: "edit_row"},
{ID: "lazy_load"},
{ID: "fetch_indicator"},
{ID: "inline_validation"},
{ID: "infinite_scroll"},
{ID: "active_search"},
{ID: "progress_bar"},
{ID: "value_select"},
{ID: "animations"},
{ID: "file_upload"},
{ID: "dialogs_browser"},
{ID: "lazy_tabs"},
},
},
{
Label: "Web Components Examples",
Examples: []*Example{
{Label: "Shoelace Kitchensink", Description: "the Shoelace Web Components library"},
Links: []*SidebarLink{
{ID: "shoelace_kitchensink"},
},
},
{
Label: "Reactive Examples",
Examples: []*Example{
{Label: "Multiline Fragments", Description: "multiline fragments"},
{Label: "Scroll Into View", Description: "scrolling an element into view"},
{Label: "On Load", Description: "how to load data on page load"},
{Label: "Model Binding", Description: "two-way data binding to signals"},
{Label: "Disable Button", Description: "how to disable a button while processing"},
{Label: "Merge Options", Description: "how to merge options in a select"},
{Label: "Redirects", Description: "how to redirect to another page"},
{Label: "View Transition API", Description: "using the view transition API"},
{Label: "Title Update Backend", Description: "target a specific element for updates"},
{Label: "Store Changed", Description: "detect when a store has changed"},
{Label: "RAF Update", Description: "update a signal on requestAnimationFrame"},
{Label: "Update Store", Description: "update a store from an SSE event"},
Links: []*SidebarLink{
{ID: "multiline_fragments"},
{ID: "scroll_into_view"},
{ID: "on_load"},
{ID: "model_binding"},
{ID: "disable_button"},
{ID: "merge_options"},
{ID: "redirects"},
{ID: "view_transition_api"},
{ID: "title_update_backend"},
{ID: "store_changed"},
{ID: "raf_update"},
{ID: "update_store"},
},
},
{
Label: "Backend Examples",
Examples: []*Example{
{Label: "Node", Description: "example backend in node"},
{Label: "Python", Description: "example backend in python"},
{Label: "Quick Primer Go", Description: "The getting started guide in Go"},
Links: []*SidebarLink{
{ID: "node"},
{ID: "python"},
{ID: "quick_primer_go"},
{ID: "templ_counter"},
},
},
}, func(g ExampleGroup, i int) ExampleGroup {
for j, example := range g.Examples {
g.Examples[j].URL = "/examples/" + toolbelt.Cased(g.Examples[j].Label, toolbelt.Snake, toolbelt.Lower)
if prevExample != nil {
example.Prev = prevExample
prevExample.Next = example
}
lo.ForEach(sidebarGroups, func(group *SidebarGroup, grpIdx int) {
lo.ForEach(group.Links, func(link *SidebarLink, linkIdx int) {
link.URL = templ.SafeURL("/examples/" + link.ID)
link.Label = strings.ToUpper(strings.ReplaceAll(link.ID, "_", " "))
if linkIdx > 0 {
link.Prev = group.Links[linkIdx-1]
} else if grpIdx > 0 {
prvGrp := sidebarGroups[grpIdx-1]
link.Prev = prvGrp.Links[len(prvGrp.Links)-1]
}
prevExample = example
examplesByURL[example.URL] = example
}
return g
if linkIdx < len(group.Links)-1 {
link.Next = group.Links[linkIdx+1]
} else if grpIdx < len(sidebarGroups)-1 {
nxtGrp := sidebarGroups[grpIdx+1]
link.Next = nxtGrp.Links[0]
}
})
})
router.Route("/examples", func(examplesRouter chi.Router) {
examplesRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, examples[0].Examples[0].URL, http.StatusFound)
http.Redirect(w, r, string(sidebarGroups[0].Links[0].URL), http.StatusFound)
})
examplesRouter.Get("/{exampleName}", func(w http.ResponseWriter, r *http.Request) {
exampleName := chi.URLParam(r, "exampleName")
contents, ok := mdElementRenderers[exampleName]
examplesRouter.Get("/{name}", func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
contents, ok := mdElementRenderers[name]
if !ok {
http.Error(w, "not found", http.StatusNotFound)
return
}
example, ok := examplesByURL[r.URL.Path]
if !ok {
http.Error(w, "not found", http.StatusNotFound)
var currentLink *SidebarLink
for _, group := range sidebarGroups {
for _, link := range group.Links {
if link.ID == name {
currentLink = link
break
}
}
}
contentGroup := []ElementRenderer{}
if example.Prev != nil {
contentGroup = append(contentGroup,
buttonLink().
CLASS("w-full").
HREF(example.Prev.URL).
Text("Back to "+example.Prev.Label).
CLASS("flex flex-col justify-center items-center no-underline"))
}
contentGroup = append(contentGroup, contents)
nextHREF := "/reference"
nextLabel := "Dive deeper"
if example.Next != nil {
nextHREF = example.Next.URL
nextLabel = "Next " + example.Next.Label
}
contentGroup = append(contentGroup,
buttonLink().
CLASS("w-full").
HREF(nextHREF).
Text(nextLabel).
CLASS("flex flex-col justify-center items-center no-underline"))
sidebarContents := Group(
Range(examples, func(g ExampleGroup) ElementRenderer {
return DIV(
DIV(
DIV().CLASS("text-2xl font-bold text-primary").Text(g.Label),
HR().CLASS("divider border-primary"),
),
TABLE().
CLASS("table w-full").
Children(
THEAD(
TR(
TH().Text("Pattern"),
TH().Text("Description"),
),
),
TBODY(
Range(g.Examples, func(e *Example) ElementRenderer {
return TR().
CLASS("hover").
Children(
TD(link(e.URL, e.Label, e.URL == r.URL.Path)),
TD().CLASS("text-xs").Text(e.Description),
)
}),
),
),
)
}),
)
pp := prosePage(r, sidebarContents, Group(contentGroup...), nil)
pp.Render(w)
SidebarPage(r, sidebarGroups, currentLink, contents).Render(r.Context(), w)
})
// mdElementRenderers, _, err := markdownRenders("examples")
// if err != nil {
// return err
// }
// type Example struct {
// URL string
// Label string
// Description string
// Prev, Next *Example
// }
// type ExampleGroup struct {
// Label string
// Examples []*Example
// }
// var (
// prevExample *Example
// examplesByURL = map[string]*Example{}
// )
// examples := lo.Map([]ExampleGroup{
// {
// Label: "Ported HTMX Examples",
// Examples: []*Example{
// {Label: "Click to Edit", Description: "inline editing of a data object"},
// {Label: "Bulk Update", Description: "bulk updating of multiple rows of data"},
// {Label: "Click to Load", Description: "loading data on demand"},
// {Label: "Delete Row", Description: "row deletion in a table"},
// {Label: "Edit Row", Description: "how to edit rows in a table"},
// {Label: "Lazy Load", Description: "how to lazy load content"},
// {Label: "Fetch Indicator", Description: "show a loading indicator when fetching data"},
// {Label: "Inline Validation", Description: "how to do inline field validation"},
// {Label: "Infinite Scroll", Description: "infinite scrolling of a page"},
// {Label: "Active Search", Description: "the active search box pattern"},
// {Label: "Progress Bar", Description: "a job-runner like progress bar"},
// {Label: "Value Select", Description: "making the values of a select dependent on another select"},
// {Label: "Animations", Description: "various animation techniques"},
// {Label: "File Upload", Description: "how to upload a file via ajax with a progress bar"},
// {Label: "Dialogs Browser", Description: "the prompt and confirm dialogs"},
// {Label: "Lazy Tabs", Description: "how to lazy load tabs"},
// },
// },
// {
// Label: "Web Components Examples",
// Examples: []*Example{
// {Label: "Shoelace Kitchensink", Description: "the Shoelace Web Components library"},
// },
// },
// {
// Label: "Reactive Examples",
// Examples: []*Example{
// {Label: "Multiline Fragments", Description: "multiline fragments"},
// {Label: "Scroll Into View", Description: "scrolling an element into view"},
// {Label: "On Load", Description: "how to load data on page load"},
// {Label: "Model Binding", Description: "two-way data binding to signals"},
// {Label: "Disable Button", Description: "how to disable a button while processing"},
// {Label: "Merge Options", Description: "how to merge options in a select"},
// {Label: "Redirects", Description: "how to redirect to another page"},
// {Label: "View Transition API", Description: "using the view transition API"},
// {Label: "Title Update Backend", Description: "target a specific element for updates"},
// {Label: "Store Changed", Description: "detect when a store has changed"},
// {Label: "RAF Update", Description: "update a signal on requestAnimationFrame"},
// {Label: "Update Store", Description: "update a store from an SSE event"},
// },
// },
// {
// Label: "Backend Examples",
// Examples: []*Example{
// {Label: "Node", Description: "example backend in node"},
// {Label: "Python", Description: "example backend in python"},
// {Label: "Quick Primer Go", Description: "The getting started guide in Go"},
// {Label: "Templ Counter", Description: "a simple counter example for Templ"},
// },
// },
// }, func(g ExampleGroup, i int) ExampleGroup {
// for j, example := range g.Examples {
// g.Examples[j].URL = "/examples/" + toolbelt.Cased(g.Examples[j].Label, toolbelt.Snake, toolbelt.Lower)
// if prevExample != nil {
// example.Prev = prevExample
// prevExample.Next = example
// }
// prevExample = example
// examplesByURL[example.URL] = example
// }
// return g
// })
// router.Route("/examples", func(examplesRouter chi.Router) {
// examplesRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
// http.Redirect(w, r, examples[0].Examples[0].URL, http.StatusFound)
// })
// examplesRouter.Get("/{exampleName}", func(w http.ResponseWriter, r *http.Request) {
// exampleName := chi.URLParam(r, "exampleName")
// contents, ok := mdElementRenderers[exampleName]
// if !ok {
// http.Error(w, "not found", http.StatusNotFound)
// return
// }
// example, ok := examplesByURL[r.URL.Path]
// if !ok {
// http.Error(w, "not found", http.StatusNotFound)
// }
// contentGroup := []ElementRenderer{}
// if example.Prev != nil {
// contentGroup = append(contentGroup,
// buttonLink().
// CLASS("w-full").
// HREF(example.Prev.URL).
// Text("Back to "+example.Prev.Label).
// CLASS("flex flex-col justify-center items-center no-underline"))
// }
// contentGroup = append(contentGroup, contents)
// nextHREF := "/reference"
// nextLabel := "Dive deeper"
// if example.Next != nil {
// nextHREF = example.Next.URL
// nextLabel = "Next " + example.Next.Label
// }
// contentGroup = append(contentGroup,
// buttonLink().
// CLASS("w-full").
// HREF(nextHREF).
// Text(nextLabel).
// CLASS("flex flex-col justify-center items-center no-underline"))
// sidebarContents := Group(
// Range(examples, func(g ExampleGroup) ElementRenderer {
// return DIV(
// DIV(
// DIV().CLASS("text-2xl font-bold text-primary").Text(g.Label),
// HR().CLASS("divider border-primary"),
// ),
// TABLE().
// CLASS("table w-full").
// Children(
// THEAD(
// TR(
// TH().Text("Pattern"),
// TH().Text("Description"),
// ),
// ),
// TBODY(
// Range(g.Examples, func(e *Example) ElementRenderer {
// return TR().
// CLASS("hover").
// Children(
// TD(link(e.URL, e.Label, e.URL == r.URL.Path)),
// TD().CLASS("text-xs").Text(e.Description),
// )
// }),
// ),
// ),
// )
// }),
// )
// pp := prosePage(r, sidebarContents, Group(contentGroup...), nil)
// pp.Render(w)
// })
examplesSessionStore := sessions.NewCookieStore([]byte("ExampleSession"))
if err := errors.Join(
@ -202,7 +305,6 @@ func setupExamples(router chi.Router) (err error) {
setupExamplesDeleteRow(examplesRouter),
setupExamplesLazyLoad(examplesRouter),
setupExamplesFetchIndicator(examplesRouter),
setupExamplesIsLoadingId(examplesRouter),
setupExamplesOnLoad(examplesRouter, examplesSessionStore),
setupExamplesDisableButton(examplesRouter),
setupExampleInlineValidation(examplesRouter),
@ -227,6 +329,7 @@ func setupExamples(router chi.Router) (err error) {
setupExamplesStoreChanged(examplesRouter, examplesSessionStore),
setupExamplesScrollIntoView(examplesRouter),
setupExamplesQuickPrimerGo(examplesRouter),
setupExamplesTemplCounter(examplesRouter, examplesSessionStore),
); err != nil {
panic(fmt.Sprintf("error setting up examples routes: %s", err))
}

View File

@ -5,11 +5,8 @@ import (
"net/http"
"slices"
"strings"
"time"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/gostar/elements/iconify/svg_spinners"
"github.com/delaneyj/toolbelt"
"github.com/go-chi/chi/v5"
"github.com/go-faker/faker/v4"
@ -18,24 +15,9 @@ import (
func setupExamplesActiveSearch(examplesRouter chi.Router) error {
// activeSearchRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
// examplePage(w, r)
// })
type Store struct {
Search string `json:"search"`
}
type User struct {
ID string `json:"id"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"email"`
}
users := make([]*User, 256)
users := make([]*ActiveSearchUser, 256)
for i := range users {
u := &User{
u := &ActiveSearchUser{
ID: faker.UUIDHyphenated(),
FirstName: faker.FirstName(),
LastName: faker.LastName(),
@ -45,7 +27,7 @@ func setupExamplesActiveSearch(examplesRouter chi.Router) error {
}
examplesRouter.Get("/active_search/data", func(w http.ResponseWriter, r *http.Request) {
store := &Store{}
store := &ActiveSearchStore{}
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@ -75,71 +57,16 @@ func setupExamplesActiveSearch(examplesRouter chi.Router) error {
}
// copy users
filteredUsers := make([]*User, len(users))
filteredUsers := make([]*ActiveSearchUser, len(users))
copy(filteredUsers, users)
// sort users by score
slices.SortFunc(filteredUsers, func(a, b *User) int {
slices.SortFunc(filteredUsers, func(a, b *ActiveSearchUser) int {
return int(10000 * (scores[b.ID] - scores[a.ID]))
})
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(
sse,
DIV().
ID("active_search").
CLASS("flex flex-col gap-4").
DATASTAR_STORE(store).
Children(
DIV().
CLASS("flex gap-2").
Children(
DIV().
CLASS("flex-1").
Text("Search Contacts"),
),
DIV().
CLASS("form-control").
Children(
DIV().
CLASS("flex gap-2").
Children(
INPUT().
CLASS("bg-accent-900 border-2 border-accent-600 text-accent-100 text-sm rounded-lg focus:ring-primary-400 focus:border-primary-400 block w-full p-2.5").
TYPE("text").
PLACEHOLDER("Search...").
DATASTAR_MODEL("search").
DATASTAR_ON("input", datastar.GET("/examples/active_search/data"), InputOnModDebounce(1*time.Second)),
svg_spinners.BlocksWave().
CLASS("text-5xl datastar-indicator"),
),
),
TABLE().
CLASS("table w-full").
Children(
CAPTION(Text("Contacts")),
THEAD(
TR(
TH(Text("First Name")),
TH(Text("Last Name")),
TH(Text("Email")),
TH(Text("Score")),
),
),
TBODY(
Range(filteredUsers[0:10], func(u *User) ElementRenderer {
score := scores[u.ID]
return TR(
TD(Text(u.FirstName)),
TD(Text(u.LastName)),
TD(Text(u.Email)),
TD(TextF("%0.2f", score)),
)
}),
).ID("active_search_rows"),
),
),
)
datastar.RenderFragmentTempl(sse, ActiveSearchComponent(filteredUsers, scores, store))
})
return nil

View File

@ -0,0 +1,56 @@
package site
import (
"fmt"
)
type ActiveSearchStore struct {
Search string `json:"search"`
}
type ActiveSearchUser struct {
ID string `json:"id"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"email"`
}
templ ActiveSearchComponent(filteredUsers []*ActiveSearchUser, scores map[string]float64, store *ActiveSearchStore) {
<div
id="active_search"
class="flex flex-col gap-4"
data-store={ templ.JSONString(store) }
>
<div class="flex gap-4">
<input
class="flex-1 input input-bordered"
type="text"
placeholder="Search..."
data-model="search"
data-on-input.debounce_500ms="$$get('/examples/active_search/data')"
/>
@icon("svg-spinners:blocks-wave", "class", "text-5xl datastar-indicator")
</div>
<table class="table w-full">
<caption>Contacts</caption>
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Score</th>
</tr>
</thead>
<tbody id="active_search_rows">
for _,user := range filteredUsers[0:10] {
<tr>
<td>{ user.FirstName }</td>
<td>{ user.LastName }</td>
<td>{ user.Email }</td>
<td>{ fmt.Sprintf("%0.2f", scores[user.ID]) }</td>
</tr>
}
</tbody>
</table>
</div>
}

View File

@ -6,9 +6,6 @@ import (
"time"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
"github.com/delaneyj/gostar/elements/iconify/svg_spinners"
"github.com/go-chi/chi/v5"
)
@ -17,12 +14,7 @@ func setupExamplesAnimations(examplesRouter chi.Router) error {
// examplePage(w, r)
// })
type Color struct {
Label string `json:"label"`
Value int `json:"value"`
}
fgPal := []Color{
fgPal := []AnimationsColor{
{Label: "red", Value: 0xfb4934},
{Label: "green", Value: 0xb8bb26},
{Label: "yellow", Value: 0xfabd2f},
@ -32,7 +24,7 @@ func setupExamplesAnimations(examplesRouter chi.Router) error {
{Label: "orange", Value: 0xfe8019},
}
bgPal := []Color{
bgPal := []AnimationsColor{
{Label: "red", Value: 0x9d0006},
{Label: "green", Value: 0x79740e},
{Label: "yellow", Value: 0xb57614},
@ -42,94 +34,14 @@ func setupExamplesAnimations(examplesRouter chi.Router) error {
{Label: "orange", Value: 0xaf3a03},
}
type RestoreStore struct {
ShouldRestore bool `json:"shouldRestore"`
}
renderViewTransition := func(sse *datastar.ServerSentEventsHandler, store *RestoreStore) {
datastar.RenderFragment(
sse,
DIV().
ID("view_transition").
DATASTAR_STORE(store).
CLASS("slide-it").
Children(
BUTTON().
CLASS("btn btn-primary").
DATASTAR_ON("click", datastar.GET("/examples/animations/data/view_transition")).
Children(
Tern(store.ShouldRestore, material_symbols.ArrowLeft(), material_symbols.ArrowRight()),
Tern(store.ShouldRestore, Text("Restore It!"), Text("Swap It!")),
),
),
)
}
examplesRouter.Route("/animations/data", func(dataRouter chi.Router) {
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(
sse,
BUTTON().
ID("fade_out_swap").
CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-error-700 hover:bg-error-600").
DATASTAR_ON("click", datastar.DELETE("/examples/animations/data")).
Children(
material_symbols.Delete(),
Text("Fade out then delete on click"),
),
)
datastar.RenderFragment(
sse,
BUTTON().
ID("fade_me_in").
CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-success-700 hover:bg-success-600").
DATASTAR_ON("click", datastar.GET("/examples/animations/data/fade_me_in")).
Children(
material_symbols.Add(),
Text("Fade me in on click"),
),
)
datastar.RenderFragment(
sse,
DIV().
ID("request_in_flight").
CLASS("flex flex-col gap-4").
Children(
DIV().
CLASS("form-control").
Children(
LABEL(Text("Name")).CLASS("label label-text"),
DIV().
CLASS("flex gap-2 items-center").
Children(
INPUT().
TYPE("text").
NAME("name").
CLASS("bg-accent-900 border-2 border-accent-600 text-accent-100 text-sm rounded-lg focus:ring-primary-400 focus:border-primary-400 block w-full p-2.5"),
DIV().
ID("request_in_flight_indicator").
Children(
svg_spinners.BlocksWave().CLASS("text-xl"),
),
),
),
BUTTON().
ID("submit_request_in_flight").
CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-success-700 hover:bg-success-600").
DATASTAR_ON("click", datastar.POST("/examples/animations/data/request_in_flight")).
DATASTAR_FETCH_INDICATOR("'#request_in_flight_indicator'").
Children(
material_symbols.PersonAdd(),
Text("Submit"),
),
),
)
renderViewTransition(sse, &RestoreStore{ShouldRestore: false})
datastar.RenderFragmentTempl(sse, animationsFadeOutSwap(false))
datastar.RenderFragmentTempl(sse, animationsFadeMeIn(true))
datastar.RenderFragmentTempl(sse, animationsRequestInFlight())
datastar.RenderFragmentTempl(sse, animationsViewTransition(&AnimationsRestoreStore{ShouldRestore: false}))
colorThrobTicker := time.NewTicker(2 * time.Second)
for {
@ -139,16 +51,9 @@ func setupExamplesAnimations(examplesRouter chi.Router) error {
case <-colorThrobTicker.C:
fg := fgPal[rand.Intn(len(fgPal))]
bg := bgPal[rand.Intn(len(bgPal))]
datastar.RenderFragment(
datastar.RenderFragmentTempl(
sse,
DIV().
ID("color_throb").
CustomData("testid", "color_throb").
CLASS("transition-all duration-1000 font-bold text-2xl text-center rounded-box p-4 uppercase").
STYLEF("color", "#%x", fg.Value).
STYLEF("background-color", "#%x", bg.Value).
TextF("%s on %s", fg.Label, bg.Label),
animationsColorThrob(fg, bg),
)
}
}
@ -156,69 +61,33 @@ func setupExamplesAnimations(examplesRouter chi.Router) error {
dataRouter.Delete("/", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(
sse,
BUTTON().
ID("fade_out_swap").
CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-error-700 hover:bg-error-600 transition-all duration-[2000ms] opacity-0").
DATASTAR_ON("click", datastar.DELETE("/examples/animations/data")).
Children(
material_symbols.Delete(),
Text("Fade out then delete on click"),
),
)
datastar.RenderFragmentTempl(sse, animationsFadeOutSwap(true))
time.Sleep(2 * time.Second)
datastar.Delete(sse, "#fade_out_swap")
})
dataRouter.Get("/fade_me_in", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(
sse,
BUTTON().
ID("fade_me_in").
CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-success-700 hover:bg-success-600 opacity-0").
DATASTAR_ON("click", datastar.GET("/examples/animations/data/fade_me_in")).
Children(
material_symbols.Add(),
Text("Fade me in on click"),
),
)
datastar.RenderFragmentTempl(sse, animationsFadeMeIn(false))
time.Sleep(500 * time.Millisecond)
datastar.RenderFragment(
sse,
BUTTON().
ID("fade_me_in").
CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-success-700 hover:bg-success-600 transition-all duration-1000 opacity-100").
DATASTAR_ON("click", datastar.GET("/examples/animations/data/fade_me_in")).
Children(
material_symbols.Add(),
Text("Fade me in on click"),
),
)
datastar.RenderFragmentTempl(sse, animationsFadeMeIn(true))
})
dataRouter.Post("/request_in_flight", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
datastar.RenderFragment(
datastar.NewSSE(w, r),
DIV().
ID("request_in_flight").
CLASS("flex gap-2").
Text("Submitted!"),
)
sse := datastar.NewSSE(w, r)
datastar.RenderFragmentString(sse, `<div id="request_in_flight">Submitted!</div>`)
})
dataRouter.Get("/view_transition", func(w http.ResponseWriter, r *http.Request) {
store := &RestoreStore{}
store := &AnimationsRestoreStore{}
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
store.ShouldRestore = !store.ShouldRestore
sse := datastar.NewSSE(w, r)
renderViewTransition(sse, store)
datastar.RenderFragmentTempl(sse, animationsViewTransition(store))
})
})

View File

@ -0,0 +1,100 @@
package site
import (
"fmt"
)
type AnimationsRestoreStore struct {
ShouldRestore bool `json:"shouldRestore"`
}
templ animationsViewTransition(store *AnimationsRestoreStore) {
<div
id="view_transition"
data-store={ templ.JSONString(store) }
class="slide-it"
>
<button
class="btn btn-primary"
data-on-click="$$get('/examples/animations/data/view_transition')"
>
if store.ShouldRestore {
@icon("material-symbols:arrow-left")
Restore It!
} else {
@icon("material-symbols:arrow-right")
"Swap It!"
}
</button>
</div>
}
templ animationsFadeOutSwap(shouldHide bool) {
<button
id="fade_out_swap"
class={
"btn btn-error",
templ.KV("transition-all duration-1000 opacity-0", shouldHide),
}
data-on-click="$$delete('/examples/animations/data')"
>
@icon("material-symbols:delete")
Fade out then delete on click
</button>
}
templ animationsFadeMeIn(shouldBeShown bool) {
<button
id="fade_me_in"
class={ "btn btn-success",
templ.KV("transition-all duration-1000", shouldBeShown),
templ.KV("opacity-0", !shouldBeShown) }
data-on-click="$$get('/examples/animations/data/fade_me_in')"
>
@icon("material-symbols:add")
Fade me in on click
</button>
}
type AnimationsColor struct {
Label string `json:"label"`
Value int `json:"value"`
}
templ animationsColorThrob(fg, bg AnimationsColor) {
{{ styl:= fmt.Sprintf("color: #%x; background-color: #%x", fg.Value, bg.Value) }}
<div
id="color_throb"
class="p-4 text-2xl font-bold text-center uppercase transition-all duration-1000 rounded-box"
{ templ.Attributes{"style": styl}... }
>
{ fg.Label } on { bg.Label }
</div>
}
templ animationsRequestInFlight() {
<div id="request_in_flight" class="flex flex-col gap-4">
<div class="form-control">
<label class="label label-text">Name</label>
<div class="flex items-center gap-2">
<input
type="text"
name="name"
class="flex-1 input input-primary"
/>
<div id="request_in_flight_indicator">
@icon("svg-spinners:blocks-wave")
</div>
</div>
</div>
<button
id="submit_request_in_flight"
class="btn btn-success"
data-on-click="$$post('/examples/animations/data/request_in_flight')"
data-fetch-indicator="'#request_in_flight_indicator'"
>
@icon("material-symbols:person-add")
Submit
</button>
</div>
}

View File

@ -7,8 +7,6 @@ import (
"strings"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
"github.com/go-chi/chi/v5"
)
@ -48,32 +46,15 @@ func starterActiveContacts() []*ContactActive {
}
}
type BulkUpdateSelectionStore struct {
Selections map[string]bool `json:"selections"`
}
func setupExamplesBulkUpdate(examplesRouter chi.Router) error {
contactToNode := func(i int, cs *ContactActive, wasChanged bool) ElementRenderer {
key := fmt.Sprintf("contact_%d", i)
return TR().
ID(key).
IfCLASS(i%2 == 0, "bg-accent-800").
IfCLASS(i%2 == 1, "bg-accent-700").
IfCLASS(wasChanged && cs.IsActive, "activate").
IfCLASS(wasChanged && !cs.IsActive, "deactivate").
Children(
TD(
INPUT().CLASS("checkbox").TYPE("checkbox").DATASTAR_MODEL("selections."+key),
TD(Text(cs.Name)),
TD(Text(cs.Email)),
TD(Tern(cs.IsActive, Text("Active"), Text("Inactive"))),
),
)
}
contacts := starterActiveContacts()
type SelectionStore struct {
Selections map[string]bool `json:"selections"`
}
defaultSelectionStore := func() SelectionStore {
defaultSelectionStore := func() *BulkUpdateSelectionStore {
selections := map[string]bool{
"all": false,
}
@ -81,71 +62,19 @@ func setupExamplesBulkUpdate(examplesRouter chi.Router) error {
key := fmt.Sprintf("contact_%d", i)
selections[key] = false
}
return SelectionStore{
return &BulkUpdateSelectionStore{
Selections: selections,
}
}
activateButtonCSS := "flex gap-2 items-center px-4 py-2 bg-success-500 hover:bg-success-600 text-success-100 font-bold rounded-lg"
deactivateButtonCSS := "flex gap-2 items-center px-4 py-2 bg-error-500 hover:bg-error-600 text-error-100 font-bold rounded-lg"
contactsToNode := func(selectionStore SelectionStore, contacts []*ContactActive) ElementRenderer {
return DIV().
ID("bulk_update").
DATASTAR_STORE(selectionStore).
CLASS("flex flex-col gap-2").
Children(
TABLE().
Children(
CAPTION().CLASS("text-sm text-accent-300").Text("Select Rows And Activate Or Deactivate Below"),
THEAD(
TR(
TH(
INPUT().
CLASS("checkbox").
TYPE("checkbox").
DATASTAR_MODEL("selections.all").
DATASTAR_ON("change", "$$setAll('contact_', $selections.all)"),
),
TH(Text("Name")),
TH(Text("Email")),
TH(Text("Status")),
),
),
TBODY(
RangeI(contacts, func(i int, cs *ContactActive) ElementRenderer {
return contactToNode(i, cs, false)
}),
),
),
DIV().
CLASS("flex gap-2").
Children(
BUTTON(
material_symbols.AccountCircle(),
Text("Activate"),
).
DATASTAR_ON("click", "$$put('/examples/bulk_update/data/activate'); $selections.all = false; $$setAll('contact_', $selections.all)").
CLASS(activateButtonCSS),
BUTTON(
material_symbols.AccountCircleOff(),
Text("Deactivate"),
).
DATASTAR_ON("click", "$$put('/examples/bulk_update/data/deactivate'); $selections.all = false; $$setAll('contact_', $selections.all)").
CLASS(deactivateButtonCSS),
),
SignalStore,
)
}
examplesRouter.Route("/bulk_update/data", func(dataRouter chi.Router) {
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
datastar.RenderFragmentSelf(sse, contactsToNode(defaultSelectionStore(), contacts))
datastar.RenderFragmentTempl(sse, bulkUpdateContacts(defaultSelectionStore(), contacts))
})
setActivation := func(w http.ResponseWriter, r *http.Request, isActive bool) {
store := &SelectionStore{}
store := &BulkUpdateSelectionStore{}
if err := datastar.BodyUnmarshal(r, store); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@ -170,9 +99,9 @@ func setupExamplesBulkUpdate(examplesRouter chi.Router) error {
}
}
datastar.RenderFragment(
datastar.RenderFragmentTempl(
sse,
contactToNode(i, c, wasChanged && wasSelected),
bulkUpdateContact(i, c, wasChanged && wasSelected),
// datastar.WithSettleDuration(5*time.Second),
)
}
@ -181,12 +110,7 @@ func setupExamplesBulkUpdate(examplesRouter chi.Router) error {
for k := range store.Selections {
store.Selections[k] = false
}
datastar.RenderFragment(
sse,
DIV().DATASTAR_STORE(store),
// datastar.WithQuerySelector("#bulk_update"),
datastar.WithMergeType(datastar.FragmentMergeUpsertAttributes),
)
datastar.PatchStore(sse, store)
}
dataRouter.Put("/activate", func(w http.ResponseWriter, r *http.Request) {

View File

@ -0,0 +1,81 @@
package site
import (
"fmt"
)
templ bulkUpdateContact(i int, cs *ContactActive, wasChanged bool) {
{{ key := fmt.Sprintf("contact_%d", i) }}
<tr
id={ key }
class={
templ.KV("activate", wasChanged && cs.IsActive),
templ.KV("deactivate", wasChanged && !cs.IsActive),
}
>
<td class="flex items-center justify-center">
<input
class="checkbox"
type="checkbox"
data-model={ "selections." + key }
/>
</td>
<td>{ cs.Name }</td>
<td>{ cs.Email }</td>
<td>
if cs.IsActive {
Active
} else {
Inactive
}
</td>
</tr>
}
templ bulkUpdateContacts(store *BulkUpdateSelectionStore, contacts []*ContactActive) {
<div
id="bulk_update"
data-store={ templ.JSONString(store) }
class="flex flex-col gap-2"
>
<table class="table table-zebra">
<caption class="text-sm text-accent-300">Select Rows And Activate Or Deactivate Below</caption>
<thead>
<tr>
<th>
<input
class="checkbox"
type="checkbox"
data-model="selections.all"
data-on-change="$$setAll('contact_', $selections.all)"
/>
</th>
<th>Name</th>
<th>Email</th>
<th>Status</th>
</tr>
</thead>
<tbody>
for i, c := range contacts {
@bulkUpdateContact(i, c, false)
}
</tbody>
</table>
<div class="flex gap-2">
<button
data-on-click="$$put('/examples/bulk_update/data/activate'); $selections.all = false; $$setAll('contact_', $selections.all)"
class="btn btn-success"
>
@icon("material-symbols:account-circle")
Activate
</button>
<button
data-on-click="$$put('/examples/bulk_update/data/deactivate'); $selections.all = false; $$setAll('contact_', $selections.all)"
class="btn btn-error"
>
@icon("material-symbols:account-circle-off")
Deactivate
</button>
</div>
</div>
}

View File

@ -1,60 +1,15 @@
package site
import (
"log"
"net/http"
goaway "github.com/TwiN/go-away"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/go-chi/chi/v5"
)
func setupExamplesClickToEdit(examplesRouter chi.Router) error {
// tsBytes, err := staticFS.ReadFile("static/examples/click_to_edit.txt")
// if err != nil {
// return fmt.Errorf("error reading examples dir: %w", err)
// }
// examplesRouter.Route("/click_to_edit", func(exampleRouter chi.Router) {
type Contact struct {
FirstName string `json:"firstName,omitempty" san:"trim,xss,max=128"`
LastName string `json:"lastName,omitempty" san:"trim,xss,max=128"`
Email string `json:"email,omitempty" san:"trim,xss,max=128"`
}
// exampleRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
// examplePage(w, r)
// })
leftBtnCSS := "bg-primary-300 hover:bg-primary-400 text-primary-800 font-bold py-2 px-4 rounded-l"
rightBtnCSS := "bg-accent-300 hover:bg-accent-400 text-accent-800 font-bold py-2 px-4 rounded-r"
contactNode := func(c *Contact) ElementRenderer {
return DIV().
ID("contact_1").
CLASS("flex flex-col gap-2 max-w-sm").
Children(
LABEL(TextF("First Name: %s", goaway.Censor(c.FirstName))),
LABEL(TextF("Last Name: %s", goaway.Censor(c.LastName))),
LABEL(TextF("Email: %s", goaway.Censor(c.Email))),
DIV().
CLASS("join").
Children(
BUTTON().
CLASS(leftBtnCSS).
DATASTAR_ON("click", datastar.GET("/examples/click_to_edit/contact/1/edit")).
Text("Edit"),
BUTTON().
CLASS(rightBtnCSS).
DATASTAR_ON("click", datastar.PATCH("/examples/click_to_edit/contact/1/reset")).
Text("Reset"),
),
)
}
c1 := &Contact{}
c1 := &ClickToEditContactStore{}
resetContact := func() {
c1.FirstName = "John"
c1.LastName = "Doe"
@ -65,70 +20,30 @@ func setupExamplesClickToEdit(examplesRouter chi.Router) error {
examplesRouter.Route("/click_to_edit/contact/{id}", func(contactRouter chi.Router) {
contactRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(sse, contactNode(c1))
datastar.RenderFragmentTempl(sse, setupExamplesClickToEditUserComponent(c1))
})
labeledInput := func(label, id string) (container ElementRenderer, input *INPUTElement) {
input = INPUT().
ID(id).
CLASS("bg-accent-900 border-2 border-accent-600 text-accent-100 text-sm rounded-lg focus:ring-primary-400 focus:border-primary-400 block w-full p-2.5")
container = DIV(
LABEL().FOR(id).CLASS("block mb-2 text-sm font-medium text-accent-100").Text(label),
input,
)
return container, input
}
labelInputModel := func(label, id string) ElementRenderer {
container, input := labeledInput(label, id)
input.TYPE("text").DATASTAR_MODEL(id)
return container
}
contactRouter.Get("/edit", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(
datastar.RenderFragmentTempl(
sse,
DIV().
ID("contact_1").
CLASS("flex flex-col gap-2").
DATASTAR_STORE(c1).
Children(
labelInputModel("First Name", "firstName"),
labelInputModel("Last Name", "lastName"),
labelInputModel("Email", "email"),
DIV().
CLASS("inline-flex").
Children(
BUTTON().
CLASS(leftBtnCSS).
DATASTAR_ON("click", datastar.PUT("/examples/click_to_edit/contact/1")).
Text("Save"),
BUTTON().
CLASS(rightBtnCSS).
DATASTAR_ON("click", datastar.GET("/examples/click_to_edit/contact/1")).
Text("Cancel"),
),
),
setupExamplesClickToEditUserEdit(c1),
)
})
contactRouter.Patch("/reset", func(w http.ResponseWriter, r *http.Request) {
resetContact()
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(sse, contactNode(c1))
datastar.RenderFragmentTempl(sse, setupExamplesClickToEditUserComponent(c1))
})
contactRouter.Put("/", func(w http.ResponseWriter, r *http.Request) {
c := &Contact{}
c := &ClickToEditContactStore{}
if err := datastar.BodyUnmarshal(r, c); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Print(c)
if err := sanitizer.Sanitize(c); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
@ -136,10 +51,10 @@ func setupExamplesClickToEdit(examplesRouter chi.Router) error {
}
c1 = c // update the contact
datastar.RenderFragment(datastar.NewSSE(w, r), contactNode(c1))
sse := datastar.NewSSE(w, r)
datastar.RenderFragmentTempl(sse, setupExamplesClickToEditUserComponent(c1))
})
})
// })
return nil
}

View File

@ -0,0 +1,64 @@
package site
import (
goaway "github.com/TwiN/go-away"
)
type ClickToEditContactStore struct {
FirstName string `json:"firstName,omitempty" san:"trim,xss,max=128"`
LastName string `json:"lastName,omitempty" san:"trim,xss,max=128"`
Email string `json:"email,omitempty" san:"trim,xss,max=128"`
}
templ setupExamplesClickToEditUserComponent(store *ClickToEditContactStore) {
<div id="contact_1" class="flex flex-col max-w-sm gap-2">
<label>First Name: { goaway.Censor(store.FirstName) }</label>
<label>Last Name: { goaway.Censor(store.LastName) }</label>
<label>Email: { goaway.Censor(store.Email) }</label>
<div class="flex gap-4">
<button
class="btn btn-primary"
data-on-click="$$get('/examples/click_to_edit/contact/1/edit')"
>
Edit
</button>
<button
class="btn btn-secondary"
data-on-click="$$patch('/examples/click_to_edit/contact/1/reset')"
>
Reset
</button>
</div>
</div>
}
templ setupExamplesClickToEditUserEdit(store *ClickToEditContactStore) {
<div id="contact_1" class="flex flex-col gap-2" data-store={ templ.JSONString(store) }>
<label class="flex items-center gap-2 input input-bordered">
First Name
<input type="text" class="grow" data-model="firstName"/>
</label>
<label class="flex items-center gap-2 input input-bordered">
Last Name
<input type="text" class="grow" data-model="lastName"/>
</label>
<label class="flex items-center gap-2 input input-bordered">
Email
<input type="text" class="grow" data-model="email"/>
</label>
<div class="flex gap-4">
<button
class="btn btn-primary"
data-on-click="$$put('/examples/click_to_edit/contact/1')"
>
Save
</button>
<button
class="btn btn-secondary"
data-on-click="$$get('/examples/click_to_edit/contact/1')"
>
Cancel
</button>
</div>
</div>
}

View File

@ -1,116 +1,40 @@
package site
import (
"fmt"
"log"
"net/http"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/toolbelt"
"github.com/go-chi/chi/v5"
)
func setupExamplesClickToLoad(examplesRouter chi.Router) error {
renderAgentRow := func(i int) ElementRenderer {
return TR().
ID(fmt.Sprintf("agent_%d", i)).
Children(
TD().Text("Agent Smith"),
TD().TextF("void%d@null.org", i+1),
TD().CLASS("uppercase").TextF("%x", toolbelt.AliasHash(fmt.Sprint(i))),
)
}
type Input struct {
SidebarOpen bool `json:"_sidebarOpen"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}
renderAgentRows := func(input *Input) []ElementRenderer {
arr := make([]ElementRenderer, input.Limit)
for i := range arr {
arr[i] = renderAgentRow(i + input.Offset)
}
return arr
}
moreButton := func(input *Input) ElementRenderer {
expression := fmt.Sprintf(
"$offset=%d; $limit=%d; %s",
input.Offset+input.Limit,
input.Limit,
datastar.GET("/examples/click_to_load/data"),
)
return BUTTON().
ID("more_btn").
CLASS("btn btn-primary").
DATASTAR_ON("click", expression).
Text("Load More")
}
renderAgentsTable := func(input *Input) ElementRenderer {
arr := make([]int, input.Limit)
for i := range arr {
arr[i] = i + input.Offset
}
return DIV().
CLASS("flex flex-col gap-2").
Children(
TABLE().
Children(
CAPTION(Text("Agents")),
THEAD(
TR(
TH(Text("Name")),
TH(Text("Email")),
TH(Text("ID")),
),
),
TBODY().
ID("click_to_load_rows").
DATASTAR_STORE(input).
Children(
Group(renderAgentRows(input)...),
),
),
moreButton(input),
)
}
// clickToLoadRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
// examplePage(w, r)
// })
examplesRouter.Get("/click_to_load/data", func(w http.ResponseWriter, r *http.Request) {
input := &Input{}
if err := datastar.QueryStringUnmarshal(r, input); err != nil {
store := &ClickToLoadStore{}
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
if input.Limit < 1 {
input.Limit = 10
} else if input.Limit > 100 {
input.Limit = 100
if store.Limit < 1 {
store.Limit = 10
} else if store.Limit > 100 {
store.Limit = 100
}
if input.Offset < 0 {
input.Offset = 0
if store.Offset < 0 {
store.Offset = 0
}
sse := datastar.NewSSE(w, r)
if input.Offset == 0 {
datastar.RenderFragmentSelf(sse, renderAgentsTable(input))
if store.Offset == 0 {
datastar.RenderFragmentTempl(sse, ClickToEditAgentsTable(store))
} else {
datastar.RenderFragment(sse, moreButton(input))
for _, node := range renderAgentRows(input) {
datastar.RenderFragment(
datastar.RenderFragmentTempl(sse, ClickToLoadMoreButton(store))
for i := 0; i < store.Limit; i++ {
log.Printf("ClickToLoadAgentRow: %d", store.Offset+i)
datastar.RenderFragmentTempl(
sse,
node,
ClickToLoadAgentRow(store.Offset+i),
datastar.WithQuerySelectorID("click_to_load_rows"),
datastar.WithMergeAppendElement(),
)

View File

@ -0,0 +1,63 @@
package site
import (
"fmt"
"github.com/delaneyj/toolbelt"
)
type ClickToLoadStore struct {
Limit int `json:"limit"`
Offset int `json:"offset"`
}
templ ClickToLoadAgentRow(i int) {
<tr
id={ fmt.Sprintf("agent_%d", i) }
>
<td class="text-center">Agent Smith</td>
<td class="text-center">{ fmt.Sprintf("void%d@null.org", i+1) }</td>
<td class="text-center uppercase">{ fmt.Sprintf("%x", toolbelt.AliasHash(fmt.Sprint(i))) }</td>
</tr>
}
templ ClickToLoadMoreButton(input *ClickToLoadStore) {
<button
id="more_btn"
class="w-full btn btn-success"
data-on-click={ fmt.Sprintf(
"console.log('click to load more');$offset=%d; $limit=%d; $$get('/examples/click_to_load/data', {useViewTransitions:false})",
input.Offset+input.Limit,input.Limit,
) }
>
Load More
</button>
}
templ ClickToEditAgentsTable(input *ClickToLoadStore) {
<div
id="click_to_load"
class="flex flex-col gap-2"
data-store={ templ.JSONString(input) }
>
<table class="table table-zebra">
<caption>Agents</caption>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>ID</th>
</tr>
</thead>
<tbody
id="click_to_load_rows"
data-star-store={ templ.JSONString(input) }
>
for i := 0; i < input.Limit; i++ {
@ClickToLoadAgentRow(i + input.Offset)
}
</tbody>
</table>
@ClickToLoadMoreButton(input)
</div>
}

View File

@ -6,8 +6,6 @@ import (
"strconv"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
"github.com/go-chi/chi/v5"
"github.com/samber/lo"
)
@ -16,72 +14,16 @@ func setupExamplesDeleteRow(examplesRouter chi.Router) error {
contacts := starterActiveContacts()
contactNode := func(i int, cs *ContactActive) ElementRenderer {
return TR().
ID(fmt.Sprintf("contact_%d", cs.ID)).
Children(
TD(Text(cs.Name)),
TD(Text(cs.Email)),
TD(Tern(cs.IsActive, Text("Active"), Text("Inactive"))),
TD().
CLASS("flex justify-end").
Children(
BUTTON().
CLASS("flex gap-2 items-center px-4 py-2 bg-error-600 hover:bg-error-500 rounded-lg").
DATASTAR_ON("click", fmt.Sprintf(
`confirm('Are you sure?') && %s`,
datastar.DELETE("/examples/delete_row/data/%d", cs.ID),
)).
Children(
material_symbols.Delete(),
Text("Delete"),
),
),
)
}
contactsToNode := func() ElementRenderer {
return DIV().
ID("contacts").
CLASS("flex flex-col gap-8").
Children(
TABLE().
CLASS("table w-full").
Children(
CAPTION(Text("Contacts")),
THEAD(
TR(
TH(Text("Name")),
TH(Text("Email")),
TH(Text("Status")),
TH(Text("Actions")).CLASS("text-right")),
),
TBODY(
RangeI(contacts, contactNode),
),
),
DIV(
BUTTON().
CLASS("flex items-center gap-2 px-4 py-2 rounded-lg bg-primary-400 hover:bg-primary-500").
DATASTAR_ON("click", datastar.GET("/examples/delete_row/data/reset")).
Children(
material_symbols.Refresh(),
Text("Reset"),
),
),
)
}
examplesRouter.Route("/delete_row/data", func(dataRouter chi.Router) {
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
datastar.RenderFragmentSelf(sse, contactsToNode())
datastar.RenderFragmentTempl(sse, deleteRowContacts(contacts))
})
dataRouter.Get("/reset", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
contacts = starterActiveContacts()
datastar.RenderFragment(sse, contactsToNode())
datastar.RenderFragmentTempl(sse, deleteRowContacts(contacts))
})
dataRouter.Delete("/{id}", func(w http.ResponseWriter, r *http.Request) {

View File

@ -0,0 +1,56 @@
package site
import "fmt"
templ deleteRowContact(cs *ContactActive) {
<tr id={ fmt.Sprintf("contact_%d", cs.ID) }>
<td class="text-center">{ cs.Name }</td>
<td class="text-center">{ cs.Email }</td>
<td class="text-center">
if cs.IsActive {
Active
} else {
Inactive
}
</td>
<td class="flex justify-end">
<button
class="btn btn-error"
data-on-click={ fmt.Sprintf(`confirm('Are you sure?') && $$delete("/examples/delete_row/data/%d")`, cs.ID) }
>
@icon("material-symbols:delete")
Delete
</button>
</td>
</tr>
}
templ deleteRowContacts(contacts []*ContactActive) {
<div id="delete_row" class="flex flex-col gap-8">
<table class="table w-full table-zebra">
<caption>Contacts</caption>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
for _, cs := range contacts {
@deleteRowContact(cs)
}
</tbody>
</table>
<div>
<button
class="btn btn-primary"
data-on-click="$$get('/examples/delete_row/data/reset')"
>
@icon("material-symbols:refresh")
Reset
</button>
</div>
</div>
}

View File

@ -4,71 +4,25 @@ import (
"net/http"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
"github.com/go-chi/chi/v5"
)
func setupExamplesDialogsBrowser(examplesRouter chi.Router) error {
type Store struct {
Prompt string `json:"prompt"`
Confirm bool `json:"confirm"`
}
examplesRouter.Get("/dialogs_browser/data", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(
sse,
BUTTON().
ID("dialogs").
CLASS("flex items-center justify-center gap-1 px-2 py-1 rounded-sm text-xs bg-primary-600 hover:bg-primary-500").
DATASTAR_STORE(&Store{Prompt: "foo"}).
DATASTAR_ON("click", `$prompt = prompt('Enter a string',$prompt);$confirm = confirm('Are you sure?');$confirm && $$get('/examples/dialogs_browser/sure')`).
Children(
Text("Click Me"),
material_symbols.QuestionMark(),
),
)
store := &DialogBrowserStore{Prompt: "foo"}
datastar.RenderFragmentTempl(sse, DialogBrowserView(store))
})
examplesRouter.Get("/dialogs_browser/sure", func(w http.ResponseWriter, r *http.Request) {
store := &Store{}
store := &DialogBrowserStore{}
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(
sse,
DIV().
ID("dialogs").
CLASS("flex flex-col gap-4").
Children(
Tern(
store.Confirm,
Group(
DIV(
TextF("You clicked the button and confirmed with prompt of "),
SPAN().CLASS("font-bold text-accent").Text(store.Prompt),
Text("!"),
),
BUTTON().
CLASS("flex items-center gap-1 px-2 py-1 rounded-sm text-xs bg-accent-600 hover:bg-accent-500").
DATASTAR_ON("click", datastar.GET("/examples/dialogs_browser/data")).
Children(
material_symbols.ArrowBack(),
Text("Reset"),
),
),
DIV().
CLASS("flex gap-2 items-center justify-between font-regular relative mb-4 block w-full rounded-lg bg-red-500 p-4 text-base leading-5 text-white opacity-100").
Children(
material_symbols.ErrorIcon(),
Text("You clicked the button and did not confirm! Should not see this"),
),
),
),
)
datastar.RenderFragmentTempl(sse, DialogBrowserSure(store))
})
return nil

View File

@ -0,0 +1,37 @@
package site
type DialogBrowserStore struct {
Prompt string `json:"prompt"`
Confirm bool `json:"confirm"`
}
templ DialogBrowserView(store *DialogBrowserStore) {
<button
id="dialogs"
class="flex items-center btn btn-primary"
data-store={ templ.JSONString(store) }
data-on-click="$prompt = prompt('Enter a string',$prompt);$confirm = confirm('Are you sure?');$confirm && $$get('/examples/dialogs_browser/sure')"
>
Click Me
@icon("material-symbols:question-mark")
</button>
}
templ DialogBrowserSure(store *DialogBrowserStore) {
<div id="dialogs" class="flex flex-col gap-4">
if store.Confirm {
<div>
You clicked the button and confirmed with prompt of <span class="font-bold text-accent">{ store.Prompt }</span>!
</div>
<button class="flex items-center gap-2 btn btn-accent" data-on-click="$$get('/examples/dialogs_browser/data')">
@icon("material-symbols:arrow-back")
Reset
</button>
} else {
<div class="alert alert-error">
@icon("material-symbols:error-icon")
You clicked the button and did not confirm! Should not see this
</div>
}
</div>
}

View File

@ -1,27 +1,25 @@
package site
import (
"fmt"
"net/http"
"time"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/go-chi/chi/v5"
)
func setupExamplesDisableButton(examplesRouter chi.Router) error {
examplesRouter.Get("/disable_button/data", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
store := map[string]any{
"shouldDisable": false,
}
fragment := fmt.Sprintf(`<div>The time is %s</div>`, time.Now().UTC().Format(time.RFC3339))
datastar.RenderFragmentString(sse, fragment, datastar.WithMergeAppendElement())
time.Sleep(1 * time.Second)
datastar.RenderFragment(sse,
DIV().TextF("The time is %s", time.Now().UTC().Format(time.RFC3339)),
datastar.WithQuerySelectorID("results"),
datastar.WithMergeAppendElement(),
)
datastar.RenderFragment(sse, DIV().ID("container").DATASTAR_STORE(store), datastar.WithMergeUpsertAttributes())
datastar.PatchStore(sse, map[string]any{
"shouldDisable": false,
})
})
return nil

View File

@ -2,13 +2,10 @@ package site
import (
"fmt"
"log"
"net/http"
"strconv"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
"github.com/go-chi/chi/v5"
)
@ -41,134 +38,19 @@ func starterEditContacts() []*ContactEdit {
func setupExamplesEditRow(examplesRouter chi.Router) error {
contacts := starterEditContacts()
type Store struct {
EditRowIndex int `json:"editRowIndex"`
Name string `json:"name"`
Email string `json:"email"`
}
contactNode := func(i int, isEditingRow, isEditingAnyRow bool) ElementRenderer {
contact := contacts[i]
contactKeyPrefix := fmt.Sprintf("contact_%d", i)
return TR().
ID(contactKeyPrefix).
Children(
TD(Tern(
isEditingRow,
INPUT().
TYPE("text").
CLASS("bg-accent-900 border-2 border-accent-600 text-accent-100 text-sm rounded-lg focus:ring-primary-400 focus:border-primary-400 block w-full p-2.5").
DATASTAR_MODEL("name").
CustomData("testid", contactKeyPrefix+"_name"),
DIV().Text(contact.Name),
)),
TD(Tern(
isEditingRow,
INPUT().
TYPE("text").
CLASS("bg-accent-900 border-2 border-accent-600 text-accent-100 text-sm rounded-lg focus:ring-primary-400 focus:border-primary-400 block w-full p-2.5").
DATASTAR_MODEL("email").
CustomData("testid", contactKeyPrefix+"_email"),
DIV().Text(contact.Email),
)),
TD().
CLASS("flex justify-end").
Children(
DynTern(
isEditingAnyRow,
func() ElementRenderer {
return DIV().
CLASS("flex gap-2").
Children(
BUTTON().
CLASS("flex items-center gap-1 px-2 py-1 rounded-sm text-xs bg-primary-600 hover:bg-primary-500").
DATASTAR_ON("click", datastar.GET("/examples/edit_row/data")).
CustomData("testid", contactKeyPrefix+"_cancel").
Children(
material_symbols.Cancel(),
Text("Cancel"),
),
BUTTON().
CLASS("flex items-center gap-1 px-2 py-1 rounded-sm text-xs bg-success-600 hover:bg-success-500").
DATASTAR_ON("click", datastar.PATCH("/examples/edit_row/edit")).
CustomData("testid", contactKeyPrefix+"_save").
Children(
material_symbols.Save(),
Text("Save"),
),
)
},
func() ElementRenderer {
return BUTTON().
CLASS("flex items-center gap-1 px-2 py-1 rounded-sm text-xs bg-accent-600 hover:bg-accent-500").
DATASTAR_ON("click", fmt.Sprintf(
"$editRowIndex = %d; %s", i,
datastar.GET("/examples/edit_row/edit"),
)).
CustomData("testid", contactKeyPrefix+"_edit").
Children(
material_symbols.Edit(),
Text("Edit"),
)
},
),
),
)
}
contactsToNode := func(store *Store) ElementRenderer {
return DIV().
ID("edit_row").
CLASS("flex flex-col").
DATASTAR_STORE(store).
Children(
TABLE().
CLASS("table w-full").
Children(
CAPTION(Text("Contacts")),
THEAD(
TR(
TH(Text("Name")),
TH(Text("Email")),
TH(Text("Actions")).CLASS("text-right"),
),
),
TBODY().
ID("edit_row_table_body").
Children(
RangeI(contacts, func(i int, cs *ContactEdit) ElementRenderer {
log.Print(cs)
return contactNode(i, i == store.EditRowIndex, store.EditRowIndex != -1)
}),
),
),
DIV(
BUTTON().
CLASS("flex items-center gap-2 px-4 py-2 rounded-lg bg-primary-600 hover:bg-primary-500").
DATASTAR_ON("click", datastar.GET("/examples/edit_row/reset")).
CustomData("testid", "reset").
Children(
material_symbols.Refresh(),
Text("Reset"),
),
),
)
}
emptyStore := &Store{EditRowIndex: -1}
emptyStore := &EditRowStore{EditRowIndex: -1}
examplesRouter.Get("/edit_row/reset", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
contacts = starterEditContacts()
datastar.RenderFragment(sse, contactsToNode(emptyStore))
datastar.RenderFragmentTempl(sse, EditRowContacts(contacts, emptyStore))
})
examplesRouter.Route("/edit_row/data", func(dataRouter chi.Router) {
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(sse, contactsToNode(emptyStore))
datastar.RenderFragmentTempl(sse, EditRowContacts(contacts, emptyStore))
})
dataRouter.Get("/{index}", func(w http.ResponseWriter, r *http.Request) {
@ -179,15 +61,19 @@ func setupExamplesEditRow(examplesRouter chi.Router) error {
http.Error(w, fmt.Sprintf("error parsing index: %s", err), http.StatusBadRequest)
return
}
store := &Store{EditRowIndex: i, Name: contacts[i].Name, Email: contacts[i].Email}
store := &EditRowStore{
EditRowIndex: i,
Name: contacts[i].Name,
Email: contacts[i].Email,
}
datastar.RenderFragment(sse, contactsToNode(store))
datastar.RenderFragmentTempl(sse, EditRowContacts(contacts, store))
})
})
examplesRouter.Route("/edit_row/edit", func(editRouter chi.Router) {
editRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
store := &Store{}
store := &EditRowStore{}
if err := datastar.QueryStringUnmarshal(r, &store); err != nil {
http.Error(w, fmt.Sprintf("error unmarshalling contact : %s", err), http.StatusBadRequest)
}
@ -199,14 +85,18 @@ func setupExamplesEditRow(examplesRouter chi.Router) error {
i := store.EditRowIndex
c := contacts[i]
store = &Store{EditRowIndex: i, Name: c.Name, Email: c.Email}
store = &EditRowStore{
EditRowIndex: i,
Name: c.Name,
Email: c.Email,
}
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(sse, contactsToNode(store))
datastar.RenderFragmentTempl(sse, EditRowContacts(contacts, store))
})
editRouter.Patch("/", func(w http.ResponseWriter, r *http.Request) {
store := &Store{}
store := &EditRowStore{}
if err := datastar.BodyUnmarshal(r, &store); err != nil {
http.Error(w, fmt.Sprintf("error unmarshalling store : %s", err), http.StatusBadRequest)
return
@ -222,7 +112,7 @@ func setupExamplesEditRow(examplesRouter chi.Router) error {
c.Email = store.Email
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(sse, contactsToNode(emptyStore))
datastar.RenderFragmentTempl(sse, EditRowContacts(contacts, emptyStore))
})
})

View File

@ -0,0 +1,111 @@
package site
import (
"fmt"
)
type EditRowStore struct {
EditRowIndex int `json:"editRowIndex"`
Name string `json:"name"`
Email string `json:"email"`
}
templ EditRowContact(contacts []*ContactEdit, i int, isEditingRow, isEditingAnyRow bool) {
{{
contact := contacts[i]
contactKeyPrefix := fmt.Sprintf("contact_%d", i)
}}
<tr
id={ contactKeyPrefix }
>
<td class="text-center">
if isEditingRow {
<input
type="text"
class="input input-bordered"
data-model="name"
data-testid={ contactKeyPrefix + "_name" }
/>
} else {
<div>{ contact.Name }</div>
}
</td>
<td class="text-center">
if isEditingRow {
<input
type="text"
class="input input-bordered"
data-model="email"
data-testid={ contactKeyPrefix + "_email" }
/>
} else {
<div>{ contact.Email }</div>
}
</td>
<td class="text-right">
if isEditingAnyRow {
<div class="flex gap-2">
<button
class="btn btn-sm btn-error"
data-on-click="$$get('/examples/edit_row/data')"
data-testid={ contactKeyPrefix + "_cancel" }
>
@icon("material-symbols:cancel")
Cancel
</button>
<button
class="btn btn-sm btn-success"
data-on-click="$$patch('/examples/edit_row/edit')"
data-testid={ contactKeyPrefix + "_save" }
>
@icon("material-symbols:save")
Save
</button>
</div>
} else {
<button
class="btn btn-sm btn-accent"
data-on-click={ fmt.Sprintf("$editRowIndex = %d; $$get('/examples/edit_row/edit')", i) }
data-testid={ contactKeyPrefix + "_edit" }
>
@icon("material-symbols:edit")
Edit
</button>
}
</td>
</tr>
}
templ EditRowContacts(contacts []*ContactEdit, store *EditRowStore) {
<div
id="edit_row"
class="flex flex-col"
data-store={ templ.JSONString(store) }
>
<table class="table w-full">
<caption>Contacts</caption>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th class="text-righ">Actions</th>
</tr>
</thead>
<tbody id="edit_row_table_body">
for i := range contacts {
@EditRowContact(contacts, i, i == store.EditRowIndex, store.EditRowIndex != -1)
}
</tbody>
</table>
<div>
<button
class="btn btn-warning"
data-on-click="$$get('/examples/edit_row/reset')"
data-testid="reset"
>
@icon("material-symbols:refresh")
Reset
</button>
</div>
</div>
}

View File

@ -5,7 +5,6 @@ import (
"time"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/go-chi/chi/v5"
)
@ -13,14 +12,9 @@ func setupExamplesFetchIndicator(examplesRouter chi.Router) error {
examplesRouter.Get("/fetch_indicator/greet", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(sse, DIV().ID("greeting").Text(""))
datastar.RenderFragmentTempl(sse, fetchIndicatorEmpty())
time.Sleep(2 * time.Second)
datastar.RenderFragment(
sse,
DIV().
ID("greeting").
TextF("Hello, the time is %s", time.Now().Format(time.RFC3339)),
)
datastar.RenderFragmentTempl(sse, fetchIndicatorGreeting())
})
return nil

View File

@ -0,0 +1,13 @@
package site
import (
"time"
)
templ fetchIndicatorEmpty() {
<div id="greeting">No data</div>
}
templ fetchIndicatorGreeting() {
<div id="greeting">Hello, the time is { time.Now().Format(time.RFC3339) }</div>
}

View File

@ -6,11 +6,8 @@ import (
"fmt"
"io"
"net/http"
"strings"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
"github.com/dustin/go-humanize"
"github.com/go-chi/chi/v5"
"github.com/goccy/go-json"
@ -18,48 +15,15 @@ import (
)
func setupExamplesFileUpload(examplesRouter chi.Router) error {
type Store struct {
FilesBase64 []string `json:"files"`
FileMimes []string `json:"filesMimes"`
FileNames []string `json:"filesNames"`
}
examplesRouter.Get("/file_upload/data", func(w http.ResponseWriter, r *http.Request) {
store := &Store{
store := &FileUploadStore{
FilesBase64: []string{},
FileMimes: []string{},
FileNames: []string{},
}
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(
sse,
DIV().
ID("file_upload").
CLASS("flex flex-col gap-4").
DATASTAR_STORE(store).
Children(
DIV().
CLASS("flex flex-col gap-2").
Children(
LABEL().
CLASS("block mb-2 text-sm font-medium text-primary-100").
FOR("file_input").
Text("Pick anything reasonably sized"),
INPUT().
ID("file_input").
TYPE("file").
DATASTAR_MODEL("files").
MULTIPLE().
CLASS("block w-full text-sm border rounded-lg cursor-pointer text-primary-400 focus:outline-none bg-primary-700 border-primary-600 placeholder-primary-500"),
),
BUTTON().
CLASS("bg-primary-300 hover:bg-primary-400 text-primary-800 font-bold py-2 px-4 rounded-l").
Text("Submit").
DATASTAR_ON("click", datastar.POST("/examples/file_upload/upload")),
PRE().DATASTAR_TEXT("JSON.stringify(ctx.store().value,null,2)"),
),
)
datastar.RenderFragmentTempl(sse, FileUploadView(store))
})
examplesRouter.Post("/file_upload/upload", func(w http.ResponseWriter, r *http.Request) {
@ -72,32 +36,14 @@ func setupExamplesFileUpload(examplesRouter chi.Router) error {
if len(data) >= maxBytesSize {
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(
sse,
DIV().
ID("file_upload").
CLASS("alert alert-error").
Children(
material_symbols.ErrorIcon(),
TextF("Error: %s", err),
),
)
datastar.RenderFragmentTempl(sse, FileUpdateAlert(err))
return
}
store := &Store{}
store := &FileUploadStore{}
sse := datastar.NewSSE(w, r)
if err := json.Unmarshal(data, store); err != nil {
datastar.RenderFragment(
sse,
DIV().
ID("file_upload").
CLASS("alert alert-error").
Children(
material_symbols.ErrorIcon(),
TextF("Error: %s", err),
),
)
datastar.RenderFragmentTempl(sse, FileUpdateAlert(err))
return
}
@ -119,33 +65,7 @@ func setupExamplesFileUpload(examplesRouter chi.Router) error {
humainzeByteCount[i] = humanize.Bytes(uint64(len(file)))
}
datastar.RenderFragment(
sse,
DIV().
ID("file_upload").
CLASS("card bg-base-300").
Children(
DIV().
CLASS("card-body").
Children(
TABLE().
CLASS("table table-zebra").
Children(
CAPTION(Text("File Upload Results")),
TBODY().
Children(
TR(TH(Text("File Names")), TD(Text(strings.Join(store.FileNames, ", ")))),
TR(TH(Text("File Sizes")), TD(Text(strings.Join(humainzeByteCount, ", ")))),
TR(TH(Text("File Mimes")), TD(Text(strings.Join(store.FileMimes, ", ")))),
TR(
TH(Text("XXH3 Hashes")),
TD().CLASS("text-ellipsis overflow-hidden").TextF(strings.Join(humanizedHashes, ", ")),
),
),
),
),
),
)
datastar.RenderFragmentTempl(sse, FileUploadResults(store, humainzeByteCount, humanizedHashes))
})
return nil

View File

@ -0,0 +1,74 @@
package site
import (
"github.com/delaneyj/datastar"
)
type FileUploadStore struct {
FilesBase64 []string `json:"files"`
FileMimes []string `json:"filesMimes"`
FileNames []string `json:"filesNames"`
}
templ FileUploadView(store *FileUploadStore) {
<div
id="file_upload"
class="flex flex-col gap-4"
data-store={ templ.JSONString(store) }
>
<div class="flex flex-col gap-2">
<label
for="file_input"
class="block mb-2 text-sm font-medium text-primary-100"
>
Pick anything reasonably sized
</label>
<input
id="file_input"
type="file"
data-model="files"
multiple
class="w-full file-input file-input-bordered"
/>
</div>
<button
class="btn btn-primary"
data-on-click="$$post('/examples/file_upload/upload')"
data-show="!!$files?.length"
>
Submit
</button>
@datastar.TemplSignalStoreView()
</div>
}
templ FileUpdateAlert(err error) {
<div id="file_upload" class="alert alert-error">
@icon("material-symbols:error-icon")
Error: { err.Error() }
</div>
}
templ FileUploadResults(store *FileUploadStore, humainzeByteCount, humanizedHashes []string) {
<table id="file_upload" class="table w-full table-compact table-zebra">
<caption>File Upload Results</caption>
<thead>
<tr>
<th>Name</th>
<th>Sizes</th>
<th>Mimes</th>
<th>XXH3 Hashes</th>
</tr>
</thead>
<tbody>
for i := range store.FileNames {
<tr>
<td class="font-bold text-center">{ store.FileNames[i] }</td>
<td class="text-center">{ humainzeByteCount[i] }</td>
<td class="text-center">{ store.FileMimes[i] }</td>
<td class="overflow-hidden font-mono text-xs text-center text-ellipsis">{ humanizedHashes[i] }</td>
</tr>
}
</tbody>
</table>
}

View File

@ -1,101 +1,16 @@
package site
import (
"fmt"
"net/http"
"time"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/gostar/elements/iconify/svg_spinners"
"github.com/delaneyj/toolbelt"
"github.com/go-chi/chi/v5"
)
func setupExamplesInfiniteScroll(examplesRouter chi.Router) error {
type Store struct {
Limit int `json:"limit"`
Offset int `json:"offset"`
}
renderAgentRow := func(i int) ElementRenderer {
return TR().
ID(fmt.Sprintf("agent_%d", i)).
Children(
TD(TextF("Agent Smith %x", i)),
TD(TextF("void%d@null.org", i+1)),
TD().CLASS("uppercase").TextF("%x", toolbelt.AliasHash(fmt.Sprint(i))),
)
}
renderAgentRows := func(input *Store) []ElementRenderer {
arr := make([]ElementRenderer, input.Limit)
for i := range arr {
arr[i] = renderAgentRow(i + input.Offset)
}
return arr
}
moreDiv := func(store *Store) ElementRenderer {
return DIV().
ID("more_btn").
DATASTAR_INTERSECTS(fmt.Sprintf(
"$offset=%d;$limit=%d;%s",
store.Offset+store.Limit,
store.Limit,
datastar.GET("/examples/infinite_scroll/data"),
)).
Children(
DIV().
CLASS("flex justify-center text-4xl gap-2").
CustomData("testid", "loading_message").
Children(
svg_spinners.BlocksWave(),
Text("Loading..."),
),
)
}
renderAgentsTable := func(store *Store) ElementRenderer {
arr := make([]int, store.Limit)
for i := range arr {
arr[i] = i + store.Offset
}
return DIV().
ID("infinite_scroll").
DATASTAR_STORE(store).
CLASS("flex flex-col gap-2").
Children(
TABLE().
CLASS("table w-full").
Children(
CAPTION(Text("Agents")),
THEAD(
TR(
TH(Text("Name")),
TH(Text("Email")),
TH(Text("ID")),
),
),
TBODY().
ID("click_to_load_rows").
Children(
Group(renderAgentRows(store)...),
),
),
moreDiv(store),
)
}
// infiniteScrollingRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
// examplePage(w, r)
// })
examplesRouter.Get("/infinite_scroll/data", func(w http.ResponseWriter, r *http.Request) {
store := &Store{}
store := &infiniteScrollStore{}
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@ -107,16 +22,13 @@ func setupExamplesInfiniteScroll(examplesRouter chi.Router) error {
sse := datastar.NewSSE(w, r)
if store.Offset == 0 {
datastar.RenderFragment(sse, renderAgentsTable(store))
datastar.RenderFragmentTempl(sse, infiniteScrollAgents(store))
} else {
time.Sleep(2 * time.Second)
datastar.RenderFragment(
sse, moreDiv(store),
datastar.WithQuerySelectorID("more_btn"),
)
for _, node := range renderAgentRows(store) {
datastar.RenderFragment(
sse, node,
datastar.RenderFragmentTempl(sse, infiniteScrollMore(store))
for i := 0; i < store.Limit; i++ {
datastar.RenderFragmentTempl(
sse, infiniteScrollAgent(store.Offset+i),
datastar.WithQuerySelectorID("click_to_load_rows"),
datastar.WithMergeAppendElement(),
)

View File

@ -0,0 +1,59 @@
package site
import (
"fmt"
"github.com/delaneyj/toolbelt"
)
type infiniteScrollStore struct {
Limit int `json:"limit"`
Offset int `json:"offset"`
}
templ infiniteScrollMore(store *infiniteScrollStore) {
<div
id="loading_message"
class="alert alert-info"
data-intersects={ fmt.Sprintf(
"$offset=%d;$limit=%d;$$get('/examples/infinite_scroll/data')",
store.Offset+store.Limit,
store.Limit,
) }
>
@icon("svg-spinners:blocks-wave")
Loading...
</div>
}
templ infiniteScrollAgent(i int) {
<tr id={ fmt.Sprintf("agent_%d", i) }>
<td>Agent Smith { fmt.Sprint(i) }</td>
<td>{ fmt.Sprintf("void%d@null.org", i+1) }</td>
<td class="uppercase">{ fmt.Sprintf("%x", toolbelt.AliasHash(fmt.Sprint(i))) }</td>
</tr>
}
templ infiniteScrollAgents(store *infiniteScrollStore) {
<div
id="infinite_scroll"
data-store={ templ.JSONString(store) }
class="flex flex-col gap-2"
>
<table class="table w-full table-zebra">
<caption>Agents</caption>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>ID</th>
</tr>
</thead>
<tbody id="click_to_load_rows">
for i := 0; i < store.Limit; i++ {
@infiniteScrollAgent(store.Offset + i)
}
</tbody>
</table>
@infiniteScrollMore(store)
</div>
}

View File

@ -2,25 +2,16 @@ package site
import (
"net/http"
"time"
"github.com/a-h/templ"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
"github.com/go-chi/chi/v5"
)
func setupExampleInlineValidation(examplesRouter chi.Router) error {
examplesRouter.Route("/inline_validation/data", func(dataRouter chi.Router) {
type User struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"email"`
}
userValidation := func(u *User) (isEmailValid bool, isFirstNameValid bool, isLastNameValid bool, isValid bool) {
userValidation := func(u *inlineValidationUser) (isEmailValid bool, isFirstNameValid bool, isLastNameValid bool, isValid bool) {
isEmailValid = u.Email == "test@test.com"
isFirstNameValid = len(u.FirstName) > 8
isLastNameValid = len(u.LastName) > 8
@ -28,103 +19,35 @@ func setupExampleInlineValidation(examplesRouter chi.Router) error {
return isEmailValid, isFirstNameValid, isLastNameValid, isValid
}
fieldToNode := func(label, field string, isValid bool, isNotValidErrorLabelFmt string, labelArgs ...any) ElementRenderer {
return DIV().
CLASS("form-control").
Children(
LABEL().
CLASS("label").
Children(
SPAN().
CLASS("label-text").
Text(label),
),
INPUT().
CLASS("bg-accent-900 border-2 border-accent-600 text-accent-100 text-sm rounded-lg focus:ring-primary-400 focus:border-primary-400 block w-full p-2.5").
IfCLASS(!isValid, "border-error").
DATASTAR_MODEL(field).
DATASTAR_ON("keydown", datastar.GET("/examples/inline_validation/data"), InputOnModDebounce(2*time.Second)).
CustomData("testid", "input_"+field),
If(!isValid, LABEL().CLASS("text-sm font-bold text-error-400").CustomData("testid", "validation_"+field).TextF(isNotValidErrorLabelFmt, labelArgs...)),
)
}
userToNode := func(u *User) ElementRenderer {
isEmailValid, isFirstNameValid, isLastNameValid, isValid := userValidation(u)
_, _ = isEmailValid, isFirstNameValid
return DIV().
ID("inline_validation").
CLASS("flex flex-col gap-4").
DATASTAR_STORE(u).
Children(
DIV().CLASS("font-bold text-2xl").Text("Sign Up"),
DIV(
fieldToNode(
"Email Address",
"email",
isEmailValid,
"Email '%s' is already taken or is invalid. Please enter another email.", u.Email,
),
fieldToNode(
"First Name",
"firstName",
isFirstNameValid,
"First name must be at least 8 characters.",
),
fieldToNode(
"Last Name",
"lastName",
isLastNameValid,
"Last name must be at least 8 characters.",
),
),
BUTTON().
CLASS("flex items-center gap-2 bg-success-300 hover:bg-success-400 text-success-800 font-bold py-2 px-4").
IfCLASS(!isValid, "disabled").
DATASTAR_ON("click", datastar.POST("/examples/inline_validation/data")).
CustomData("testid", "submit_button").
Children(
material_symbols.PersonAdd(),
Text("Submit"),
),
SignalStore,
)
}
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
u := &User{}
u := &inlineValidationUser{}
if err := datastar.QueryStringUnmarshal(r, u); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(sse, userToNode(u))
isEmailValid, isFirstNameValid, isLastNameValid, isValid := userValidation(u)
datastar.RenderFragmentTempl(sse, inlineValidationUserComponent(u, isEmailValid, isFirstNameValid, isLastNameValid, isValid))
})
dataRouter.Post("/", func(w http.ResponseWriter, r *http.Request) {
u := &User{}
u := &inlineValidationUser{}
if err := datastar.BodyUnmarshal(r, u); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_, _, _, isValid := userValidation(u)
isEmailValid, isFirstNameValid, isLastNameValid, isValid := userValidation(u)
sse := datastar.NewSSE(w, r)
var node ElementRenderer
var node templ.Component
if !isValid {
node = userToNode(u)
node = inlineValidationUserComponent(u, isEmailValid, isFirstNameValid, isLastNameValid, isValid)
} else {
node = DIV().
ID("inline_validation").
CLASS("font-bold text-4xl alert alert-success").
Children(
material_symbols.CheckCircle(),
Text("Thank you for signing up!"),
)
node = inlineValidationThankYou()
}
datastar.RenderFragment(sse, node)
datastar.RenderFragmentTempl(sse, node)
})
})

View File

@ -0,0 +1,58 @@
package site
import (
"fmt"
"github.com/delaneyj/datastar"
)
type inlineValidationUser struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"email"`
}
templ inlineValidationFieldComponent(label, field string, isValid bool, isNotValidErrorLabelFmt string, labelArgs ...any) {
<div class="form-control">
<label class="label">
<span class="label-text">{ label }</span>
</label>
<input
class={ "input input-bordered", templ.KV("input-error",!isValid) }
data-model={ field }
data-on-keydown.debounce_500ms="$$get('/examples/inline_validation/data')"
data-testid={ "input_" + field }
/>
if !isValid {
<label class="text-sm font-bold text-error" data-testid={ "validation_" + field }>{ fmt.Sprintf( isNotValidErrorLabelFmt, labelArgs...) }</label>
}
</div>
}
templ inlineValidationUserComponent(u *inlineValidationUser, isEmailValid, isFirstNameValid, isLastNameValid, isValid bool) {
<div id="inline_validation" class="flex flex-col gap-4" data-store={ templ.JSONString(u) }>
<div class="text-2xl font-bold">Sign Up</div>
<div>
@inlineValidationFieldComponent("Email Address", "email", isEmailValid, "Email '%s' is already taken or is invalid. Please enter another email.", u.Email)
@inlineValidationFieldComponent("First Name", "firstName", isFirstNameValid, "First name must be at least 8 characters.")
@inlineValidationFieldComponent("Last Name", "lastName", isLastNameValid, "Last name must be at least 8 characters.")
</div>
<button
class="btn btn-success"
disabled?={ !isValid }
data-on-click="$$post('/examples/inline_validation/data')"
data-testid="submit_button"
>
@icon("material-symbols:person-add")
Add User
</button>
<div class="divider"></div>
@datastar.TemplSignalStoreView()
</div>
}
templ inlineValidationThankYou() {
<div id="inline_validation" class="alert alert-success">
@icon("material-symbols:check-circle")
Thank you for signing up!
</div>
}

View File

@ -1,30 +0,0 @@
package site
import (
"net/http"
"time"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/go-chi/chi/v5"
)
func setupExamplesIsLoadingId(examplesRouter chi.Router) error {
examplesRouter.Get("/is_loading_identifier/greet", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
d := 2 * time.Second
datastar.RenderFragment(sse, DIV().ID("greeting").TextF("Calculating... waiting for %s", d))
time.Sleep(d)
datastar.RenderFragment(
sse,
DIV().
ID("greeting").
TextF("Hello, the time is %s", time.Now().Format(time.RFC3339)),
)
})
return nil
}

View File

@ -5,40 +5,21 @@ import (
"time"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/gostar/elements/iconify/svg_spinners"
"github.com/go-chi/chi/v5"
)
func setupExamplesLazyLoad(examplesRouter chi.Router) error {
examplesRouter.Get("/lazy_load/data", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(
sse,
DIV().
ID("lazy_load").
DATASTAR_ON("load", datastar.GET("/examples/lazy_load/graph")).
Children(
DIV().
CLASS("flex justify-center text-4xl gap-2").
Children(
svg_spinners.BlocksWave(),
Text("Loading..."),
),
),
)
datastar.RenderFragmentTempl(sse, lazyLoadLoader())
})
examplesRouter.Get("/lazy_load/graph", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(3 * time.Second)
sp := staticPath("images/examples/tokyo.png")
time.Sleep(2 * time.Second)
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(
datastar.RenderFragmentTempl(
sse,
IMG().
ID("lazy_load").
CLASS("transition-opacity").
SRC(sp),
lazyLoadGraph(),
datastar.WithSettleDuration(1*time.Second),
)
})

View File

@ -0,0 +1,14 @@
package site
templ lazyLoadLoader() {
<div id="lazy_load" data-on-load="$$get('/examples/lazy_load/graph')">
<div class="flex justify-center gap-2 text-4xl">
@icon("svg-spinners:blocks-wave")
Loading...
</div>
</div>
}
templ lazyLoadGraph() {
<img id="lazy_load" class="transition-opacity" src={ staticPath("images/examples/tokyo.png") }/>
}

View File

@ -1,73 +1,28 @@
package site
import (
"fmt"
"log"
"net/http"
"github.com/a-h/templ"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/go-chi/chi/v5"
"github.com/go-faker/faker/v4"
)
func setupExamplesLazyTabs(examplesRouter chi.Router) error {
tabs := make([]ElementRenderer, 8)
tabs := make([]templ.Component, 8)
for i := range tabs {
paragraphs := make([]ElementRenderer, 5)
for j := range paragraphs {
paragraphs[j] = P(Text(faker.Paragraph()))
}
tabs[i] = Group(paragraphs...)
}
type Store struct {
TabID int `json:"tabId"`
}
tabsToNode := func(activeIdx int) ElementRenderer {
log.Printf("tabsToNode: %d", activeIdx)
return DIV().
ID("lazy_tabs").
DATASTAR_STORE(Store{TabID: activeIdx}).
CLASS("flex flex-col").
Children(
UL().
CLASS("list-none flex border-b-2 border-accent-700").
Children(
RangeI(tabs, func(i int, t ElementRenderer) ElementRenderer {
return LI().
CLASS("flex-1 px-2 cursor-pointer ").
Children(
A().
CLASS("text-lg flex justify-center items-center inline-block border-l border-t border-r rounded-t py-px hover:bg-accent-500").
IfCLASS(i == activeIdx, "bg-accent-500").
IfCLASS(i != activeIdx, "bg-accent-700").
TextF("Tab %d", i).
CustomData("testid", fmt.Sprintf("tab_%d", i)).
DATASTAR_ON(
"click",
fmt.Sprintf("$tabId=%d;$$get('/examples/lazy_tabs/data')", i),
),
)
}),
),
DIV().
ID("tab_content").
CLASS("p-4 shadow-lg bg-base-200").
Children(tabs[activeIdx]),
)
tabs[i] = setupExamplesLazyTabsContent()
}
examplesRouter.Get("/lazy_tabs/data", func(w http.ResponseWriter, r *http.Request) {
store := &Store{}
store := &LazyTabsStore{}
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(sse, tabsToNode(store.TabID))
component := setupExamplesLazyTabsComponent(len(tabs), tabs[store.TabID], store)
datastar.RenderFragmentTempl(sse, component)
})
return nil

View File

@ -0,0 +1,38 @@
package site
import (
"fmt"
"github.com/go-faker/faker/v4"
)
templ setupExamplesLazyTabsContent() {
<p>{ faker.Paragraph() }</p>
<p>{ faker.Paragraph() }</p>
<p>{ faker.Paragraph() }</p>
<p>{ faker.Paragraph() }</p>
<p>{ faker.Paragraph() }</p>
}
type LazyTabsStore struct {
TabID int `json:"tabId"`
}
templ setupExamplesLazyTabsComponent(tabCount int, contents templ.Component, store *LazyTabsStore) {
<div id="lazy_tabs" data-store={ templ.JSONString(store) } class="flex flex-col">
<div role="tablist" class="tabs tabs-bordered">
for i := 0; i < tabCount; i++ {
<button
role="tab"
class={ "tab", templ.KV("tab-active", i == store.TabID) }
data-on-click={ fmt.Sprintf("$tabId=%d;$$get('/examples/lazy_tabs/data')", i) }
data-testid={ fmt.Sprintf("tab_%d", i) }
>
Tab { fmt.Sprint(i) }
</button>
}
</div>
<div id="tab_content" class="p-4 shadow-lg bg-base-200">
@contents
</div>
</div>
}

View File

@ -1,45 +1,20 @@
package site
import (
"bytes"
"log"
"fmt"
"net/http"
"time"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/go-chi/chi/v5"
"github.com/samber/lo"
"github.com/zeebo/xxh3"
)
func setupExamplesMergeOptions(examplesRouter chi.Router) error {
setupContents := DIV().
ID("contents").
CLASS("flex flex-col gap-8").
Children(
DIV().ID("target").TextF("Target DIV"),
DIV().CLASS("flex gap-2 flex-wrap").
Children(
Range(datastar.ValidFragmentMergeTypes, func(mergeMode datastar.FragmentMergeType) ElementRenderer {
return BUTTON().
CLASS("border-2 border-accent-500 px-4 py-2 rounded text-accent-200").
DATASTAR_ON("click", datastar.GET("/examples/merge_options/%s", mergeMode)).
Text(string(mergeMode))
}),
),
BUTTON().
CLASS("bg-accent-500 px-4 py-2 rounded text-accent-200").
DATASTAR_ON("click", datastar.GET("/examples/merge_options/reset")).
Text("Reset"),
)
examplesRouter.Get("/merge_options/reset", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
buf := bytes.NewBuffer(nil)
setupContents.Render(buf)
log.Printf("contents: %s", buf.String())
datastar.RenderFragment(sse, setupContents)
datastar.RenderFragmentTempl(sse, mergeOptionsView())
})
brewerColorsBG := []string{
@ -88,14 +63,9 @@ func setupExamplesMergeOptions(examplesRouter chi.Router) error {
return
} else {
now := time.Now().UTC().Format(time.RFC3339)
h := xxh3.HashString(now)
updatedTarget := DIV().
ID("target").
STYLE("background-color", brewerColorsBG[idx]).
STYLE("color", brewrColorsFG[idx]).
CLASS("p-4 rounded").
TextF("Update %x at %s", h, now)
datastar.RenderFragment(sse, updatedTarget, datastar.WithMergeType(mergeMode))
h := fmt.Sprint(xxh3.HashString(now))
frag := mergeOptionsViewUpdate(brewerColorsBG[idx], brewrColorsFG[idx], h)
datastar.RenderFragmentTempl(sse, frag, datastar.WithMergeType(mergeMode))
}
})

View File

@ -0,0 +1,42 @@
package site
import (
"fmt"
"time"
"github.com/delaneyj/datastar"
)
templ mergeOptionsView() {
<div id="contents" class="flex flex-col gap-8">
<div id="target">Target DIV</div>
<div class="flex gap-2 flex-wrap justify-center">
for _, mergeMode := range datastar.ValidFragmentMergeTypes {
<button
class="btn btn-accent"
data-on-click={ fmt.Sprintf("$$get('/examples/merge_options/%s')", mergeMode) }
>
{ string(mergeMode) }
</button>
}
</div>
<button
class="btn btn-warning"
data-on-click="$$get('/examples/merge_options/reset')"
>
Reset
</button>
</div>
}
templ mergeOptionsViewUpdate(bg, fg, hash string) {
<div
id="target"
class="p-4 rounded"
{ templ.Attributes{
"style": fmt.Sprintf("background-color:%s;color:%s;", bg, fg),
}... }
>
Update { hash } at { time.Now().UTC().Format(time.RFC3339) }
</div>
}

View File

@ -4,87 +4,21 @@ import (
"net/http"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/go-chi/chi/v5"
"github.com/martinusso/inflect"
"github.com/samber/lo"
)
func setupExamplesModelBinding(examplesRouter chi.Router) error {
type Store struct {
BindText string `json:"bindText"`
BindNumber int `json:"bindNumber"`
BindBool bool `json:"bindBool"`
BindSelection int `json:"bindSelection"`
}
examplesRouter.Get("/model_binding/data", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
store := Store{
store := &ModelBindingStore{
BindText: "foo",
BindNumber: 42,
BindSelection: 1,
BindBool: true,
}
optionValues := lo.Range(7)
selectOptions := lo.Map(optionValues, func(i, index int) ElementRenderer {
return OPTION().VALUEF("%d", i).TextF("Option %d", i)
})
radioOptions := lo.Map(optionValues, func(i, index int) ElementRenderer {
return LABEL().
CLASS("font-brand font-bold text-xl flex items-center gap-2").
Children(
INPUT().
TYPE("radio").
DATASTAR_MODEL("bindSelection").
VALUEF("%d", i),
TextF("%s Option", inflect.Ordinalize(i)),
)
})
datastar.RenderFragment(
sse,
DIV().
ID("container").
CLASS("flex flex-col gap-4").
DATASTAR_STORE(store).
Children(
INPUT().
TYPE("text").
CLASS("border border-accent-500 bg-accent-700 rounded px-4 py-2 w-full py text-accent-200").
DATASTAR_MODEL("bindText"),
INPUT().
TYPE("number").
CLASS("border border-accent-500 bg-accent-700 rounded px-4 py-2 w-full py text-accent-200").
DATASTAR_MODEL("bindNumber"),
TEXTAREA().
CLASS("border border-accent-500 bg-accent-700 rounded px-4 py-2 w-full py text-accent-200").
DATASTAR_MODEL("bindText"),
LABEL().
CLASS("flex items-center gap-1").
Children(
SPAN().Text("Checkbox"),
INPUT().
TYPE("checkbox").
CLASS("border border-accent-500 bg-accent-700 rounded px-4 py-2 w-full py text-accent-200").
DATASTAR_MODEL("bindBool"),
),
SELECT().
CLASS("border border-accent-500 bg-accent-700 rounded px-4 py-2 w-full py text-accent-200").
DATASTAR_MODEL("bindSelection").
Children(selectOptions...),
DIV().
CLASS("flex flex-col").
Children(radioOptions...),
CODE().
CLASS("border text-primary-200 border-primary-500 rounded p-8").
Children(PRE().DATASTAR_TEXT("JSON.stringify(ctx.store(),null,2)")),
),
)
datastar.RenderFragmentTempl(sse, ModelBindingView(7, store))
})
return nil

View File

@ -0,0 +1,49 @@
package site
import (
"fmt"
"github.com/delaneyj/datastar"
)
type ModelBindingStore struct {
BindText string `json:"bindText"`
BindNumber int `json:"bindNumber"`
BindBool bool `json:"bindBool"`
BindSelection int `json:"bindSelection"`
}
templ ModelBindingView(optionCount int, store *ModelBindingStore) {
<div
id="container"
class="flex flex-col gap-4"
data-store={ templ.JSONString(store) }
>
<input class="input input-bordered" data-model="bindText"/>
<input class="input input-bordered" type="number" data-model="bindNumber"/>
<textarea class="textarea textarea-bordered" data-model="bindText"></textarea>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Checkbox</span>
<input type="checkbox" checked="checked" class="checkbox" data-model="bindBool"/>
</label>
</div>
<select class="select select-bordered" data-model="bindSelection">
for i :=1 ; i <= optionCount; i++ {
{{ str := fmt.Sprint( i) }}
<option value={ str }>Option { str }</option>
}
</select>
<div class="flex flex-col">
for i := 1; i <= optionCount; i++ {
{{ str := fmt.Sprint( i) }}
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Option { str }</span>
<input type="radio" class="radio" data-model="bindSelection" value={ str }/>
</label>
</div>
}
</div>
@datastar.TemplSignalStoreView()
</div>
}

View File

@ -10,14 +10,15 @@ import (
func setupExamplesMultilineFragments(examplesRouter chi.Router) error {
examplesRouter.Get("/multiline_fragments/data", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
datastar.RenderFragmentString(sse, `
<div id="replaceMe">
<pre>
<div id="replaceMe">
<pre>
This is a multiline fragment.
Used when you are writing a lot of text by hand
</pre>
</div>
</pre>
</div>
`)
})

View File

@ -2,10 +2,8 @@ package site
import (
"net/http"
"time"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/go-chi/chi/v5"
"github.com/gorilla/sessions"
)
@ -22,18 +20,11 @@ func setupExamplesOnLoad(examplesRouter chi.Router, store sessions.Store) error
if err := session.Save(r, w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// You can comment out the below block and still persist the session
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(sse,
DIV(
TextF("Loaded at %s", time.Now().Format(time.RFC3339)),
DIV().TextF("Session name: %s", sessionKey),
DIV().TextF("Session contents: %v", session.Values),
).ID("replaceMe"),
)
datastar.RenderFragmentTempl(sse, onLoadView(sessionKey, session.Values))
})
return nil

View File

@ -0,0 +1,14 @@
package site
import (
"time"
"fmt"
)
templ onLoadView(sessionKey string, sessionValues any) {
<div id="replaceMe">
Loaded at { time.Now().Format(time.RFC3339) }
<div>Session name: { sessionKey }</div>
<div>Session contents: { fmt.Sprintf("%+v",sessionValues) }</div>
</div>
}

View File

@ -1,15 +1,11 @@
package site
import (
"fmt"
"math/rand"
"net/http"
"time"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
"github.com/delaneyj/toolbelt"
"github.com/go-chi/chi/v5"
)
@ -21,67 +17,9 @@ func setupExamplesProgressBar(examplesRouter chi.Router) error {
progress := 0
for progress < 100 {
progress = min(100, progress+rand.Intn(10)+1)
datastar.RenderFragment(
sse,
DIV().
ID("progress_bar").
Children(
Tern(
progress == 100,
A().
HREF("/examples/progress_bar").
CLASS("btn btn-success").
ID("completed_link").
Children(
DIV().
CLASS("flex gap-2 font-bold text-2xl").
Children(
material_symbols.CheckCircle(),
Text("Completed! Try again"),
),
),
SVG_SVG().
WIDTH("200").
HEIGHT("200").
Attr("viewbox", "-25 -25 250 250").
STYLE("transform", "rotate(-90deg)").
Children(
SVG_CIRCLE().Attrs(
"r", "90",
"cx", "100",
"cy", "100",
"fill", "transparent",
"stroke", "#e0e0e0",
"stroke-width", "16px",
"stroke-dasharray", "565.48px",
"stroke-dashoffset", "565px",
),
SVG_CIRCLE().Attrs(
"r", "90",
"cx", "100",
"cy", "100",
"fill", "transparent",
"stroke", "#6bdba7",
"stroke-width", "16px",
"stroke-linecap", "round",
"stroke-dashoffset", fmt.Sprintf("%dpx", int(toolbelt.Fit(float32(progress), 0, 100, 565, 0))),
"stroke-dasharray", "565.48px",
),
SVG_TEXT().Attrs(
"x", "44px",
"y", "115px",
"fill", "#6bdba7",
"font-size", "52px",
"font-weight", "bold",
"style", "transform:rotate(90deg) translate(0px, -196px)",
).TextF("%d%%", progress),
),
),
),
)
time.Sleep(500 * time.Millisecond)
progress = min(100, progress+rand.Intn(20)+1)
datastar.RenderFragmentTempl(sse, progressBarView(progress))
time.Sleep(250 * time.Millisecond)
}
})

View File

@ -0,0 +1,53 @@
package site
import (
"fmt"
"github.com/delaneyj/toolbelt"
)
templ progressBarView(progress int) {
<div id="progress_bar">
if progress == 100 {
<button
id="completed_link"
class="flex items-center gap-2 btn btn-success"
data-on-click="window.location.reload()"
>
@icon("material-symbols:check-circle")
Completed! Try again
</button>
} else {
<svg width="200" height="200" viewbox="-25 -25 250 250" style="transform: rotate(-90deg)">
<circle
r="90"
cx="100"
cy="100"
fill="transparent"
stroke="#e0e0e0"
stroke-width="16px"
stroke-dasharray="565.48px"
stroke-dashoffset="565px"
></circle>
<circle
r="90"
cx="100"
cy="100"
fill="transparent"
stroke="#6bdba7"
stroke-width="16px"
stroke-linecap="round"
stroke-dashoffset={ fmt.Sprintf("%0.0fpx", toolbelt.Fit(float32(progress), 0, 100, 565, 0)) }
stroke-dasharray="565.48px"
></circle>
<text
x="44px"
y="115px"
fill="#6bdba7"
font-size="52px"
font-weight="bold"
style="transform:rotate(90deg) translate(0px, -196px)"
>{ fmt.Sprint(progress) }%</text>
</svg>
}
</div>
}

View File

@ -3,79 +3,32 @@ package site
import (
"encoding/binary"
"encoding/hex"
"fmt"
"math/rand"
"net/http"
"sync"
"time"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/go-chi/chi/v5"
"github.com/goccy/go-json"
// "github.com/goccy/go-json"
)
func setupExamplesQuickPrimerGo(examplesRouter chi.Router) error {
examplesRouter.Route("/quick_primer_go/data", func(dataRouter chi.Router) {
type Store struct {
Input string `json:"input"`
Show bool `json:"show"`
}
store := &Store{"initial backend data", false}
store := &QuickPrimerGoStore{"initial backend data", false}
mu := &sync.RWMutex{}
dataRouter.Get("/replace", func(w http.ResponseWriter, r *http.Request) {
fragment := DIV().
ID("replaceMe").
DATASTAR_STORE(store).
Children(
H2().Text("Go Datastar Example"),
MAIN().
ID("main").
CLASS("container flex flex-col gap-4").
Children(
INPUT().
TYPE("text").
CLASS("p-2 border border-accent-400 rounded text-accent-200 bg-accent-700").
PLACEHOLDER("Type here!").
DATASTAR_MODEL("input"),
DIV().DATASTAR_TEXT("$input"),
BUTTON().
CLASS("p-2 border border-accent-400 rounded text-accent-200 bg-accent-700").
DATASTAR_ON("click", "$show = !$show").
Text("Toggle"),
DIV().
Attr("data-show", "$show").
Children(
SPAN().Text("Hello from Datastar!"),
),
DIV().ID("output").Text("#output"),
BUTTON().
CLASS("p-2 border border-accent-400 rounded text-accent-200 bg-accent-700").
DATASTAR_ON("click", datastar.PUT("/examples/quick_primer_go/data")).
Text("Send State"),
DIV().ID("output2").Text("#output2"),
BUTTON().
CLASS("p-2 border border-accent-400 rounded text-accent-200 bg-accent-700").
DATASTAR_ON("click", datastar.GET("/examples/quick_primer_go/data")).
Text("Get State"),
DIV().
Children(
SPAN().Text("Feed from server: "),
SPAN().
ID("feed").
DATASTAR_ON("load", datastar.GET("/examples/quick_primer_go/data/feed")),
),
),
)
mu.RLock()
defer mu.RUnlock()
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(sse, fragment)
datastar.RenderFragmentTempl(sse, QuickPrimerGoView(store))
})
dataRouter.Put("/", func(w http.ResponseWriter, r *http.Request) {
reqStore := &Store{}
reqStore := &QuickPrimerGoStore{}
if err := datastar.BodyUnmarshal(r, reqStore); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@ -84,30 +37,29 @@ func setupExamplesQuickPrimerGo(examplesRouter chi.Router) error {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
mu.Lock()
store = reqStore
fragment := DIV().
ID("output").
TextF("Your input: %s, is %d long.", store.Input, len(store.Input))
mu.Unlock()
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(sse, fragment)
datastar.RenderFragmentTempl(sse, QuickPrimerGoPut(store))
})
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
mu.RLock()
defer mu.RUnlock()
b, err := json.Marshal(store)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fragment := fmt.Sprintf(`<div id="output2">Backend state: %s</div>`, string(b))
datastar.RenderFragmentString(sse, fragment)
datastar.RenderFragmentTempl(sse, QuickPrimerGoGet(string(b)))
fragment = `<div>Check this out!</div>`
datastar.RenderFragmentString(
sse, fragment,
datastar.RenderFragmentTempl(
sse, QuickPrimerCheckThisOut(),
datastar.WithQuerySelector("main"),
datastar.WithMergePrependElement(),
)
@ -117,7 +69,7 @@ func setupExamplesQuickPrimerGo(examplesRouter chi.Router) error {
dataRouter.Get("/feed", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
ticker := time.NewTicker(time.Second)
ticker := time.NewTicker(2 * time.Second)
for {
select {
@ -126,9 +78,7 @@ func setupExamplesQuickPrimerGo(examplesRouter chi.Router) error {
case <-ticker.C:
buf := make([]byte, 8)
binary.LittleEndian.PutUint64(buf, rand.Uint64())
fragment := `<span id="feed">` + hex.EncodeToString(buf) + `</span>`
datastar.RenderFragmentString(sse, fragment)
datastar.RenderFragmentTempl(sse, QuickPrimerGoFeed(hex.EncodeToString(buf)))
}
}
})

View File

@ -0,0 +1,50 @@
package site
import (
"fmt"
"github.com/delaneyj/datastar"
)
type QuickPrimerGoStore struct {
Input string `json:"input"`
Show bool `json:"show"`
}
templ QuickPrimerGoView(store *QuickPrimerGoStore) {
<div id="replaceMe" data-store={ templ.JSONString(store) }>
<h2>Go Datastar Example</h2>
<main id="main" class="container flex flex-col gap-4">
<input type="text" class="input input-bordered" placeholder="Type here!" data-model="input"/>
<div data-text="$input"></div>
<button class="btn btn-accent" data-on-click="$show = !$show">Toggle</button>
<div data-show="$show">
<span>Hello from Datastar!</span>
</div>
<div id="output">#output</div>
<button class="btn btn-accent" data-on-click="$$put('/examples/quick_primer_go/data')">Send State</button>
<div id="output2">#output2</div>
<button class="btn btn-accent" data-on-click="$$get('/examples/quick_primer_go/data')">Get State</button>
<div>
<span>Feed from server: </span>
<span id="feed" data-on-load="$$get('/examples/quick_primer_go/data/feed')"></span>
</div>
</main>
@datastar.TemplSignalStoreView()
</div>
}
templ QuickPrimerGoPut(store *QuickPrimerGoStore) {
<div id="output">Your input: { store.Input }, is { fmt.Sprint(len(store.Input)) } characters long.</div>
}
templ QuickPrimerGoGet(stateStr string) {
<div id="output2">Backend state: { stateStr }</div>
}
templ QuickPrimerCheckThisOut() {
<div>Check this out!</div>
}
templ QuickPrimerGoFeed(feed string) {
<span id="feed">{ feed }</span>
}

View File

@ -1,51 +1,28 @@
package site
import (
"fmt"
"net/http"
"time"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/go-chi/chi/v5"
)
func setupExamplesRedirects(examplesRouter chi.Router) error {
examplesRouter.Route("/redirects/data", func(dataRouter chi.Router) {
type Store struct {
RedirectTo string `json:"redirectTo"`
}
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
store := &Store{
RedirectTo: "/essays/why_another_framework",
store := &RedirectsStore{
RedirectTo: "/essays/grugs_around_fire",
}
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(
sse,
DIV().
ID("demo").
DATASTAR_STORE(store).
CLASS("flex gap-4 w-full").
Children(
LABEL().
CLASS("flex-1").
Children(
SPAN().Text("Redirect to: "),
INPUT().
DATASTAR_MODEL("redirectTo").
CLASS("bg-accent-900 border-2 border-accent-600 text-accent-100 text-sm rounded-lg focus:ring-primary-400 focus:border-primary-400 block w-full p-2.5"),
),
BUTTON().
CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-success-700 hover:bg-success-600").
DATASTAR_ON("click", datastar.POST("/examples/redirects/data")).
Text("Redirect"),
),
)
datastar.RenderFragmentTempl(sse, redirectsView(store))
})
dataRouter.Post("/", func(w http.ResponseWriter, r *http.Request) {
store := &Store{}
store := &RedirectsStore{}
if err := datastar.BodyUnmarshal(r, store); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@ -53,11 +30,11 @@ func setupExamplesRedirects(examplesRouter chi.Router) error {
sse := datastar.NewSSE(w, r)
for i := 5; i > 0; i-- {
datastar.RenderFragment(
datastar.RenderFragmentString(
sse,
DIV().ID("update").TextF("Redirecting in %d...", i),
fmt.Sprintf(`<div id="update">Redirecting in %d...</div>`, i),
)
time.Sleep(time.Second)
time.Sleep(500 * time.Millisecond)
}
datastar.Redirect(sse, store.RedirectTo)
})

View File

@ -0,0 +1,15 @@
package site
type RedirectsStore struct {
RedirectTo string `json:"redirectTo"`
}
templ redirectsView(store *RedirectsStore) {
<div id="demo" class="flex w-full gap-4" data-store={ templ.JSONString(store) }>
<label class="flex items-center flex-1 gap-2">
<span>Redirect to: </span>
<input data-model="redirectTo" class="flex-1 input input-bordered"/>
</label>
<button class="btn btn-success" data-on-click="$$post('/examples/redirects/data')">Redirect</button>
</div>
}

View File

@ -1,14 +1,13 @@
package site
import (
"fmt"
"log"
"net/http"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
lorem "github.com/drhodes/golorem"
"github.com/go-chi/chi/v5"
"github.com/valyala/bytebufferpool"
)
func setupExamplesScrollIntoView(examplesRouter chi.Router) error {
@ -18,18 +17,7 @@ func setupExamplesScrollIntoView(examplesRouter chi.Router) error {
paragraphs[i] = lorem.Paragraph(40, 60)
}
type Store struct {
Behavior string `json:"behavior"`
Block string `json:"block"`
Inline string `json:"inline"`
}
type Options struct {
Label string
Values []string
}
opts := []Options{
opts := []ScrollIntoViewOption{
{"behavior", []string{"smooth", "instant", "auto"}},
{"block", []string{"vstart", "vcenter", "vend", "vnearest"}},
{"inline", []string{"hstart", "hcenter", "hend", "hnearest"}},
@ -40,53 +28,17 @@ func setupExamplesScrollIntoView(examplesRouter chi.Router) error {
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
store := &Store{
store := &ScrollIntoViewStore{
Behavior: "smooth",
Block: "vcenter",
Inline: "hcenter",
}
contents := DIV().
ID("replaceMe").
DATASTAR_STORE(store).
Children(
DIV().
CLASS("flex flex-wrap gap-8 justify-center").
Children(
Range(opts, func(o Options) ElementRenderer {
return DIV().
CLASS("flex flex-col gap-2").
Children(
H3().Text(o.Label),
SELECT().
ID(o.Label).
CLASS("bg-accent-800 border border-accent-600 text-accent-200 text-sm rounded-lg focus:ring-accent-400 focus:border-accent-400 block w-full p-2.5").
DATASTAR_MODEL(o.Label).
Children(
Range(o.Values, func(v string) ElementRenderer {
return OPTION().Text(v).VALUE(v)
}),
),
)
}),
BUTTON().
CLASS("bg-accent-800 border border-accent-600 text-accent-200 text-sm rounded-lg focus:ring-accent-400 focus:border-accent-400 block w-full p-2.5").
Text("Scroll into view").
DATASTAR_ON("click", datastar.PUT("/examples/scroll_into_view/data")),
),
RangeI(paragraphs, func(i int, p string) ElementRenderer {
isMiddle := i == paragraphCount/2
return Group(
P().IDF("p%d", i).Text(p).IfCLASS(isMiddle, "bg-accent-800 text-accent-200 p-4 rounded-lg"),
// If(isMiddle, A().HREF("#replaceMe").Text("Back to top")),
)
}),
)
datastar.RenderFragment(sse, contents)
datastar.RenderFragmentTempl(sse, scrollIntoViewView(paragraphs, opts, store))
})
dataRouter.Put("/", func(w http.ResponseWriter, r *http.Request) {
store := &Store{}
store := &ScrollIntoViewStore{}
if err := datastar.BodyUnmarshal(r, store); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@ -105,12 +57,10 @@ func setupExamplesScrollIntoView(examplesRouter chi.Router) error {
attr += "." + store.Inline
}
updated := P().IDF("p%d", paragraphCount/2).CustomData(attr, "")
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
updated.Render(buf)
log.Println(buf.String())
datastar.RenderFragment(sse, updated, datastar.WithMergeUpsertAttributes())
updated := fmt.Sprintf(`<p id="p%d" data-%s></p>`, paragraphCount/2, attr)
log.Println(updated)
datastar.RenderFragmentString(sse, updated, datastar.WithMergeUpsertAttributes())
})
})

View File

@ -0,0 +1,58 @@
package site
import (
"fmt"
)
type ScrollIntoViewStore struct {
Behavior string `json:"behavior"`
Block string `json:"block"`
Inline string `json:"inline"`
}
type ScrollIntoViewOption struct {
Label string
Values []string
}
templ scrollIntoViewView(paragraphs []string, opts []ScrollIntoViewOption, store *ScrollIntoViewStore) {
<div id="replaceMe" data-store={ templ.JSONString(store) }>
<div class="flex flex-col gap-4">
<div class="flex flex-wrap gap-8 justify-center">
for _, o := range opts {
<div class="flex flex-col gap-2">
<h3>{ o.Label }</h3>
<select
id={ o.Label }
class="select select-bordered"
data-model={ o.Label }
>
for _, v := range o.Values {
<option value={ v }>{ v }</option>
}
</select>
</div>
}
</div>
<button
class="btn btn-accent"
data-on-click="$$put('/examples/scroll_into_view/data')"
>Scroll into view</button>
</div>
{{ middle := len(paragraphs)/2 }}
for i, p := range paragraphs {
{{
isMiddle := i == middle
id := fmt.Sprintf("p%d", +i)
}}
if isMiddle {
<blockquote id={ id }>{ p }</blockquote>
} else {
<p id={ id }>{ p }</p>
}
if isMiddle {
<a href="#replaceMe">Back to top</a>
}
}
</div>
}

View File

@ -8,7 +8,6 @@ import (
"net/http"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/toolbelt"
"github.com/go-chi/chi/v5"
"github.com/samber/lo"
@ -16,24 +15,10 @@ import (
func setupExamplesShoelaceKitchensink(examplesRouter chi.Router) error {
examplesRouter.Route("/shoelace_kitchensink/data", func(dataRouter chi.Router) {
type Nested struct {
Label string `json:"label"`
Selection uint32 `json:"selection"`
IsChecked bool `json:"isChecked"`
}
type Input struct {
Nested *Nested `json:"nested"`
}
type Option struct {
Label string `json:"label"`
Value uint32 `json:"value"`
}
options := lo.Map(lo.Range(7), func(i, index int) Option {
// offset := 100199071137923140 + int64(index)
options := lo.Map(lo.Range(7), func(i, index int) ShoelaceKitchensinkOption {
offset := toolbelt.NextID()
return Option{
return ShoelaceKitchensinkOption{
Label: fmt.Sprintf("Option %d", i),
Value: uint32(offset % math.MaxUint32),
}
@ -41,66 +26,26 @@ func setupExamplesShoelaceKitchensink(examplesRouter chi.Router) error {
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
input := &Input{
Nested: &Nested{
store := &ShoelaceKitchensinkStore{
Nested: &ShoelaceKitchensinkNested{
Label: fmt.Sprintf("Hello World %d", rand.Intn(100)),
Selection: options[rand.Intn(len(options))].Value,
IsChecked: true,
},
}
datastar.RenderFragment(sse,
DIV().
ID("shoelace_kitchensink").
CLASS("sl-theme-dark flex flex-col gap-4").
DATASTAR_STORE(input).
Children(
SL_INPUT().
LABEL("Label").
DATASTAR_MODEL("nested.label"),
SL_SELECT().
LABEL("Select").
DATASTAR_MODEL("nested.selection").
DATASTAR_ON("sl-change", "console.log('change')").
Children(
Range(options, func(o Option) ElementRenderer {
return SL_OPTION().
VALUE(fmt.Sprint(o.Value)).
TextF("%s (%d)", o.Label, o.Value)
}),
),
SL_RADIOGROUP().
LABEL("Radio Group").
DATASTAR_MODEL("nested.selection").
Children(
Range(options, func(o Option) ElementRenderer {
return SL_RADIOBUTTON().
VALUE(fmt.Sprint(o.Value)).
Text(o.Label)
}),
),
SL_CHECKBOX().
DATASTAR_MODEL("nested.isChecked").
Text("Checkbox"),
SL_BUTTON().
VARIANT(SLButtonVariant_primary).
Text("Submit").
DATASTAR_ON("click", datastar.POST(r.URL.Path)),
SignalStore,
),
)
datastar.RenderFragmentTempl(sse, ShoelaceKitchensinkView(r, options, store))
})
dataRouter.Post("/", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
var res any
if err := datastar.BodyUnmarshal(r, &res); err != nil {
datastar.Error(sse, err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Printf("res: %#v", res)
log.Printf("res: %v", res)
sse := datastar.NewSSE(w, r)
datastar.RenderFragmentString(sse, "<div></div>")
})
})

View File

@ -0,0 +1,53 @@
package site
import (
"fmt"
"net/http"
"github.com/delaneyj/datastar"
)
type ShoelaceKitchensinkStore struct {
Nested *ShoelaceKitchensinkNested `json:"nested"`
}
type ShoelaceKitchensinkNested struct {
Label string `json:"label"`
Selection uint32 `json:"selection"`
IsChecked bool `json:"isChecked"`
}
type ShoelaceKitchensinkOption struct {
Label string `json:"label"`
Value uint32 `json:"value"`
}
templ ShoelaceKitchensinkView(r *http.Request, options []ShoelaceKitchensinkOption, store *ShoelaceKitchensinkStore) {
<div
id="shoelace_kitchensink"
class="flex flex-col gap-4 sl-theme-dark"
data-store={ templ.JSONString(store) }
>
<sl-input label="Label" data-model="nested.label"></sl-input>
<sl-select
label="Select"
data-model="nested.selection"
data-on-sl-change="console.log('change')"
>
for _, o := range options {
<sl-option value={ fmt.Sprint(o.Value) }>{ o.Label } ({ fmt.Sprint(o.Value) })</sl-option>
}
</sl-select>
<sl-radio-group label="Radio Group" data-bind-value="$nested.selection" data-on-sl-change="$nested.selection = ctx.el.value">
for _, o := range options {
<sl-radio value={ fmt.Sprint(o.Value) }>{ o.Label } ({ fmt.Sprint(o.Value) })</sl-radio>
}
</sl-radio-group>
<sl-checkbox data-model="nested.isChecked">Checkbox</sl-checkbox>
<sl-button
variant="primary"
data-on-click={ fmt.Sprintf("$$post('%s')", r.URL.Path) }
>Submit</sl-button>
@datastar.TemplSignalStoreView()
</div>
}

View File

@ -1,10 +1,10 @@
package site
import (
"fmt"
"net/http"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/go-chi/chi/v5"
"github.com/gorilla/sessions"
)
@ -31,10 +31,8 @@ func setupExamplesStoreChanged(examplesRouter chi.Router, store sessions.Store)
}
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(sse,
DIV().
ID("from_server").
TextF("Lifetime server updates %d", totalUpdates),
datastar.RenderFragmentString(sse,
fmt.Sprintf(`<div id="from_server">Lifetime server updates %d</div>`, totalUpdates),
)
}

View File

@ -0,0 +1,84 @@
package site
import (
"net/http"
"sync/atomic"
"github.com/Jeffail/gabs/v2"
"github.com/delaneyj/datastar"
"github.com/go-chi/chi/v5"
"github.com/gorilla/sessions"
)
func setupExamplesTemplCounter(examplesRouter chi.Router, sessionStore sessions.Store) error {
var globalCounter atomic.Uint32
const (
sessionKey = "templ_counter"
countKey = "count"
)
userVal := func(r *http.Request) (uint32, *sessions.Session, error) {
sess, err := sessionStore.Get(r, sessionKey)
if err != nil {
return 0, nil, err
}
val, ok := sess.Values[countKey].(uint32)
if !ok {
val = 0
}
return val, sess, nil
}
examplesRouter.Get("/templ_counter/data", func(w http.ResponseWriter, r *http.Request) {
userVal, _, err := userVal(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
store := TemplCounterStore{
Global: globalCounter.Load(),
User: userVal,
}
sse := datastar.NewSSE(w, r)
datastar.RenderFragmentTempl(sse, templCounterExampleInitialContents(store))
})
updateGlobal := func(store *gabs.Container) {
store.Set(globalCounter.Add(1), "global")
}
examplesRouter.Route("/templ_counter/increment", func(incrementRouter chi.Router) {
incrementRouter.Post("/global", func(w http.ResponseWriter, r *http.Request) {
update := gabs.New()
updateGlobal(update)
sse := datastar.NewSSE(w, r)
datastar.PatchStore(sse, update)
})
incrementRouter.Post("/user", func(w http.ResponseWriter, r *http.Request) {
val, sess, err := userVal(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
val++
sess.Values[countKey] = val
if err := sess.Save(r, w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
update := gabs.New()
updateGlobal(update)
update.Set(val, "user")
sse := datastar.NewSSE(w, r)
datastar.PatchStore(sse, update)
})
})
return nil
}

View File

@ -0,0 +1,47 @@
package site
type TemplCounterStore struct {
Global uint32 `json:"global"`
User uint32 `json:"user"`
}
templ templCounterExampleButtons() {
<div class="flex justify-around gap-4">
<button
class="btn btn-info"
data-on-click="$$post('/examples/templ_counter/increment/global')"
>
Increment Global
</button>
<button
class="btn btn-success"
data-on-click="$$post('/examples/templ_counter/increment/user')"
>
Increment User
</button>
</div>
}
templ templCounterExampleCounts() {
<div class="flex justify-around gap-4">
<div class="flex flex-col items-center gap-1">
<div class="text-lg font-bold">Global</div>
<div class="text-2xl" data-text="$global"></div>
</div>
<div class="flex flex-col items-center gap-1">
<div class="text-lg font-bold">User</div>
<div class="text-2xl" data-text="$user"></div>
</div>
</div>
}
templ templCounterExampleInitialContents(store TemplCounterStore) {
<div
id="container"
data-store={ templ.JSONString(store) }
class="flex flex-col gap-4"
>
@templCounterExampleButtons()
@templCounterExampleCounts()
</div>
}

View File

@ -1,29 +1,32 @@
package site
import (
"fmt"
"net/http"
"time"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/go-chi/chi/v5"
)
func setupExamplesTitleUpdateBackend(examplesRouter chi.Router) error {
examplesRouter.Get("/title_update_backend/updates", func(w http.ResponseWriter, r *http.Request) {
// You can comment out the below block and still persist the session
sse := datastar.NewSSE(w, r)
t := time.NewTicker(1 * time.Second)
updateTitle := func() {
datastar.RenderFragmentString(sse,
fmt.Sprintf(`<title>%s from server</title>`, time.Now().Format(time.TimeOnly)),
datastar.WithQuerySelector("title"),
)
}
updateTitle()
t := time.NewTicker(1 * time.Second)
for {
select {
case <-r.Context().Done():
return
case <-t.C:
datastar.RenderFragment(sse,
TITLE().TextF("%s from server", time.Now().Format(time.TimeOnly)),
datastar.WithQuerySelector("title"),
)
updateTitle()
}
}
})

View File

@ -12,19 +12,6 @@ import (
func setupExamplesUpdateStore(examplesRouter chi.Router) error {
examplesRouter.Route("/update_store/data", func(dataRouter chi.Router) {
// dataRouter.Post("/replace", func(w http.ResponseWriter, r *http.Request) {
// store := map[string]any{}
// if err := datastar.BodyUnmarshal(r, &store); err != nil {
// http.Error(w, err.Error(), http.StatusBadRequest)
// return
// }
// delete(store, "stuffAlreadyInStore")
// store["_sidebarOpen"] = false
// sse := datastar.NewSSE(w, r)
// datastar.ReplaceStore(sse, store)
// })
dataRouter.Post("/patch", func(w http.ResponseWriter, r *http.Request) {
store := map[string]any{}
if err := datastar.BodyUnmarshal(r, &store); err != nil {

View File

@ -4,35 +4,17 @@ import (
"net/http"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
"github.com/delaneyj/toolbelt"
"github.com/go-chi/chi/v5"
"github.com/samber/lo"
)
func setupExamplesValueSelect(examplesRouter chi.Router) error {
// lazyLoadRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
// examplePage(w, r)
// })
type Model struct {
ID string `json:"id"`
Label string `json:"label"`
}
type Make struct {
ID string `json:"id"`
Label string `json:"label"`
Models []*Model `json:"models"`
}
cars := []*Make{
cars := []*ValueSelectMake{
{
ID: toolbelt.NextEncodedID(),
Label: "Audi",
Models: []*Model{
Models: []*ValueSelectModel{
{
ID: toolbelt.NextEncodedID(),
Label: "A1",
@ -50,7 +32,7 @@ func setupExamplesValueSelect(examplesRouter chi.Router) error {
{
ID: toolbelt.NextEncodedID(),
Label: "Toyota",
Models: []*Model{
Models: []*ValueSelectModel{
{
ID: toolbelt.NextEncodedID(),
Label: "Land Cruiser",
@ -68,7 +50,7 @@ func setupExamplesValueSelect(examplesRouter chi.Router) error {
{
ID: toolbelt.NextEncodedID(),
Label: "Ford",
Models: []*Model{
Models: []*ValueSelectModel{
{
ID: toolbelt.NextEncodedID(),
Label: "F-150",
@ -85,12 +67,7 @@ func setupExamplesValueSelect(examplesRouter chi.Router) error {
},
}
type Store struct {
Make string `json:"make"`
Model string `json:"model"`
}
storeValidation := func(store *Store) (make *Make, model *Model, isValid bool) {
storeValidation := func(store *ValueSelectStore) (make *ValueSelectMake, model *ValueSelectModel, isValid bool) {
if store.Make != "" {
for _, possibleMake := range cars {
if possibleMake.ID == store.Make {
@ -115,7 +92,7 @@ func setupExamplesValueSelect(examplesRouter chi.Router) error {
examplesRouter.Route("/value_select/data", func(dataRouter chi.Router) {
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
store := &Store{}
store := &ValueSelectStore{}
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@ -124,76 +101,14 @@ func setupExamplesValueSelect(examplesRouter chi.Router) error {
make, model, isValid := storeValidation(store)
sse := datastar.NewSSE(w, r)
datastar.RenderFragment(
datastar.RenderFragmentTempl(
sse,
DIV().
ID("value_select").
DATASTAR_STORE(store).
CLASS("flex flex-col gap-2").
Children(
DIV().
CLASS("text-2xl font-bold").
Text("Pick a Make / Model"),
SELECT().
CLASS("bg-accent-800 border border-accent-600 text-accent-200 text-sm rounded-lg focus:ring-accent-400 focus:border-accent-400 block w-full p-2.5").
DATASTAR_MODEL("make").
DATASTAR_ON("change", datastar.GET("/examples/value_select/data")).
CustomData("testid", "make_select").
Children(
OPTION().
DISABLED().
SELECTED().
Text("Select a Make").
VALUE(""),
Group(Range(cars, func(item *Make) ElementRenderer {
return OPTION().
VALUE(item.ID).
Text(item.Label).
CustomData("testid", "make_option_"+item.Label)
})),
),
DynIf(
make != nil,
func() ElementRenderer {
return SELECT().
CLASS("bg-accent-800 border border-accent-600 text-accent-200 text-sm rounded-lg focus:ring-accent-400 focus:border-accent-400 block w-full p-2.5").
DATASTAR_MODEL("model").
DATASTAR_ON("change", datastar.GET("/examples/value_select/data")).
CustomData("testid", "model_select").
Children(
OPTION().
DISABLED().
SELECTED().
Text("Select a Model").
VALUE(""),
Group(Range(make.Models, func(item *Model) ElementRenderer {
return OPTION().
VALUE(item.ID).
Text(item.Label).
CustomData("testid", "model_option_"+item.Label)
})),
)
},
),
DynIf(
isValid,
func() ElementRenderer {
return BUTTON().
CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-success-700 hover:bg-success-600").
DATASTAR_ON("click", datastar.POST("/examples/value_select/data")).
CustomData("testid", "select_button").
Children(
material_symbols.CarRepair(),
TextF("Submit selected '%s / %s' choice", make.Label, model.Label),
)
},
),
),
valueSelectView(cars, store, make, model, isValid),
)
})
dataRouter.Post("/", func(w http.ResponseWriter, r *http.Request) {
store := &Store{}
store := &ValueSelectStore{}
if err := datastar.BodyUnmarshal(r, store); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@ -208,7 +123,7 @@ func setupExamplesValueSelect(examplesRouter chi.Router) error {
sse := datastar.NewSSE(w, r)
make, ok := lo.Find(cars, func(item *Make) bool {
make, ok := lo.Find(cars, func(item *ValueSelectMake) bool {
return item.ID == store.Make
})
if !ok {
@ -216,7 +131,7 @@ func setupExamplesValueSelect(examplesRouter chi.Router) error {
return
}
model, ok := lo.Find(make.Models, func(item *Model) bool {
model, ok := lo.Find(make.Models, func(item *ValueSelectModel) bool {
return item.ID == store.Model
})
@ -225,23 +140,7 @@ func setupExamplesValueSelect(examplesRouter chi.Router) error {
return
}
datastar.RenderFragment(sse,
DIV().
ID("value_select").
Children(
Text("You selected"),
BR(),
TextF("Make '%s' db id:%s", make.Label, make.ID),
BR(),
TextF("Model '%s' db id:%s", model.Label, model.ID),
BUTTON().CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-success-700 hover:bg-success-600").
DATASTAR_ON("click", datastar.GET("/examples/value_select/data")).
Children(
material_symbols.ResetWrench(),
TextF("Resest form"),
),
),
)
datastar.RenderFragmentTempl(sse, valueSelectResults(make, model))
})
})

View File

@ -0,0 +1,80 @@
package site
type ValueSelectStore struct {
Make string `json:"make"`
Model string `json:"model"`
}
type ValueSelectMake struct {
ID string `json:"id"`
Label string `json:"label"`
Models []*ValueSelectModel `json:"models"`
}
type ValueSelectModel struct {
ID string `json:"id"`
Label string `json:"label"`
}
templ valueSelectView(cars []*ValueSelectMake, store *ValueSelectStore, make *ValueSelectMake, model *ValueSelectModel, isValid bool) {
<div
id="value_select"
class="flex flex-col gap-2"
data-store={ templ.JSONString(store) }
>
<div class="text-2xl font-bold">Pick a Make / Model</div>
<select
class="select select-bordered"
data-model="make"
data-on-change="$$get('/examples/value_select/data')"
data-testid="make_select"
>
<option disabled selected>Select a Make</option>
for _, make := range cars {
<option value={ make.ID } data-testid={ "make_option_" + make.Label }>{ make.Label }</option>
}
</select>
if make != nil {
<select
class="select select-bordered"
data-model="model"
data-on-change="$$get('/examples/value_select/data')"
data-testid="model_select"
>
<option disabled selected>Select a Model</option>
for _, model := range make.Models {
<option value={ model.ID } data-testid={ "model_option_" + model.Label }>{ model.Label }</option>
}
</select>
}
if isValid {
<button
class="btn btn-success"
data-on-click="$$post('/examples/value_select/data')"
data-testid="select_button"
>
@icon("material-symbols:car-repair")
Submit selected { make.Label } / { model.Label } choice
</button>
}
</div>
}
templ valueSelectResults(make *ValueSelectMake, model *ValueSelectModel) {
<div id="value_select">
<div class="card">
<div class="card-body">
<div class="card-title">You selected</div>
<div>Make "{ make.Label }" db id:{ make.ID }</div>
<div>Model "{ model.Label }" db id:{ model.ID }</div>
</div>
</div>
<button
class="btn btn-warning"
data-on-click="$$get('/examples/value_select/data')"
>
@icon("material-symbols:reset-wrench")
Resest form
</button>
</div>
}

View File

@ -5,55 +5,29 @@ import (
"time"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/go-chi/chi/v5"
)
func setupExamplesViewTransitionAPI(examplesRouter chi.Router) error {
type Store struct {
UseSlide bool `json:"useSlide"`
}
examplesRouter.Get("/view_transition_api/watch", func(w http.ResponseWriter, r *http.Request) {
// You can comment out the below block and still persist the session
store := &Store{}
store := &viewTransitionAPIStore{}
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
sse := datastar.NewSSE(w, r)
datastar.RenderFragmentTempl(sse, viewTransitionAPIUpdate(store.UseSlide))
t := time.NewTicker(time.Second)
for {
select {
case <-r.Context().Done():
return
case <-t.C:
datastar.RenderFragment(sse, DIV().
ID("stuff").
CLASS("font-brand font-bold text-2xl flex flex-col gap-4").
Children(
DIV().
CLASS("flex gap-2").
Children(
DIV().Text("The time is:"),
DIV().
CLASS("text-primary-300").
IfDATASTAR_VIEW_TRANSITION(
store.UseSlide,
"'slide-it'",
).
Text(time.Now().Format(time.TimeOnly)),
),
BUTTON().
DATASTAR_ON("click", "location.reload()").
CLASS("bg-primary-600 hover:bg-primary-700 flex flex-col justify-center items-center no-underline font-brand font-bold w-full p-4 cursor-pointer text-accent-50 rounded-md text-center flex gap-2 items-center justify-center").
Text("Reload"),
),
)
datastar.RenderFragmentTempl(sse, viewTransitionAPIUpdate(store.UseSlide))
}
}
})

View File

@ -0,0 +1,30 @@
package site
import (
"time"
)
type viewTransitionAPIStore struct {
UseSlide bool `json:"useSlide"`
}
templ viewTransitionAPIUpdate(useSlide bool) {
<div
id="stuff"
class="flex flex-col gap-4 text-2xl font-bold font-brand"
>
<div class="flex gap-2">
<div>The time is:</div>
<div
class="text-primary-300"
if useSlide {
data-view-transition="'slide-it'"
}
>{ time.Now().Format(time.TimeOnly) }</div>
</div>
<button
data-on-click="location.reload()"
class="btn btn-primary"
>Reload</button>
</div>
}

View File

@ -0,0 +1,80 @@
package site
import (
"net/http"
"strings"
"github.com/a-h/templ"
"github.com/go-chi/chi/v5"
"github.com/samber/lo"
)
func setupGuide(router chi.Router) error {
mdElementRenderers, _, err := markdownRenders("guide")
if err != nil {
return err
}
sidebarGroups := []*SidebarGroup{
{
Label: "Getting Started",
Links: []*SidebarLink{
{ID: "getting_started"},
{ID: "go_deeper"},
{ID: "howl"},
{ID: "batteries_included"},
{ID: "streaming_backend"},
},
},
}
lo.ForEach(sidebarGroups, func(group *SidebarGroup, grpIdx int) {
lo.ForEach(group.Links, func(link *SidebarLink, linkIdx int) {
link.URL = templ.SafeURL("/guide/" + link.ID)
link.Label = strings.ToUpper(strings.ReplaceAll(link.ID, "_", " "))
if linkIdx > 0 {
link.Prev = group.Links[linkIdx-1]
} else if grpIdx > 0 {
prvGrp := sidebarGroups[grpIdx-1]
link.Prev = prvGrp.Links[len(prvGrp.Links)-1]
}
if linkIdx < len(group.Links)-1 {
link.Next = group.Links[linkIdx+1]
} else if grpIdx < len(sidebarGroups)-1 {
nxtGrp := sidebarGroups[grpIdx+1]
link.Next = nxtGrp.Links[0]
}
})
})
router.Route("/guide", func(essaysRouter chi.Router) {
essaysRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, string(sidebarGroups[0].Links[0].URL), http.StatusFound)
})
essaysRouter.Get("/{name}", func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
contents, ok := mdElementRenderers[name]
if !ok {
http.Error(w, "not found", http.StatusNotFound)
return
}
var currentLink *SidebarLink
for _, group := range sidebarGroups {
for _, link := range group.Links {
if link.ID == name {
currentLink = link
break
}
}
}
SidebarPage(r, sidebarGroups, currentLink, contents).Render(r.Context(), w)
})
})
return nil
}

View File

@ -2,21 +2,21 @@ package site
import (
"bytes"
"fmt"
"html"
"math/rand"
"net/http"
"sync/atomic"
"github.com/a-h/templ"
"github.com/delaneyj/datastar"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
"github.com/delaneyj/gostar/elements/iconify/simple_icons"
"github.com/delaneyj/gostar/elements/iconify/svg_spinners"
"github.com/delaneyj/toolbelt"
"github.com/dustin/go-humanize"
"github.com/go-chi/chi/v5"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
)
var homePageChartSVG string
func setupHome(router chi.Router) error {
chartWidth := 480
@ -62,88 +62,42 @@ func setupHome(router chi.Router) error {
if err != nil {
panic(err)
}
svgChart := buffer.String()
homePageChartSVG = buffer.String()
var globalCount = new(int32)
c := int32(toolbelt.Fit(rand.Float32(), 0, 1, -100, 100))
globalCount = &c
globalCountExample := func() templ.Component {
store := &GlobalCountStore{
Count: atomic.LoadInt32(globalCount),
}
return HomeGlobalCountExample(*store)
}
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
cdnText := `<script type="module" defer src="https://cdn.jsdelivr.net/npm/@sudodevnull/datastar" ></script>`
page(
DIV().CLASS("min-h-screen md:flex md:items-center flex-col md:p-8 bg-gradient-to-br from-accent-700 to-accent-900").Children(
DIV().
CLASS("p-4 md:max-w-md md:max-w-2xl md:flex flex-col gap-8 md:items-center").
Children(
datastarLogo().CLASS(
"w-24 md:w-64 md:h-64 fill-current text-accent-200",
"animate__animated animate__pulse animate__infinite animate__faster",
),
DIV().
CLASS("font-brand font-bold text-3xl md:text-6xl text-primary-200").
Text("DATASTAR"),
DIV().CLASS("font-brand text-lg").Text("Real-time hypermedia framework"),
P().
CLASS("text-primary-100 md:text-center").
TextF(
`Using a single <span class="text-lg font-bold text-primary-300">%s</span> CDN link and have access to everything needed to rival a full-stack SPA framework; all in the language of your choice.`,
iifeBuildSize,
),
DIV().CLASS("flex flex-col justify-center gap-4 w-full").Children(
buttonLink(true).
CLASS("w-full").
HREF("https://discord.gg/CHvPMrAp6F").
Children(
simple_icons.Discord(),
SPAN().Text("Join the conversation"),
).
TARGET("_blank").
REL("noopener", "noreferrer"),
buttonLink(true).
CLASS("w-full").
HREF("https://github.com/delaneyj/datastar/tree/main/library/src/lib").
Children(
simple_icons.Github(),
SPAN().Text("Check out the source!"),
).
TARGET("_blank").
REL("noopener", "noreferrer"),
),
DIV().
CLASS("p-4 rounded bg-primary-900 text-xs text-primary-200 flex flex-col gap-2 cursor-pointer items-center").
DATASTAR_ON("click", html.EscapeString(fmt.Sprintf("$$clipboard('%s')", cdnText))).
Children(
SPAN().CLASS("font-bold italic text-primary-300").Text("Just add to your HTML:"),
CODE().CLASS("flex justify-center items-center gap-4").
Children(
material_symbols.ContentCopy().CLASS("text-4xl"),
Escaped(cdnText),
),
),
DIV().CLASS("md:flex md:justify-center md:items-center md:p-4").Text(svgChart),
DIV().
CLASS(
"flex flex-col gap-6 border-dashed border-2 border-primary-300 p-4 rounded-lg",
"bg-gradient-to-br from-primary-800 to-primary-900",
).
Children(
DIV().
CLASS("text-2xl font-bold").
Children(Text("Example of a dynamically loaded area of page with shared global state")),
DIV().
ID("global-count-example").
CLASS("flex justify-center p-4 items-center gap-4").
DATASTAR_ON("load", datastar.GET("/api/globalCount")).
Children(
SPAN().
CLASS("text-2xl").
Text("Loading example on delay..."),
svg_spinners.Eclipse().ID("spinner"),
),
),
buttonLink().
CLASS("w-full").
HREF("/docs/"+docNames[0]).
Text("Let's Get Started"),
).CLASS("flex flex-col justify-center items-center"),
),
).Render(w)
Home().Render(r.Context(), w)
})
router.Route("/api", func(apiRouter chi.Router) {
apiRouter.Route("/globalCount", func(globalCountRouter chi.Router) {
globalCountRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
datastar.RenderFragmentTempl(sse, globalCountExample())
})
globalCountRouter.Post("/", func(w http.ResponseWriter, r *http.Request) {
store := &GlobalCountStore{}
if err := datastar.BodyUnmarshal(r, store); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
atomic.StoreInt32(globalCount, store.Count)
sse := datastar.NewSSE(w, r)
datastar.RenderFragmentTempl(sse, globalCountExample())
})
})
})
return nil

View File

@ -0,0 +1,114 @@
package site
import (
"fmt"
)
templ Home() {
{{ cdnText := `<script type="module" defer src="https://cdn.jsdelivr.net/npm/@sudodevnull/datastar" ></script>` }}
@Page() {
<div class="flex flex-col items-center min-h-screen gap-4 p-16 bg-gradient-to-br from-base-300 to-base-100">
<div class="flex flex-col items-center max-w-lg gap-8">
<img
class="w-24 border-4 rounded-full shadow-xl md:w-96 border-primary"
src={ staticPath("/images/rocket.png") }
/>
<div class="text-4xl font-bold uppercase font-brand md:text-6xl text-primary">Datastar</div>
<div class="text-center font-brand">
<div class="text-xl">Real-time hypermedia framework</div>
</div>
<p>
Using a single
<span class="text-lg font-bold text-primary">{ iifeBuildSize }</span>
CDN link and have access to everything needed to rival a full-stack SPA framework; all in the language of your choice.
</p>
<div class="flex flex-wrap w-full gap-4">
<a
class="flex items-center justify-center flex-1 btn btn-secondary"
href="https://discord.gg/CHvPMrAp6F"
>
@icon("simple-icons:discord")
Join the conversation
</a>
<a
class="flex items-center justify-center flex-1 btn btn-accent"
href="https://github.com/delaneyj/datastar/tree/main/library/src/lib"
>
@icon("simple-icons:github")
View the source
</a>
</div>
<div class="w-full shadow-xl card bg-base-100">
<div class="card-body">
<div class="flex items-center gap-4">
<button
class="btn btn-primary btn-ghost"
data-on-click={ fmt.Sprintf("$$clipboard('%s')", cdnText) }
>
@icon("material-symbols:content-copy")
</button>
<code
class="flex-1 overflow-hidden text-xs text-primary text-ellipsis"
>
{ cdnText }
</code>
</div>
</div>
</div>
@templ.Raw(homePageChartSVG)
<div class="w-full shadow-xl card bg-base-100">
<div class="card-body">
<h2 class="card-title">Example of a dynamically loaded area of page with shared global state</h2>
<div
id="global-count-example"
class="flex items-center justify-center gap-4 p-4"
data-on-load="$$get('/api/globalCount')"
data-fetch-indicator="'#spinner'"
>
<span class="text-2xl">Loading example on delay...</span>
@icon("svg-spinners:eclipse", "id", "spinner")
</div>
</div>
</div>
<a
class="flex items-center w-full gap-1 btn btn-primary btn-outline btn-lg"
href={ templ.SafeURL("/guide") }
>
@icon("simple-icons:rocket")
Let's Get Started!
</a>
</div>
</div>
}
}
type GlobalCountStore struct {
Count int32 `json:"count"`
}
templ HomeGlobalCountExample(store GlobalCountStore) {
<div
id="global-count-example"
class="flex flex-col gap-4"
data-store={ templ.JSONString(store) }
>
<div class="flex gap-4">
<a class="flex-1 btn btn-success" data-on-click="$count++">Increment Local +</a>
<a class="flex-1 btn btn-error" data-on-click="$count--">Decrement Local -</a>
</div>
<div class="flex flex-col gap-2">
<div data-text="`Count is ${$count % 2 === 0 ? 'even' : 'odd'}`"></div>
<input
class="flex-1 input input-bordered"
type="number"
name="count"
data-model="count"
data-testid="localcount_input"
/>
</div>
<div class="flex gap-4">
<a class="flex-1 btn btn-info" data-on-click="$$get('/api/globalCount')">Load global</a>
<a class="flex-1 btn btn-warning" data-on-click="$$post('/api/globalCount')">Store global</a>
</div>
</div>
}

View File

@ -2,125 +2,82 @@ package site
import (
"net/http"
"strings"
. "github.com/delaneyj/gostar/elements"
"github.com/a-h/templ"
"github.com/go-chi/chi/v5"
"github.com/samber/lo"
)
func setupReferenceRoutes(router chi.Router) error {
mdElementRenderers, mdAnchors, err := markdownRenders("reference")
mdElementRenderers, _, err := markdownRenders("reference")
if err != nil {
return err
}
type Reference struct {
ID string
Label string
URL string
Description string
Prev, Next *Reference
}
type ReferenceGroup struct {
Label string
References []*Reference
}
var (
prevRef *Reference
referenceByURL = map[string]*Reference{}
)
references := lo.Map([]ReferenceGroup{
sidebarGroups := []*SidebarGroup{
{
Label: "Included Plugins",
References: []*Reference{
{ID: "plugins_core", Label: "Core"},
{ID: "plugins_attributes", Label: "Attributes"},
{ID: "plugins_backend", Label: "Backend"},
{ID: "plugins_helpers", Label: "Helpers"},
{ID: "plugins_visibility", Label: "Visibility"},
Label: "Included",
Links: []*SidebarLink{
{ID: "core"},
{ID: "attributes"},
{ID: "backend"},
{ID: "helpers"},
{ID: "visibility"},
},
},
{
Label: "How it Works",
References: []*Reference{
{ID: "expressions", Label: "Expressions"},
Links: []*SidebarLink{
{ID: "expressions"},
},
},
// {
// Label: "Writing Plugins",
// References: []*Reference{},
// },
}, func(g ReferenceGroup, i int) ReferenceGroup {
for _, example := range g.References {
example.URL = "/reference/" + example.ID
if prevRef != nil {
example.Prev = prevRef
prevRef.Next = example
}
lo.ForEach(sidebarGroups, func(group *SidebarGroup, grpIdx int) {
lo.ForEach(group.Links, func(link *SidebarLink, linkIdx int) {
link.URL = templ.SafeURL("/reference/" + link.ID)
link.Label = strings.ToUpper(strings.ReplaceAll(link.ID, "_", " "))
if linkIdx > 0 {
link.Prev = group.Links[linkIdx-1]
} else if grpIdx > 0 {
prvGrp := sidebarGroups[grpIdx-1]
link.Prev = prvGrp.Links[len(prvGrp.Links)-1]
}
prevRef = example
referenceByURL[example.URL] = example
}
return g
if linkIdx < len(group.Links)-1 {
link.Next = group.Links[linkIdx+1]
} else if grpIdx < len(sidebarGroups)-1 {
nxtGrp := sidebarGroups[grpIdx+1]
link.Next = nxtGrp.Links[0]
}
})
})
router.Route("/reference", func(referenceRouter chi.Router) {
referenceRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, references[0].References[0].URL, http.StatusFound)
router.Route("/reference", func(essaysRouter chi.Router) {
essaysRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, string(sidebarGroups[0].Links[0].URL), http.StatusFound)
})
sidebarContents := func(r *http.Request) ElementRenderer {
return Range(references, func(g ReferenceGroup) ElementRenderer {
return DIV(
DIV(
DIV().CLASS("text-2xl font-bold text-primary").Text(g.Label),
HR().CLASS("divider border-primary"),
),
UL().
CLASS("list-disc pl-4").
Children(Range(g.References, func(e *Reference) ElementRenderer {
return LI(
link(e.URL, e.Label, e.URL == r.URL.Path),
)
})),
)
})
}
referenceRouter.Get("/{refName}", func(w http.ResponseWriter, r *http.Request) {
ref, ok := referenceByURL[r.URL.Path]
if !ok {
ref = references[0].References[0]
}
contents, ok := mdElementRenderers[ref.ID]
essaysRouter.Get("/{name}", func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
contents, ok := mdElementRenderers[name]
if !ok {
http.Error(w, "not found", http.StatusNotFound)
return
}
contentGroup := []ElementRenderer{}
if ref.Prev != nil {
contentGroup = append(contentGroup,
buttonLink().
CLASS("w-full no-underline").
HREF(ref.Prev.URL).
Text("Back to "+ref.Prev.Label),
)
}
contentGroup = append(contentGroup, contents)
if ref.Next != nil {
contentGroup = append(contentGroup,
buttonLink().
CLASS("w-full no-underline").
HREF(ref.Next.URL).
Text("Next "+ref.Next.Label),
)
var currentLink *SidebarLink
for _, group := range sidebarGroups {
for _, link := range group.Links {
if link.ID == name {
currentLink = link
break
}
}
}
anchors := mdAnchors[ref.ID]
prosePage(r, sidebarContents(r), Group(contentGroup...), anchors).Render(w)
SidebarPage(r, sidebarGroups, currentLink, contents).Render(r.Context(), w)
})
})

View File

@ -0,0 +1,227 @@
package site
import (
"net/http"
"strings"
)
templ Page() {
<!DOCTYPE html>
<html lang="en">
<head>
<title>DATASTAR</title>
<link rel="icon" href={ staticPath("images/datastar_icon.svg") }/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&family=Inter:wght@100..900&family=Gideon+Roman:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400;1,700;1,900&display=swap" rel="stylesheet"/>
<script src="https://code.iconify.design/iconify-icon/2.1.0/iconify-icon.min.js"></script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-QZ4RYHJW6X"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-QZ4RYHJW6X');
</script>
<script type="module" defer src={ staticPath("library/datastar.js") }></script>
// <script defer>
// function initHotReload() {
// console.log("Hot reload initializing")
// if (typeof(EventSource) !== "undefined") {
// const es = new EventSource("/hotreload");
// es.onmessage = function(event) {
// location.reload();
// }
// es.onerror = function(err) {
// console.log("lost connection to server, reloading");
// setTimeout(() => {
// location.reload();
// }, 500);
// };
// }
// }
// initHotReload();
// </script>
<link href={ staticPath("css/site.css") } rel="stylesheet" type="text/css"/>
</head>
<body class="flex flex-col w-full h-full min-h-screen overflow-y-scroll scrollbar scrollbar-thumb-primary scrollbar-track-accent">
{ children... }
</body>
</html>
}
templ icon(icon string, attrs ...string) {
<iconify-icon icon={ icon } { KVPairsAttrs(attrs...)... }></iconify-icon>
}
templ headerIconLink(iconName, href string) {
<a target="_blank" rel="noopener noreferrer" href={ templ.SafeURL(href) }>
@icon(iconName)
</a>
}
templ headerTopLevelLink(r *http.Request, text string) {
{{ url := templ.SafeURL("/"+ strings.ToLower(text)) }}
<a
href={ url }
class={
"font-bold uppercase link-hover",
templ.KV("link-primary", strings.HasPrefix(r.URL.Path, string(url))),
}
>{ text }</a>
}
templ headerExternalLinks() {
@headerIconLink("simple-icons:discord", "https://discord.gg/CHvPMrAp6F")
@headerIconLink("simple-icons:github", "https://github.com/delaneyj/datastar/tree/main/library")
@headerIconLink("simple-icons:npm", "https://www.npmjs.com/package/@sudodevnull/datastar")
@headerIconLink("simple-icons:twitter", "https://twitter.com/delaneyj")
}
templ headerTopLevelLinks(r *http.Request) {
@headerTopLevelLink(r, "Guide")
@headerTopLevelLink(r, "Reference")
@headerTopLevelLink(r, "Examples")
@headerTopLevelLink(r, "Essays")
}
templ header(r *http.Request) {
<div class="navbar bg-base-200">
<div class="flex justify-between w-full gap-4">
<div class="flex items-baseline gap-1">
<a
class="flex gap-1 text-2xl font-bold uppercase font-brand"
href="/"
>
<span>Datastar</span>
<img src={ staticPath("images/datastar_icon.svg") } class="h-8"/>
</a>
<div class="font-mono text-xs text-accent">v{ packageJSON.Version }</div>
</div>
<div class="hidden md:text-md lg:text-lg md:flex md:gap-4 md:visible ">
@headerTopLevelLinks(r)
</div>
<div class="hidden text-xl md:flex md:gap-4 md:visible">
@headerExternalLinks()
</div>
</div>
</div>
<div class="flex flex-wrap justify-around visible gap-2 pb-8 text-sm bg-base-200 md:hidden">
@headerTopLevelLinks(r)
</div>
<div class="visible navbar bg-base-300 md:hidden">
<div class="navbar-start">
<label for="sidebar-drawer" class="btn btn-ghost drawer-button">
@icon("material-symbols:menu")
</label>
</div>
<div class="gap-4 navbar-end">
@headerExternalLinks()
</div>
</div>
}
type SidebarLink struct {
ID string
Label string
URL templ.SafeURL
Prev *SidebarLink
Next *SidebarLink
IsDisabled bool
}
type SidebarGroup struct {
Label string
Links []*SidebarLink
}
templ SidebarPage(r *http.Request, sidebarGroups []*SidebarGroup, current *SidebarLink, contents string) {
@Page() {
@highlightCSS
<div class="drawer">
<input id="sidebar-drawer" type="checkbox" class="drawer-toggle"/>
<div class="flex flex-col min-h-screen drawer-content">
@header(r)
<div class="flex flex-1">
<aside class="flex-col hidden gap-4 px-4 py-8 overflow-y-auto md:flex min-w-64 bg-base-300 md:visible">
@SidebarContents(sidebarGroups, current)
</aside>
<div class="flex flex-col items-center w-full gap-16 p-4 md:p-16">
@SidebarPrevNextLinks(sidebarGroups, current)
<article
class={
"flex-1",
"prose prose-primary prose-sm md:prose lg:prose-lg p-4",
"prose-a:link-primary",
}
>
@templ.Raw(contents)
</article>
@SidebarPrevNextLinks(sidebarGroups, current)
</div>
</div>
</div>
<aside class="drawer-side">
<label for="sidebar-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="min-h-full p-4 menu w-80 bg-base-300 text-base-content">
@SidebarContents(sidebarGroups, current)
</ul>
</aside>
</div>
}
}
templ SidebarContents(sidebarGroups []*SidebarGroup, current *SidebarLink) {
<div class="flex flex-col gap-8 uppercase">
for i, grp := range sidebarGroups {
<div class="flex flex-col gap-2">
<h3 class="text-xs font-bold text-primary">{ grp.Label }</h3>
for _, link := range grp.Links {
if link.IsDisabled {
<div class="opacity-25">{ link.Label }</div>
} else {
<a
class="link-secondary link-hover"
href={ link.URL }
>{ link.Label }</a>
}
}
if i != len(sidebarGroups)-1 {
<div class="divider"></div>
}
</div>
}
</div>
}
templ SidebarPrevNextLinks(essayGroups []*SidebarGroup, current *SidebarLink) {
<div class="flex flex-wrap justify-between w-full gap-4">
<div>
if current.Prev != nil {
<a
class="btn btn-sm btn-ghost"
disabled?={ current.Prev.IsDisabled }
href={ current.Prev.URL }
>
@icon("material-symbols:arrow-back-ios")
{ current.Prev.Label }
</a>
}
</div>
<div>
if current.Next != nil {
<a
class="btn btn-sm btn-ghost"
disabled?={ current.Next.IsDisabled }
href={ current.Next.URL }
>
{ current.Next.Label }
@icon("material-symbols:arrow-forward-ios")
</a>
}
</div>
</div>
}

View File

@ -5,93 +5,14 @@ import (
"compress/gzip"
"fmt"
"log"
"net/http"
"os"
"strings"
. "github.com/delaneyj/gostar/elements"
"github.com/delaneyj/gostar/elements/iconify/mdi"
"github.com/delaneyj/gostar/elements/iconify/simple_icons"
"github.com/delaneyj/toolbelt"
"github.com/a-h/templ"
"github.com/dustin/go-humanize"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/parser"
"github.com/samber/lo"
)
var isDEV = os.Getenv("ENV") == "dev"
func page(children ...ElementRenderer) ElementRenderer {
return Group(
NewElement("!DOCTYPE").Attr("html", ""),
HTML().
LANG("en").
Children(
TITLE().Text("DATASTAR"),
HEAD(
META().CHARSET("UTF-8"),
META().NAME("viewport").CONTENT("width=device-width, initial-scale=1"),
META().NAME("description").CONTENT("Datastar is declarative real-time hypermedia framework"),
LINK().REL("icon").HREF(staticPath("images/datastar_icon.svg")),
LINK().REL("stylesheet").HREF(staticPath("css/site.css")),
cdnLink("@unocss/reset/tailwind.min.css"),
cdnLink("animate.css@4.1.1/animate.min.css"),
STYLE().Text(`
.un-cloak {
display: none;
}
`),
),
SCRIPT().
ASYNC().
SRC("https://www.googletagmanager.com/gtag/js?id=G-QZ4RYHJW6X"),
SCRIPT().Text(`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-QZ4RYHJW6X');
`),
BODY().
CLASS("font-sans min-h-screen un-cloak").
Children(
DIV().
CLASS("bg-accent-900 text-primary-50 w-full h-full").
Children(children...),
),
If(isDEV,
SCRIPT(Text(`
function initHotReload() {
console.log("Hot reload initializing")
if (typeof(EventSource) !== "undefined") {
const es = new EventSource("/hotreload");
es.onmessage = function(event) {
location.reload();
}
es.onerror = function(err) {
console.log("lost connection to server, reloading");
setTimeout(() => {
location.reload();
}, 250);
};
}
}
initHotReload();
// `)),
),
SCRIPT().
TYPE("module").
SRC(staticPath("library/datastar.js")).
DEFER(),
),
)
}
func cdnLink(css string) ElementRenderer {
return LINK().REL("stylesheet").HREF("https://cdn.jsdelivr.net/npm/" + css)
}
var iifeBuildSize string
func upsertIIfeBuildSize() string {
@ -117,7 +38,7 @@ func upsertIIfeBuildSize() string {
return iifeBuildSize
}
func markdownRenders(staticMdPath string) (mdElementRenderers map[string]ElementRenderer, mdAnchors map[string][]string, err error) {
func markdownRenders(staticMdPath string) (mdElementRenderers map[string]string, mdAnchors map[string][]string, err error) {
mdDir := "static/md/" + staticMdPath
docs, err := staticFS.ReadDir(mdDir)
if err != nil {
@ -127,7 +48,7 @@ func markdownRenders(staticMdPath string) (mdElementRenderers map[string]Element
// regExpImg := regexp.MustCompile(`(?P<whole>!\[[^\]]+]\((?P<path>[^)]+)\))`)
// prefix := []byte("/static/")
mdElementRenderers = map[string]ElementRenderer{}
mdElementRenderers = map[string]string{}
mdAnchors = map[string][]string{}
for _, de := range docs {
fullPath := mdDir + "/" + de.Name()
@ -152,119 +73,20 @@ func markdownRenders(staticMdPath string) (mdElementRenderers map[string]Element
renderedHTML := string(markdown.Render(doc, mdRenderer()))
name := de.Name()[0 : len(de.Name())-3]
mdElementRenderers[name] = Text(renderedHTML)
mdElementRenderers[name] = renderedHTML
mdAnchors[name] = anchors
}
return mdElementRenderers, mdAnchors, nil
}
func header(r *http.Request) ElementRenderer {
linkChildren := []ElementRenderer{
linkChild("https://discord.gg/CHvPMrAp6F", simple_icons.Discord()).TARGET("_blank").REL("noopener", "noreferrer"),
linkChild("https://github.com/delaneyj/datastar/tree/main/library", simple_icons.Github()).TARGET("_blank").REL("noopener", "noreferrer"),
linkChild("https://www.npmjs.com/package/@sudodevnull/datastar", simple_icons.Npm()).TARGET("_blank").REL("noopener", "noreferrer"),
func KVPairsAttrs(kvPairs ...string) templ.Attributes {
if len(kvPairs)%2 != 0 {
panic("kvPairs must be a multiple of 2")
}
topLevelLinks := lo.Map([]string{
"docs",
"reference",
"examples",
"essays",
}, func(name string, i int) ElementRenderer {
return link("/"+name, name, strings.HasPrefix(r.URL.Path, "/"+name))
})
return DIV().
CLASS("shadow-lg").
Children(
HEADER().CLASS("bg-accent-700 text-accent-200 px-4 py-2 flex flex-wrap gap-2 justify-between items-center").Children(
DIV().CLASS("flex flex-wrap gap-2 items-end").Children(
A().CLASS("flex gap-1").
HREF("/").
Children(
DIV().CLASS("font-brand font-bold text-2xl uppercase hidden md:block").Text("Datastar"),
datastarLogo().CLASS("h-8"),
),
DIV().
CLASS("font-mono text-accent-300").
TextF("v%s", packageJSON.Version),
),
DIV().
CLASS("flex flex-wrap gap-3 uppercase font-brand text-xs md:text-sm lg:text-lg items-center").
Children(topLevelLinks...),
DIV().CLASS("hidden md:flex").Children(
linkChildren...,
),
),
HEADER().CLASS("md:hidden bg-accent-800 text-accent-200 px-4 py-2 flex flex-wrap gap-2 justify-between items-center").Children(
BUTTON().
DATASTAR_ON("click", "$_sidebarOpen = true").
CLASS("bg-accent-600 hover:bg-accent-700 text-primary-50 p-2 rounded-md").
Children(mdi.Menu()),
DIV().CLASS("flex gap-1 text-2xl").Children(linkChildren...),
),
)
}
func prosePage(r *http.Request, sidebarContents ElementRenderer, contents ElementRenderer, asideAnchors []string) ElementRenderer {
log.Print(r.URL.Path)
return page(
highlightCSS,
DIV().
DATASTAR_STORE(map[string]any{
"_sidebarOpen": false,
}).
CLASS("grid grid-rows-[auto_1fr] h-screen").
Children(
header(r),
DIV().
CLASS("flex justify-start overflow-hidden relative").
Children(
DynIf(sidebarContents != nil, func() ElementRenderer {
return Group(
DIV().
CLASS("fixed inset-0 z-40 md:hidden").
CustomData("show", "$_sidebarOpen").
Children(
ASIDE().
CLASS("px-4 py-8 w-64 bg-accent-800 text-accent-200 relative z-30 h-full flex flex-col gap-4").
Children(
IMG().SRC(staticPath("images/datastar.svg")).ALT("logo").CLASS("h-8"),
DIV(sidebarContents).CLASS("overflow-y-auto h-full flex flex-col"),
),
DIV().
DATASTAR_ON("click", "$_sidebarOpen = false").
CLASS("fixed inset-0 bg-primary-900 bg-opacity-70 z-10"),
),
ASIDE(sidebarContents).
CLASS("hidden md:flex flex-col px-4 py-8 min-w-64 bg-accent-800 text-accent-200 hidden md:visible gap-4 overflow-y-auto"),
)
}),
DIV().
CLASS("md:flex md:justify-center px-2 py-4 md:px-4 w-full overflow-y-scroll scroll-mb-16").
Children(
// SignalStore,
DIV(contents).CLASS("prose prose-xs md:prose-2xl"),
),
DynIf(len(asideAnchors) > 0, func() ElementRenderer {
return ASIDE().
CLASS("min-w-52 py-4 h-screen text-accent-200 hidden lg:block transition-all").
Children(
DIV().
CLASS("h-full border-l-2 border-accent-600 px-4 py-4 flex flex-col gap-4").
Children(
DIV().CLASS("border-b border-accent-600 w-full ").Text("On this page"),
Range(asideAnchors, func(anchor string) ElementRenderer {
kebab := toolbelt.Kebab(anchor)
return link("#"+kebab, anchor, false).CLASS("font-light text-sm")
}),
),
)
}),
),
),
)
attrs := templ.Attributes{}
for i := 0; i < len(kvPairs); i += 2 {
attrs[kvPairs[i]] = kvPairs[i+1]
}
return attrs
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:877715a68505229029a7d08627225f98ab5fd7feede8b6945e17abdb4f10a0fe
size 748508

View File

@ -1,14 +1,10 @@
_March 29 2024_
# Another Dependency
Datastar is small, like really small. Even with all the plugins included it hovers between 10-11Kb minified+gzipped. One of the things that might not be know is how much of that is actually "external" dependencies.
Datastar is small, like really small. Even with all the plugins included it hovers between 10-12Kb minified+gzipped. One of the things that you might not know is how much of that is actually "external" dependencies.
![Datastar Dependencies](/static/images/essays/datastar_dependencies.png)
As you can see over half of the size is actually the dependencies.
Let's break it down:
It accounts for over half of the size is actually the dependencies; let's break it down:
| Dependency | Usage |
| --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |

View File

@ -1,6 +1,4 @@
_November 16 2023_
# `text/event-stream` All the Way Down
# Streams All the Way Down
[SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) is a really great idea. With a single request you can create a server driven stream of events. Using this for driving Datastar's [HTMX](https://htmx.org/) like fragments plugin is a natural fit. The only problem is that SSE doesn't support anything but the `GET` method. This means that you can't use SSE to send data to the server. This is a problem for HTMX, because HTMX uses the `POST` method to send data to the server. So, what to do?

View File

@ -1,5 +1,3 @@
_October 1, 2023_
# Grugs around the fire
## You probably don't need to care

View File

@ -1,7 +1,7 @@
_September 13 2023_
# Haikus
## HTMX
[HTMX](https://htmx.org/) has a wonderful haiku in its footer
> Javascript fatigue
@ -10,6 +10,8 @@ _September 13 2023_
> already in hand
## Datastar
I think this also applies to Datastar, but thought of a few that might match more to this project's goals.
> Hypermedia
@ -18,7 +20,7 @@ I think this also applies to Datastar, but thought of a few that might match mor
> attributes enhance
--
---
> Code compiles with ease,
@ -26,7 +28,7 @@ I think this also applies to Datastar, but thought of a few that might match mor
> Dev's joy, future peace.
--
---
> Standard tags convey,

View File

@ -1,5 +1,3 @@
_October 13 2023_
# 418 I'm a teapot
A discussion on the [HTMX Discord](https://discord.com/channels/725789699527933952/1156332851093065788) started talking about HTTP status codes. Apparently I hold the minority opinion that if

View File

@ -1,5 +1,3 @@
_September 1 2023_
# Why another framework?
## What's different?

View File

@ -1,5 +1,3 @@
_September 7, 2023_
# Yes, you want a build step
## In response to the HTMX essay [No Build Step](https://htmx.org/essays/no-build-step/)

View File

@ -9,9 +9,7 @@ This example actively searches a contacts database as the user enters text.
## Demo
<div>
<div id="active_search" data-on-load="$$get('/examples/active_search/data')">
Replace me
</div>
<div id="active_search" data-on-load="$$get('/examples/active_search/data')"></div>
</div>
The interesting part is the input field:

View File

@ -20,7 +20,6 @@ transition: all 1.2s;
id="bulk_update"
data-on-load="$$get('/examples/bulk_update/data')"
>
Replace me
</div>
## Explanation

View File

@ -8,7 +8,7 @@
id="contact_1"
data-on-load="$$get('/examples/click_to_edit/contact/1')"
>
Replace me
</div>
## Explanation

View File

@ -8,7 +8,6 @@
id="click_to_load"
data-on-load="$$get('/examples/click_to_load/data')"
>
Replace me
</div>
## Explanation

View File

@ -15,7 +15,6 @@ tr.datastar-swapping td {
id="delete_row"
data-on-load="$$get('/examples/delete_row/data')"
>
Replace me
</div>
## Explanation

View File

@ -8,7 +8,6 @@
id="dialogs"
data-on-load="$$get('/examples/dialogs_browser/data')"
>
Replace me!
</div>
## Explanation

View File

@ -23,9 +23,6 @@
data-on-click="$shouldDisable = true;$$get('/examples/disable_button/data')"
data-bind-disabled="$shouldDisable"
>Click Me</button>
<div id="results">
<h1>Results from server</h1>
</div>
</div>
## Explanation

View File

@ -8,7 +8,6 @@
id="edit_row"
data-on-load="$$get('/examples/edit_row/data')"
>
Replace me!
</div>
## Explanation

View File

@ -1,18 +1,19 @@
## Fetch Indicator
## Demo
## Demo
<div>
<div id="ind">Loading Indicator</div>
<button type="button" class="p-2 bg-accent-200 text-accent-700 shadow rounded" data-on-click="$$get('/examples/fetch_indicator/greet')" data-fetch-indicator="'#ind'" data-testid="greeting_button">Click me for a greeting</button>
<button
class="bg-success-300 hover:bg-success-500 text-success-800 font-bold py-2 px-4 rounded"
data-on-click="$$get('/examples/fetch_indicator/greet')"
data-fetch-indicator="'#ind'"
data-testid="greeting_button"
data-bind-disabled="$$isFetching('#ind')"
>
Click me for a greeting
</button>
<div id="greeting"></div>
<div>Example from https://github.com/delaneyj/datastar/pull/24</div>
<div data-store="{input: ''}">
<input type="text" data-bind-readonly="$$isFetching('#submit')" data-model="input" class="bg-accent-900 border-2 border-accent-600 text-accent-100 text-sm rounded-lg focus:ring-primary-400 focus:border-primary-400 block w-full p-2.5" />
<div data-show="$$isFetching('#submit')">Loading...</div>
<button id="submit" data-bind-disabled="$$isFetching('#submit')" data-on-click="$$get('/examples/fetch_indicator/greet')">Submit</button>
</div>
</div>
## Explanation
@ -20,16 +21,18 @@
```html
<div id="ind">Loading Indicator</div>
<button
type="button"
class="p-2 bg-accent-200 text-accent-700 shadow rounded"
data-on-click="$$get('/examples/fetch_indicator/greet')"
data-fetch-indicator="'#ind'"
data-bind-disabled="$$isFetching('#ind')"
>
Click me for a greeting
</button>
<div id="greeting"></div>
```
The `data-fetch-indicator` attribute is used to specify the element that should be shown as a loading indicator while the fetch request is in progress. The value of the attribute is a CSS selector that selects the element to be shown as the loading indicator.
The `data-fetch-indicator` attribute is used to specify the elements that should be made visible when the fetch request is in progress. The value of the attribute is a CSS selector that can represent multiple elements. The same `data-fetch-indicator` selector can be used by different elements at the same time.
The `$$isFetching("#ind")` action returns a computed value that allows you to easily react to the state of the indicator.
**Note:** The contents of the `data-fetch-indicator` is an expression. In this case, the expression is a string literal, hence the single quotes around the CSS selector.

View File

@ -8,7 +8,6 @@
id="file_upload"
data-on-load="$$get('/examples/file_upload/data')"
>
Replace me
</div>
## Explanation

View File

@ -37,9 +37,11 @@ data: fragment <<tr id=\"agent_60\"><td>Agent Smith 3c</td><td>void61@null.org</
## Demo
<div>
<div
id="infinite_scroll"
data-on-load="$$get('/examples/infinite_scroll/data')"
>
Replace me
</div>
<div id="more_btn"></div>
</div>

View File

@ -10,7 +10,6 @@ The only email that will be accepted is test@test.com.
id="inline_validation"
data-on-load="$$get('/examples/inline_validation/data')"
>
Replace me
</div>
## Explanation

View File

@ -1,36 +0,0 @@
## Is loading identifier
## Demo
<div>
<div data-show="$$isLoading('get_greet')" id="loadingIndicator">Loading Signal</div>
<button
type="button"
class="flex items-center select-none justify-center gap-1 px-4 py-2 rounded-lg bg-success-700"
data-on-click="$$get('/examples/is_loading_identifier/greet')"
data-is-loading-id="get_greet"
id="greetingButton"
>Click me for a greeting</button>
<div id="greeting"></div>
</div>
## Explanation
```html
<div>
<div data-show="$$isLoading('get_greet')" id="loadingIndicator">
Loading Signal
</div>
<button
type="button"
data-on-click="$$get('/examples/is_loading_identifier/greet')"
data-is-loading-id="get_greet"
id="greetingButton"
>
Click me for a greeting
</button>
<div id="greeting"></div>
</div>
```
The `data-is-loading-id` attribute is used to specify the name of the identifier that will be present in the store's isLoading array when an element is fetching.

View File

@ -11,7 +11,6 @@
</style>
<div id="lazy_load" data-on-load="$$get('/examples/lazy_load/data')">
Replace me
</div>
## Explanation

View File

@ -8,7 +8,6 @@
id="lazy_tabs"
data-on-load="$$get('/examples/lazy_tabs/data')"
>
Replace me
</div>
## Explanation

View File

@ -2,7 +2,7 @@
## Demo
<div id="contents" data-on-load="$$get('/examples/merge_options/reset')">Replace Me</div>
<div id="contents" data-on-load="$$get('/examples/merge_options/reset')"></div>
## Explanation

Some files were not shown because too many files have changed in this diff Show More