Skip to content

Todos Example

The starter template comes with a todos application.

You can find this example at https://github.com/razshare/frizzante-starter.

alt text

Although the application itself is simple, there are many things that might need some explanation.

First of all every interaction happens through a GET verb.

main.go
func main() {
frz.NewServer().
WithEfs(efs).
AddRoute(frz.Route{Pattern: "GET /", Handler: handlers.Default}).
AddRoute(frz.Route{Pattern: "GET /welcome", Handler: handlers.Welcome}).
AddRoute(frz.Route{Pattern: "GET /todos", Handler: handlers.Todos}).
AddRoute(frz.Route{Pattern: "GET /check", Handler: handlers.Check}).
AddRoute(frz.Route{Pattern: "GET /uncheck", Handler: handlers.Uncheck}).
AddRoute(frz.Route{Pattern: "GET /add", Handler: handlers.Add}).
AddRoute(frz.Route{Pattern: "GET /remove", Handler: handlers.Remove}).
Start()
}

As you can see, all handlers are exposed with a GET /... pattern.

The default handler is exposed with a GET / pattern.

This is important because this pattern will capture any request that is not captured by the other patterns.

This means that the default handler not only handles the default application view, but it also handles all file requests.

It is for that reason that this handler tries to send a matching file with SendFileOrElse() before doing anything else.

lib/handlers/default.go
func Default(c *frz.Connection) {
c.SendFileOrElse(func() { Welcome(c) })
}

SendFileOrElse will try find the file on the host file system first, if the file is not found, it will try to find it in the binary embedded file system, otherwise it falls back to the Welcome handler.

All this handler does is send the "Welcome" view to the user using SendView().

lib/handlers/welcome.go
func Welcome(c *frz.Connection) {
c.SendView(frz.View{Name: "Welcome"})
}

This "Welcome" view is exported by the application for both the client and the server.

app/lib/exports/server.ts
import Welcome from "$lib/views/Welcome.svelte"
import Todos from "$lib/views/Todos.svelte"
export const views = {
"Welcome": Welcome, // Exporting "Welcome".
"Todos": Todos,
}
app/lib/exports/client.ts
export const views = {
"Welcome": import("$lib/views/Welcome.svelte"), // Exporting "Welcome".
"Todos": import("$lib/views/Todos.svelte"),
}

These are key/value records.

The keys are important, because they dictate the Name of the view, the actual property Name of the view.

frz.View{
Name: "Welcome", // This.
}

The "Welcome" view is simply laying down a header message and a link pointing to "/todos".

app/lib/views/Welcome.svelte
<script lang="ts">
import Layout from "$lib/components/Layout.svelte"
import { href } from "$lib/utilities/frz/scripts/href.ts"
</script>
<Layout title="Welcome">
<h1>Welcome to Frizzante.</h1>
<a class="link" {...href("/todos")}> Show todos </a>
</Layout>

This hyperlink is a bit special, because it doesn’t just specify an href attribute as you would normally expect.

Instead it uses the href() function to set said attribute.

This is because in reality href() also returns a onclick handler.

//$lib/utilities/frz/href.ts
export function href(path = ""): {
href: string
onclick: (e: MouseEvent) => void
}

When the view is allowed to execute JavaScript inside the client’s browser, the hyperlink will swap the view dynamically instead of navigating away to the given path.

However, if the view is not allowed to execute JavaScript code, then the hyperlink will fallback to using web standards, and it will navigate away to the given path when clicked.

It sends the "Todos" view to the user, along with a list of todos, which is retrieved from the session state.

lib/handlers/todos.go
func Todos(c *frz.Connection) {
state, _ := frz.Session(c, lib.NewState())
c.SendView(frz.View{Name: "Todos", Data: map[string]any{
"todos": state.Todos,
}})
}

As mentioned in the sessions section, the default session operator handles things in a local sessions directory.

Initially, the session state has a few items in it, which are initialized by NewState().

lib/state.go
func NewState() State {
return State{
Todos: []Todo{
{Checked: false, Description: "Pet the cat."},
{Checked: false, Description: "Do laundry"},
{Checked: false, Description: "Pet the cat."},
{Checked: false, Description: "Cook"},
{Checked: false, Description: "Pet the cat."},
},
}
}

This state is immediately overwritten by the session operator if an existing session is found for the user.

If the session is a fresh one instead, the initial state remains untouched, thus the final state variable will always contain the 5 todo items when initialized.

This "Todos" view does quite a few things.

  1. Lists Items
  2. Removes items.
  3. Checks items.
  4. Unchecks items.
  5. Adds items.
app/lib/views/Todos.svelte
<script lang="ts">
import Layout from "$lib/components/Layout.svelte"
import { action } from "$lib/utilities/frz/scripts/action.ts"
import { href } from "$lib/utilities/frz/scripts/href.ts"
type Todo = {
Checked: boolean
Description: string
}
type Props = {
todos: Todo[],
error: string,
}
let {todos, error}:Props = $props()
</script>
<Layout title="Todos">
<ol>
{#each todos as todo, index (index)}
<li>
<form {...action("/remove")}>
<input type="hidden" name="index" value={index} />
<button class="link">[Remove]</button>
</form>
{#if todo.Checked}
<form {...action("/uncheck")}>
<input type="hidden" name="index" value={index} />
<button class="link">
<!---->
(x) {todo.Description}
<!---->
</button>
</form>
{:else}
<form {...action("/check")}>
<input type="hidden" name="index" value={index} />
<button class="link">
<!---->
(&nbsp;&nbsp;) {todo.Description}
<!---->
</button>
</form>
{/if}
</li>
{/each}
</ol>
<form {...action("/add")}>
<span class="link">Description</span>
<input type="text" value="" name="description" />
<button class="link" type="submit">Add +</button>
</form>
{#if error}
<br />
<span class="error">{error}</span>
{/if}
<br />
<a class="link" {...href("/")}>&lt; Back</a>
</Layout>

The component itself receives the todo list as a todos property from the server

app/lib/views/Todos.svelte
let {todos, error}:Props = $props()

which is then iterated upon to render the items

app/lib/views/Todos.svelte
{#each todos as todo, index (index)}
<li>
<form {...action("/remove")}>
<input type="hidden" name="index" value={index} />
<button class="link">[Remove]</button>
</form>
{#if todo.Checked}
<form {...action("/uncheck")}>
<input type="hidden" name="index" value={index} />
<button class="link">
<!---->
(x) {todo.Description}
<!---->
</button>
</form>
{:else}
<form {...action("/check")}>
<input type="hidden" name="index" value={index} />
<button class="link">
<!---->
(&nbsp;&nbsp;) {todo.Description}
<!---->
</button>
</form>
{/if}
</li>
{/each}

Each item has two buttons, a remove button and a toggle button.

Removing an item from the list involves submitting a form to /remove, along with the index of the item, which is hidden.

app/lib/views/Todos.svelte
<form {...action("/remove")}>
<input type="hidden" name="index" value={index} />
<button class="link">[Remove]</button>
</form>

This action is then captured by the Remove handler, which does some basic validation and error handling to then finally remove the item from the session.

lib/handlers/remove.go
func Remove(c *frz.Connection) {
state, operator := frz.Session(c, lib.NewState())
defer operator.Save(state)
if 0 == len(state.Todos) {
// No items found, ignore the request.
return
}
index := c.ReceiveQuery("index")
if "" == index {
// No index found, ignore the request.
return
}
id, intError := strconv.ParseInt(index, 10, 64)
if nil != intError {
c.SendView(frz.View{Name: "Todos", Data: map[string]any{
"error": intError.Error(),
}})
return
}
// Removes item from session.
state.Todos = append(state.Todos[:id], state.Todos[id+1:]...)
// This will update the client user interface.
c.SendNavigate("/todos")
}

Checking and unchecking items is also done using forms.

app/lib/views/Todos.svelte
{#if todo.Checked}
<form {...action("/uncheck")}>
<input type="hidden" name="index" value={index} />
<button class="link">
<!---->
(x) {todo.Description}
<!---->
</button>
</form>
{:else}
<form {...action("/check")}>
<input type="hidden" name="index" value={index} />
<button class="link">
<!---->
(&nbsp;&nbsp;) {todo.Description}
<!---->
</button>
</form>
{/if}

Checking an item sends a form to /check and unchecking it sends the form to /uncheck.

Both forms indicate the index of the item using a hidden input field.

lib/views/Todos.svelte
<input type="hidden" name="index" value={index} />

Checking is handled by the Check handler.

lib/handlers/check.go
func Check(c *frz.Connection) {
state, operator := frz.Session(c, lib.NewState())
defer operator.Save(state)
index := c.ReceiveQuery("index")
if "" == index {
// No index found, ignore the request.
return
}
id, intError := strconv.ParseInt(index, 10, 64)
if nil != intError {
c.SendView(frz.View{Name: "Todos", Data: map[string]any{
"error": intError.Error(),
}})
return
}
// Checks the item.
state.Todos[id].Checked = true
// This will update the client user interface.
c.SendNavigate("/todos")
}

While unchecking is handled by the Uncheck handler, which does the exact same thing as Check, except it sets .Checked to false instead of true.

lib/handlers/uncheck.go
// ...
state.Todos[id].Checked = false
// ...

The final piece of the puzzle is adding items to the list, which is done by sending a form to /add.

lib/views/Todos.svelte
<form {...action("/add")}>
<span class="link">Description</span>
<input type="text" value="" name="description" />
<button class="link" type="submit">Add +</button>
</form>

This form is then captured by the Add handler.

lib/handlers/add.go
func Add(c *frz.Connection) {
state, operator := frz.Session(c, lib.NewState())
defer operator.Save(state)
description := c.ReceiveQuery("description")
if "" == description {
c.SendView(frz.View{Name: "Todos", Data: map[string]any{
"todos": state.Todos,
"error": "todo description cannot be empty",
}})
return
}
// Adds the item to the session.
state.Todos = append(
state.Todos,
lib.Todo{
Checked: false,
Description: description,
},
)
// This will update the client user interface.
c.SendNavigate("/todos")
}
DescriptionHyperlink
A live chat applicationhttps://github.com/razshare/frizzante-example-chat
A blog application with login and registration formshttps://github.com/razshare/frizzante-example-blog