V0.15.0 (#68)
* 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:
parent
f754c6e13f
commit
ccce8472eb
|
@ -2,4 +2,5 @@ debug_bin*
|
|||
node_modules
|
||||
|
||||
site_bin
|
||||
.task
|
||||
.task
|
||||
*_templ.go
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
37
Taskfile.yml
37
Taskfile.yml
|
@ -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:
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2
|
||||
}
|
|
@ -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
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
plugins: [require('tailwindcss'), require('autoprefixer')],
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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))
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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") }/>
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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())
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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>")
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:877715a68505229029a7d08627225f98ab5fd7feede8b6945e17abdb4f10a0fe
|
||||
size 748508
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
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 |
|
||||
| --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
_October 1, 2023_
|
||||
|
||||
# Grugs around the fire
|
||||
|
||||
## You probably don't need to care
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
_September 1 2023_
|
||||
|
||||
# Why another framework?
|
||||
|
||||
## What's different?
|
||||
|
|
|
@ -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/)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -20,7 +20,6 @@ transition: all 1.2s;
|
|||
id="bulk_update"
|
||||
data-on-load="$$get('/examples/bulk_update/data')"
|
||||
>
|
||||
Replace me
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
id="contact_1"
|
||||
data-on-load="$$get('/examples/click_to_edit/contact/1')"
|
||||
>
|
||||
Replace me
|
||||
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
id="click_to_load"
|
||||
data-on-load="$$get('/examples/click_to_load/data')"
|
||||
>
|
||||
Replace me
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
|
|
@ -15,7 +15,6 @@ tr.datastar-swapping td {
|
|||
id="delete_row"
|
||||
data-on-load="$$get('/examples/delete_row/data')"
|
||||
>
|
||||
Replace me
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
id="dialogs"
|
||||
data-on-load="$$get('/examples/dialogs_browser/data')"
|
||||
>
|
||||
Replace me!
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
id="edit_row"
|
||||
data-on-load="$$get('/examples/edit_row/data')"
|
||||
>
|
||||
Replace me!
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
id="file_upload"
|
||||
data-on-load="$$get('/examples/file_upload/data')"
|
||||
>
|
||||
Replace me
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -11,7 +11,6 @@
|
|||
</style>
|
||||
|
||||
<div id="lazy_load" data-on-load="$$get('/examples/lazy_load/data')">
|
||||
Replace me
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
id="lazy_tabs"
|
||||
data-on-load="$$get('/examples/lazy_tabs/data')"
|
||||
>
|
||||
Replace me
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue