Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82a3314089 | ||
|
|
73f4e4009b | ||
|
|
c77ccc0796 | ||
|
|
d4b831492e | ||
|
|
ea7b9ad4a1 | ||
|
|
03b6d7453a |
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/go-via/via/h"
|
"github.com/ryanhamamura/via/h"
|
||||||
)
|
)
|
||||||
|
|
||||||
// actionTrigger represents a trigger to an event handler fn
|
// actionTrigger represents a trigger to an event handler fn
|
||||||
|
|||||||
@@ -37,4 +37,12 @@ type Options struct {
|
|||||||
// with scs LoadAndSave middleware. Configure the session manager before
|
// with scs LoadAndSave middleware. Configure the session manager before
|
||||||
// passing it (lifetime, cookie settings, store, etc).
|
// passing it (lifetime, cookie settings, store, etc).
|
||||||
SessionManager *scs.SessionManager
|
SessionManager *scs.SessionManager
|
||||||
|
|
||||||
|
// DatastarContent is the Datastar.js script content.
|
||||||
|
// If nil, the embedded default is used.
|
||||||
|
DatastarContent []byte
|
||||||
|
|
||||||
|
// DatastarPath is the URL path where the script is served.
|
||||||
|
// Defaults to "/_datastar.js" if empty.
|
||||||
|
DatastarPath string
|
||||||
}
|
}
|
||||||
|
|||||||
33
context.go
33
context.go
@@ -11,7 +11,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-via/via/h"
|
"github.com/ryanhamamura/via/h"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Context is the living bridge between Go and the browser.
|
// Context is the living bridge between Go and the browser.
|
||||||
@@ -318,6 +318,37 @@ func (c *Context) ExecScript(s string) {
|
|||||||
c.sendPatch(patch{patchTypeScript, s})
|
c.sendPatch(patch{patchTypeScript, s})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Redirect navigates the browser to the given URL.
|
||||||
|
// This triggers a full page navigation - the current context will be disposed
|
||||||
|
// and a new context created at the destination URL.
|
||||||
|
func (c *Context) Redirect(url string) {
|
||||||
|
if url == "" {
|
||||||
|
c.app.logWarn(c, "redirect failed: empty url")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.sendPatch(patch{patchTypeRedirect, url})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirectf navigates the browser to a URL constructed from the format string and arguments.
|
||||||
|
func (c *Context) Redirectf(format string, a ...any) {
|
||||||
|
c.Redirect(fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceURL updates the browser's URL and history without triggering navigation.
|
||||||
|
// Useful for updating query params or path to reflect UI state changes.
|
||||||
|
func (c *Context) ReplaceURL(url string) {
|
||||||
|
if url == "" {
|
||||||
|
c.app.logWarn(c, "replace url failed: empty url")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.sendPatch(patch{patchTypeReplaceURL, url})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceURLf updates the browser's URL using a format string.
|
||||||
|
func (c *Context) ReplaceURLf(format string, a ...any) {
|
||||||
|
c.ReplaceURL(fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
// stopAllRoutines stops all go routines tied to this Context preventing goroutine leaks.
|
// stopAllRoutines stops all go routines tied to this Context preventing goroutine leaks.
|
||||||
func (c *Context) stopAllRoutines() {
|
func (c *Context) stopAllRoutines() {
|
||||||
select {
|
select {
|
||||||
|
|||||||
7
go.mod
7
go.mod
@@ -1,4 +1,4 @@
|
|||||||
module github.com/go-via/via
|
module github.com/ryanhamamura/via
|
||||||
|
|
||||||
go 1.25.4
|
go 1.25.4
|
||||||
|
|
||||||
@@ -6,6 +6,8 @@ require maragu.dev/gomponents v1.2.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||||
|
github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de
|
||||||
|
github.com/alexedwards/scs/v2 v2.9.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.32
|
github.com/mattn/go-sqlite3 v1.14.32
|
||||||
github.com/starfederation/datastar-go v1.0.3
|
github.com/starfederation/datastar-go v1.0.3
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
@@ -13,13 +15,10 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/CAFxX/httpcompression v0.0.9 // indirect
|
github.com/CAFxX/httpcompression v0.0.9 // indirect
|
||||||
github.com/alexedwards/scs/v2 v2.9.0 // indirect
|
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
11
go.sum
11
go.sum
@@ -2,6 +2,8 @@ github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWG
|
|||||||
github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM=
|
github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
|
github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de h1:c72K9HLu6K442et0j3BUL/9HEYaUJouLkkVANdmqTOo=
|
||||||
|
github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
|
||||||
github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90=
|
github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90=
|
||||||
github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
|
github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
|
||||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
@@ -17,11 +19,7 @@ github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQs
|
|||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
||||||
@@ -45,9 +43,8 @@ github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g
|
|||||||
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
|
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
1
h/h.go
1
h/h.go
@@ -79,7 +79,6 @@ func HTML5(p HTML5Props) H {
|
|||||||
Body: retype(p.Body),
|
Body: retype(p.Body),
|
||||||
HTMLAttrs: retype(p.HTMLAttrs),
|
HTMLAttrs: retype(p.HTMLAttrs),
|
||||||
}
|
}
|
||||||
gp.Head = append(gp.Head, Script(Type("module"), Src("/_datastar.js")))
|
|
||||||
return gc.HTML5(gp)
|
return gc.HTML5(gp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
|
||||||
"github.com/go-via/via"
|
"github.com/ryanhamamura/via"
|
||||||
"github.com/go-via/via/h"
|
"github.com/ryanhamamura/via/h"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/go-via/via"
|
"github.com/ryanhamamura/via"
|
||||||
"github.com/go-via/via/h"
|
"github.com/ryanhamamura/via/h"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Counter struct{ Count int }
|
type Counter struct{ Count int }
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/go-via/via"
|
"github.com/ryanhamamura/via"
|
||||||
"github.com/go-via/via/h"
|
"github.com/ryanhamamura/via/h"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/go-via/via"
|
"github.com/ryanhamamura/via"
|
||||||
"github.com/go-via/via/h"
|
"github.com/ryanhamamura/via/h"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/go-via/via"
|
"github.com/ryanhamamura/via"
|
||||||
// "github.com/go-via/via-plugin-picocss/picocss"
|
// "github.com/go-via/via-plugin-picocss/picocss"
|
||||||
"github.com/go-via/via/h"
|
"github.com/ryanhamamura/via/h"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Counter struct{ Count int }
|
type Counter struct{ Count int }
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package main
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/go-via/via"
|
"github.com/ryanhamamura/via"
|
||||||
// "github.com/go-via/via-plugin-picocss/picocss"
|
// "github.com/go-via/via-plugin-picocss/picocss"
|
||||||
. "github.com/go-via/via/h"
|
. "github.com/ryanhamamura/via/h"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/go-via/via"
|
"github.com/ryanhamamura/via"
|
||||||
// "github.com/go-via/via-plugin-picocss/picocss"
|
// "github.com/go-via/via-plugin-picocss/picocss"
|
||||||
"github.com/go-via/via/h"
|
"github.com/ryanhamamura/via/h"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Counter struct{ Count int }
|
type Counter struct{ Count int }
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/go-via/via"
|
"github.com/ryanhamamura/via"
|
||||||
"github.com/go-via/via/h"
|
"github.com/ryanhamamura/via/h"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Example of a Via application with a plugin that adds PicoCSS. The plugin
|
// Example of a Via application with a plugin that adds PicoCSS. The plugin
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-via/via"
|
"github.com/ryanhamamura/via"
|
||||||
// "github.com/go-via/via-plugin-picocss/picocss"
|
// "github.com/go-via/via-plugin-picocss/picocss"
|
||||||
"github.com/go-via/via/h"
|
"github.com/ryanhamamura/via/h"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -1,17 +1,37 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/go-via/via"
|
"database/sql"
|
||||||
"github.com/go-via/via/h"
|
"log"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"github.com/ryanhamamura/via"
|
||||||
|
"github.com/ryanhamamura/via/h"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Open SQLite database for persistent sessions
|
||||||
|
db, err := sql.Open("sqlite3", "sessions.db")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to open database: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Create session manager with SQLite store
|
||||||
|
sm, err := via.NewSQLiteSessionManager(db)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create session manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
v := via.New()
|
v := via.New()
|
||||||
|
v.Config(via.Options{
|
||||||
|
ServerAddress: ":7331",
|
||||||
|
SessionManager: sm,
|
||||||
|
})
|
||||||
|
|
||||||
v.Page("/", func(c *via.Context) {
|
// Login page
|
||||||
username := c.Session().GetString("username")
|
v.Page("/login", func(c *via.Context) {
|
||||||
flash := c.Session().PopString("flash")
|
flash := c.Session().PopString("flash")
|
||||||
|
|
||||||
usernameInput := c.Signal("")
|
usernameInput := c.Signal("")
|
||||||
|
|
||||||
login := c.Action(func() {
|
login := c.Action(func() {
|
||||||
@@ -20,38 +40,69 @@ func main() {
|
|||||||
c.Session().Set("username", name)
|
c.Session().Set("username", name)
|
||||||
c.Session().Set("flash", "Welcome, "+name+"!")
|
c.Session().Set("flash", "Welcome, "+name+"!")
|
||||||
c.Session().RenewToken()
|
c.Session().RenewToken()
|
||||||
|
c.Redirect("/dashboard")
|
||||||
}
|
}
|
||||||
c.Sync()
|
|
||||||
})
|
|
||||||
|
|
||||||
logout := c.Action(func() {
|
|
||||||
c.Session().Set("flash", "Goodbye!")
|
|
||||||
c.Session().Delete("username")
|
|
||||||
c.Sync()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
c.View(func() h.H {
|
c.View(func() h.H {
|
||||||
|
// Already logged in? Redirect to dashboard
|
||||||
|
if c.Session().GetString("username") != "" {
|
||||||
|
c.Redirect("/dashboard")
|
||||||
|
return h.Div()
|
||||||
|
}
|
||||||
|
|
||||||
var flashMsg h.H
|
var flashMsg h.H
|
||||||
if flash != "" {
|
if flash != "" {
|
||||||
flashMsg = h.P(h.Text(flash), h.Style("color: green"))
|
flashMsg = h.P(h.Text(flash), h.Style("color: green"))
|
||||||
}
|
}
|
||||||
|
return h.Div(
|
||||||
|
flashMsg,
|
||||||
|
h.H1(h.Text("Login")),
|
||||||
|
h.Input(h.Type("text"), h.Placeholder("Username"), usernameInput.Bind()),
|
||||||
|
h.Button(h.Text("Login"), login.OnClick()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Dashboard page (protected)
|
||||||
|
v.Page("/dashboard", func(c *via.Context) {
|
||||||
|
logout := c.Action(func() {
|
||||||
|
c.Session().Set("flash", "Goodbye!")
|
||||||
|
c.Session().Delete("username")
|
||||||
|
c.Redirect("/login")
|
||||||
|
})
|
||||||
|
|
||||||
|
c.View(func() h.H {
|
||||||
|
username := c.Session().GetString("username")
|
||||||
|
|
||||||
|
// Not logged in? Redirect to login
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return h.Div(
|
c.Session().Set("flash", "Please log in first")
|
||||||
flashMsg,
|
c.Redirect("/login")
|
||||||
h.H1(h.Text("Login")),
|
return h.Div()
|
||||||
h.Input(h.Type("text"), h.Placeholder("Username"), usernameInput.Bind()),
|
}
|
||||||
h.Button(h.Text("Login"), login.OnClick()),
|
|
||||||
)
|
flash := c.Session().PopString("flash")
|
||||||
|
var flashMsg h.H
|
||||||
|
if flash != "" {
|
||||||
|
flashMsg = h.P(h.Text(flash), h.Style("color: green"))
|
||||||
}
|
}
|
||||||
return h.Div(
|
return h.Div(
|
||||||
flashMsg,
|
flashMsg,
|
||||||
h.H1(h.Textf("Hello, %s!", username)),
|
h.H1(h.Textf("Dashboard - Hello, %s!", username)),
|
||||||
h.P(h.Text("Your session persists across page refreshes.")),
|
h.P(h.Text("Your session persists across page refreshes.")),
|
||||||
h.Button(h.Text("Logout"), logout.OnClick()),
|
h.Button(h.Text("Logout"), logout.OnClick()),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Redirect root to login
|
||||||
|
v.Page("/", func(c *via.Context) {
|
||||||
|
c.View(func() h.H {
|
||||||
|
c.Redirect("/login")
|
||||||
|
return h.Div()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
v.Start()
|
v.Start()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-via/via"
|
"github.com/ryanhamamura/via"
|
||||||
"github.com/go-via/via/h"
|
"github.com/ryanhamamura/via/h"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
23
session.go
23
session.go
@@ -2,11 +2,34 @@ package via
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/alexedwards/scs/sqlite3store"
|
||||||
"github.com/alexedwards/scs/v2"
|
"github.com/alexedwards/scs/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NewSQLiteSessionManager creates a session manager using SQLite for persistence.
|
||||||
|
// Creates the sessions table if it doesn't exist.
|
||||||
|
// The returned manager can be configured further (Lifetime, Cookie settings, etc.)
|
||||||
|
// before passing to Options.SessionManager.
|
||||||
|
func NewSQLiteSessionManager(db *sql.DB) (*scs.SessionManager, error) {
|
||||||
|
_, err := db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
token TEXT PRIMARY KEY,
|
||||||
|
data BLOB NOT NULL,
|
||||||
|
expiry REAL NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS sessions_expiry_idx ON sessions(expiry);
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sm := scs.New()
|
||||||
|
sm.Store = sqlite3store.New(db)
|
||||||
|
return sm, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Session provides access to the user's session data.
|
// Session provides access to the user's session data.
|
||||||
// Session data persists across page views for the same browser.
|
// Session data persists across page views for the same browser.
|
||||||
type Session struct {
|
type Session struct {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-via/via/h"
|
"github.com/ryanhamamura/via/h"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Signal represents a value that is reactive in the browser. Signals
|
// Signal represents a value that is reactive in the browser. Signals
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
// "net/http/httptest"
|
// "net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-via/via/h"
|
"github.com/ryanhamamura/via/h"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
74
via.go
74
via.go
@@ -15,13 +15,14 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/alexedwards/scs/v2"
|
"github.com/alexedwards/scs/v2"
|
||||||
"github.com/go-via/via/h"
|
"github.com/ryanhamamura/via/h"
|
||||||
"github.com/starfederation/datastar-go/datastar"
|
"github.com/starfederation/datastar-go/datastar"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,14 +32,17 @@ var datastarJS []byte
|
|||||||
// V is the root application.
|
// V is the root application.
|
||||||
// It manages page routing, user sessions, and SSE connections for live updates.
|
// It manages page routing, user sessions, and SSE connections for live updates.
|
||||||
type V struct {
|
type V struct {
|
||||||
cfg Options
|
cfg Options
|
||||||
mux *http.ServeMux
|
mux *http.ServeMux
|
||||||
contextRegistry map[string]*Context
|
contextRegistry map[string]*Context
|
||||||
contextRegistryMutex sync.RWMutex
|
contextRegistryMutex sync.RWMutex
|
||||||
documentHeadIncludes []h.H
|
documentHeadIncludes []h.H
|
||||||
documentFootIncludes []h.H
|
documentFootIncludes []h.H
|
||||||
devModePageInitFnMap map[string]func(*Context)
|
devModePageInitFnMap map[string]func(*Context)
|
||||||
sessionManager *scs.SessionManager
|
sessionManager *scs.SessionManager
|
||||||
|
datastarPath string
|
||||||
|
datastarContent []byte
|
||||||
|
datastarOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *V) logFatal(format string, a ...any) {
|
func (v *V) logFatal(format string, a ...any) {
|
||||||
@@ -107,6 +111,12 @@ func (v *V) Config(cfg Options) {
|
|||||||
if cfg.SessionManager != nil {
|
if cfg.SessionManager != nil {
|
||||||
v.sessionManager = cfg.SessionManager
|
v.sessionManager = cfg.SessionManager
|
||||||
}
|
}
|
||||||
|
if cfg.DatastarContent != nil {
|
||||||
|
v.datastarContent = cfg.DatastarContent
|
||||||
|
}
|
||||||
|
if cfg.DatastarPath != "" {
|
||||||
|
v.datastarPath = cfg.DatastarPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendToHead appends the given h.H nodes to the head of the base HTML document.
|
// AppendToHead appends the given h.H nodes to the head of the base HTML document.
|
||||||
@@ -140,6 +150,7 @@ func (v *V) AppendToFoot(elements ...h.H) {
|
|||||||
// })
|
// })
|
||||||
// })
|
// })
|
||||||
func (v *V) Page(route string, initContextFn func(c *Context)) {
|
func (v *V) Page(route string, initContextFn func(c *Context)) {
|
||||||
|
v.ensureDatastarHandler()
|
||||||
// check for panics
|
// check for panics
|
||||||
func() {
|
func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -175,7 +186,7 @@ func (v *V) Page(route string, initContextFn func(c *Context)) {
|
|||||||
if v.cfg.DevMode {
|
if v.cfg.DevMode {
|
||||||
v.devModePersist(c)
|
v.devModePersist(c)
|
||||||
}
|
}
|
||||||
headElements := []h.H{}
|
headElements := []h.H{h.Script(h.Type("module"), h.Src(v.datastarPath))}
|
||||||
headElements = append(headElements, v.documentHeadIncludes...)
|
headElements = append(headElements, v.documentHeadIncludes...)
|
||||||
headElements = append(headElements,
|
headElements = append(headElements,
|
||||||
h.Meta(h.Data("signals", fmt.Sprintf("{'via-ctx':'%s'}", id))),
|
h.Meta(h.Data("signals", fmt.Sprintf("{'via-ctx':'%s'}", id))),
|
||||||
@@ -257,6 +268,15 @@ func (v *V) HTTPServeMux() *http.ServeMux {
|
|||||||
return v.mux
|
return v.mux
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *V) ensureDatastarHandler() {
|
||||||
|
v.datastarOnce.Do(func() {
|
||||||
|
v.mux.HandleFunc("GET "+v.datastarPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/javascript")
|
||||||
|
_, _ = w.Write(v.datastarContent)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (v *V) devModePersist(c *Context) {
|
func (v *V) devModePersist(c *Context) {
|
||||||
p := filepath.Join(".via", "devmode", "ctx.json")
|
p := filepath.Join(".via", "devmode", "ctx.json")
|
||||||
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
||||||
@@ -359,6 +379,8 @@ const (
|
|||||||
patchTypeElements = iota
|
patchTypeElements = iota
|
||||||
patchTypeSignals
|
patchTypeSignals
|
||||||
patchTypeScript
|
patchTypeScript
|
||||||
|
patchTypeRedirect
|
||||||
|
patchTypeReplaceURL
|
||||||
)
|
)
|
||||||
|
|
||||||
type patch struct {
|
type patch struct {
|
||||||
@@ -375,6 +397,8 @@ func New() *V {
|
|||||||
contextRegistry: make(map[string]*Context),
|
contextRegistry: make(map[string]*Context),
|
||||||
devModePageInitFnMap: make(map[string]func(*Context)),
|
devModePageInitFnMap: make(map[string]func(*Context)),
|
||||||
sessionManager: scs.New(),
|
sessionManager: scs.New(),
|
||||||
|
datastarPath: "/_datastar.js",
|
||||||
|
datastarContent: datastarJS,
|
||||||
cfg: Options{
|
cfg: Options{
|
||||||
DevMode: false,
|
DevMode: false,
|
||||||
ServerAddress: ":3000",
|
ServerAddress: ":3000",
|
||||||
@@ -383,16 +407,7 @@ func New() *V {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
v.mux.HandleFunc("GET /_datastar.js", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/javascript")
|
|
||||||
_, _ = w.Write(datastarJS)
|
|
||||||
})
|
|
||||||
|
|
||||||
v.mux.HandleFunc("GET /_sse", func(w http.ResponseWriter, r *http.Request) {
|
v.mux.HandleFunc("GET /_sse", func(w http.ResponseWriter, r *http.Request) {
|
||||||
isReconnect := false
|
|
||||||
if r.Header.Get("last-event-id") == "via" {
|
|
||||||
isReconnect = true
|
|
||||||
}
|
|
||||||
var sigs map[string]any
|
var sigs map[string]any
|
||||||
_ = datastar.ReadSignals(r, &sigs)
|
_ = datastar.ReadSignals(r, &sigs)
|
||||||
cID, _ := sigs["via-ctx"].(string)
|
cID, _ := sigs["via-ctx"].(string)
|
||||||
@@ -417,11 +432,7 @@ func New() *V {
|
|||||||
v.logDebug(c, "SSE connection established")
|
v.logDebug(c, "SSE connection established")
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if isReconnect || v.cfg.DevMode {
|
c.Sync()
|
||||||
c.Sync()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.SyncSignals()
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -453,6 +464,21 @@ func New() *V {
|
|||||||
v.logErr(c, "ExecuteScript failed: %v", err)
|
v.logErr(c, "ExecuteScript failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case patchTypeRedirect:
|
||||||
|
if err := sse.Redirect(patch.content); err != nil {
|
||||||
|
if sse.Context().Err() == nil {
|
||||||
|
v.logErr(c, "Redirect failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case patchTypeReplaceURL:
|
||||||
|
parsedURL, err := url.Parse(patch.content)
|
||||||
|
if err != nil {
|
||||||
|
v.logErr(c, "ReplaceURL failed to parse URL: %v", err)
|
||||||
|
} else if err := sse.ReplaceURL(*parsedURL); err != nil {
|
||||||
|
if sse.Context().Err() == nil {
|
||||||
|
v.logErr(c, "ReplaceURL failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
50
via_test.go
50
via_test.go
@@ -5,7 +5,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-via/via/h"
|
"github.com/ryanhamamura/via/h"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,6 +28,10 @@ func TestPageRoute(t *testing.T) {
|
|||||||
|
|
||||||
func TestDatastarJS(t *testing.T) {
|
func TestDatastarJS(t *testing.T) {
|
||||||
v := New()
|
v := New()
|
||||||
|
v.Page("/", func(c *Context) {
|
||||||
|
c.View(func() h.H { return h.Div() })
|
||||||
|
})
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/_datastar.js", nil)
|
req := httptest.NewRequest("GET", "/_datastar.js", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
v.mux.ServeHTTP(w, req)
|
v.mux.ServeHTTP(w, req)
|
||||||
@@ -37,6 +41,50 @@ func TestDatastarJS(t *testing.T) {
|
|||||||
assert.Contains(t, w.Body.String(), "🖕JS_DS🚀")
|
assert.Contains(t, w.Body.String(), "🖕JS_DS🚀")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCustomDatastarContent(t *testing.T) {
|
||||||
|
customScript := []byte("// Custom Datastar Script")
|
||||||
|
v := New()
|
||||||
|
v.Config(Options{
|
||||||
|
DatastarContent: customScript,
|
||||||
|
})
|
||||||
|
v.Page("/", func(c *Context) {
|
||||||
|
c.View(func() h.H { return h.Div() })
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/_datastar.js", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
v.mux.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Equal(t, "application/javascript", w.Header().Get("Content-Type"))
|
||||||
|
assert.Contains(t, w.Body.String(), "Custom Datastar Script")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomDatastarPath(t *testing.T) {
|
||||||
|
v := New()
|
||||||
|
v.Config(Options{
|
||||||
|
DatastarPath: "/assets/datastar.js",
|
||||||
|
})
|
||||||
|
v.Page("/test", func(c *Context) {
|
||||||
|
c.View(func() h.H { return h.Div() })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Custom path should serve the script
|
||||||
|
req := httptest.NewRequest("GET", "/assets/datastar.js", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
v.mux.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Equal(t, "application/javascript", w.Header().Get("Content-Type"))
|
||||||
|
assert.Contains(t, w.Body.String(), "🖕JS_DS🚀")
|
||||||
|
|
||||||
|
// Page should reference the custom path in script tag
|
||||||
|
req2 := httptest.NewRequest("GET", "/test", nil)
|
||||||
|
w2 := httptest.NewRecorder()
|
||||||
|
v.mux.ServeHTTP(w2, req2)
|
||||||
|
assert.Contains(t, w2.Body.String(), `src="/assets/datastar.js"`)
|
||||||
|
}
|
||||||
|
|
||||||
func TestSignal(t *testing.T) {
|
func TestSignal(t *testing.T) {
|
||||||
var sig *signal
|
var sig *signal
|
||||||
v := New()
|
v := New()
|
||||||
|
|||||||
Reference in New Issue
Block a user