Todos Example
The starter template comes with a todos application.
Main
The server defines a few routes. func main() {
server := servers.New()
server.Efs = efs
server.Render = ssr.New(1)
server.Routes = []routes.Route{
{Pattern: "GET /", Handler: fallback.View},
{Pattern: "GET /welcome", Handler: welcome.View},
{Pattern: "GET /todos", Handler: todos.View},
{Pattern: "POST /toggle", Handler: todos.Toggle},
{Pattern: "POST /add", Handler: todos.Add},
{Pattern: "POST /remove", Handler: todos.Remove},
}
if err := servers.Start(server); err != nil {
log.Fatal(err)
}
} Fallback
The GET / pattern acts as a fallback.
With that in mind, the fallback handler tries to send back a matching file using send.RequestedFile() or, if it doesn’t exist, the Welcome view is sent instead using welcome.View() . func View(client *clients.Client) {
if !send.RequestedFile(client) {
welcome.View(client)
}
} Welcome View
The Welcome view, among other things, renders a hyperlink pointing to "GET /todos". func View(client *clients.Client) {
send.View(client, views.View{Name: "Welcome"})
} <script lang="ts">
import Icon from "$lib/components/icons/icon.svelte"
import Layout from "$lib/components/layout.svelte"
import Logo from "$lib/components/logo.svelte"
import { href } from "$lib/scripts/core/href.ts"
import { mdiArrowRight } from "@mdi/js"
</script>
<Layout title="Welcome">
<Logo />
{@render Description()}
<div class="pt-6"></div>
<div class="flex justify-center gap-2 relative">
{@render TodosButton()}
{@render DocumentationButton()}
</div>
</Layout>
{#snippet Description()}
<p>Modern Go + Svelte Framework</p>
{/snippet}
{#snippet TodosButton()}
<a {...href("/todos")}>
<button>
<span>Show Todos</span>
<Icon path={mdiArrowRight} />
</button>
</a>
{/snippet}
{#snippet DocumentationButton()}
<a href="https://razshare.github.io/frizzante-docs/guides/get-started" target="_blank">
<button>Documentation</button>
</a>
{/snippet} Todos View
The GET /todos pattern is then captured by a Go handler function, which sends back the "Todos"
view along with a list of items retrieved from the user’s session. func View(client *clients.Client) {
var session schema.Session
defer func() {
if err := databases.Queries.ModifySessionById(client.Request.Context(), schema.ModifySessionByIdParams{
ID: session.ID,
}); err != nil {
logs.Error(client, err)
}
}()
receive.Session(client, &session)
context := client.Request.Context()
var err error
var todos []schema.Todo
if todos, err = databases.Queries.FindTodosBySessionId(context, session.ID); err != nil {
session.Error = err.Error()
return
}
send.View(client, views.View{Name: "Todos", Props: Props{
Error: session.Error,
Items: todos,
}})
} The Todos view is a CRUD web ui. <script lang="ts">
//...
import type { Props, schemas } from "$lib/types/server/main/lib/routes/todos/props"
let { items, error }: Props = $props()
//...
</script> <Layout title="Todos">
{@render Description()}
{@render AddTodoForm()}
{@render TodoList(items)}
{@render BackButton()}
</Layout> List Todos
Items are listed by iterating over the items prop (which comes from the server). {#snippet TodoList(items: schema.Todo[])}
{#if items.length > 0}
<div class="todo-list">
{#each items as todo, index (index)}
<div transition:slide class="item">
{@render ToggleTodoButton(todo, index)}
{@render RemoveTodoButton(index)}
</div>
{/each}
</div>
{@render CountUncheckedTodos()}
{:else}
<div class="todo-list">
{@render NoTodosFound()}
</div>
{/if}
{/snippet} Each item has remove and toggle buttons. Note
Type sessions.Todo is an autogenerated type definition. See type definitions. Remove Todos
Items are removed by submitting a form to POST /remove . {#snippet RemoveTodoButton(id: string)}
<form method="POST" {...action("/remove")}>
<input type="hidden" name="id" value={id} />
<label aria-label="Delete">
<Icon path={mdiClose} />
<button aria-label="remove"></button>
</label>
</form>
{/snippet} The form is then captured by the Remove handler, which does some basic validation, error handling and then finally removes the item from the session. func Remove(client *clients.Client) {
var session schema.Session
defer send.Navigate(client, "/todos")
defer func() {
if err := databases.Queries.ModifySessionById(client.Request.Context(), schema.ModifySessionByIdParams{
ID: session.ID,
Error: session.Error,
}); err != nil {
logs.Error(client, err)
}
}()
receive.Session(client, &session)
var form struct {
Id string `form:"id"`
}
if !receive.Form(client, &form) {
session.Error = "could not parse form"
return
}
context := client.Request.Context()
if err := databases.Queries.RemoveTodosByIdAndSessionId(context, schema.RemoveTodosByIdAndSessionIdParams{
ID: form.Id,
SessionID: session.ID,
}); err != nil {
session.Error = err.Error()
return
}
} Toggle Todos
Items are toggled by submitting a form to POST /toggle . {#snippet ToggleTodoButton(todo: schema.Todo, id: string)}
{@const aria = todo.checked > 0 ? "Uncheck" : "Check"}
{@const nextValue = todo.checked > 0 ? 0 : 1}
{@const icon = todo.checked ? mdiCheckCircleOutline : mdiCircleOutline}
<form method="POST" {...action("/toggle")}>
<input type="hidden" name="id" value={id} />
<input type="hidden" name="value" value={nextValue} />
<label aria-label={aria}>
<Icon path={icon} />
{#if todo.checked}
<strike>
<span>{todo.description}</span>
</strike>
{:else}
<span>{todo.description}</span>
{/if}
<button aria-label="toggle"></button>
</label>
</form>
{/snippet} The form is then captured by the Toggle handler. func Toggle(client *clients.Client) {
var session schema.Session
defer send.Navigate(client, "/todos")
defer func() {
if err := databases.Queries.ModifySessionById(client.Request.Context(), schema.ModifySessionByIdParams{
ID: session.ID,
Error: session.Error,
}); err != nil {
logs.Error(client, err)
}
}()
receive.Session(client, &session)
var form struct {
Id string `form:"id"`
Value int64 `form:"value"`
}
if !receive.Form(client, &form) {
session.Error = "could not parse form"
return
}
if err := databases.Queries.ToggleTodosByIdAndSessionId(client.Request.Context(), schema.ToggleTodosByIdAndSessionIdParams{
ID: form.Id,
SessionID: session.ID,
Checked: form.Value,
}); err != nil {
session.Error = err.Error()
return
}
} Add Todos
Items are added by submitting a form to POST /add . {#snippet AddTodoForm()}
<form method="POST" {...action("/add")}>
<input type="text" name="description" placeholder="Add a new task..." />
<br />
<button type="submit">
<Icon path={mdiPlus} />
<span>Add</span>
</button>
</form>
{#if error}
<br />
<div transition:slide>
<span>{error}</span>
</div>
{/if}
{/snippet} The form is then captured by the Add handler. func Add(client *clients.Client) {
var session schema.Session
defer send.Navigate(client, "/todos")
defer func() {
if err := databases.Queries.ModifySessionById(client.Request.Context(), schema.ModifySessionByIdParams{
ID: session.ID,
Error: session.Error,
}); err != nil {
logs.Error(client, err)
}
}()
receive.Session(client, &session)
var form struct {
Description string `form:"description"`
}
if !receive.Form(client, &form) {
session.Error = "could not parse form"
return
}
if form.Description == "" {
session.Error = "description cannot be empty"
return
}
ido, err := uuid.NewV4()
if err != nil {
session.Error = err.Error()
return
}
id := ido.String()
context := client.Request.Context()
if err = databases.Queries.AddTodoWithIdAndSessionId(context, schema.AddTodoWithIdAndSessionIdParams{
ID: id,
SessionID: session.ID,
Description: form.Description,
}); err != nil {
session.Error = err.Error()
return
}
}