Skip to content

Commit

Permalink
Merge pull request #4 from syarul/rc
Browse files Browse the repository at this point in the history
added unit cypress unit test
  • Loading branch information
syarul authored Jan 7, 2024
2 parents 6641b78 + 26770a6 commit 87806bd
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 221 deletions.
28 changes: 25 additions & 3 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Go
name: Go Build and Cypress Tests

on:
push:
Expand Down Expand Up @@ -29,5 +29,27 @@ jobs:
go install github.com/a-h/templ/cmd/templ@latest
templ generate
- name: Build
run: go build .
- name: Build & Run
run: |
go build .
go run . &
- uses: actions/setup-node@v3
with:
node-version: 18

- name: Clone cypress-example-todomvc
run: git clone https://github.com/cypress-io/cypress-example-todomvc.git cypress-example-todomvc

- name: Install Dependencies
run: |
cd cypress-example-todomvc
npm install
- name: Run Cypress Tests
run: |
cd cypress-example-todomvc
npm run cypress:run
- name: Stop Go Application
run: pkill -f "go run ."
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
todomvc_templ.go
tpl/*.go
go-templ-htmx-_hyperscript.exe
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
===========================================================
Build with GO, TEMPL, HTMX & _HYPERSCRIPT
[![Go](https://github.com/syarul/todomvc-go-templ-htmx-_hyperscript/actions/workflows/go.yml/badge.svg)](https://github.com/syarul/todomvc-go-templ-htmx-_hyperscript/actions/workflows/go.yml)

### Testing
As evidence of HTMX's capabilities in emulating the functionalities of modern frameworks, I have incorporated [unit test](https://github.com/syarul/todomvc-go-templ-htmx-_hyperscript/actions/runs/7412273948/job/20168687544) from https://github.com/cypress-io/cypress-example-todomvc. This demonstration serves to showcase that HTMX, when paired with _hyperscript, can replicate all the behaviors typically associated with most modern client frameworks.

### Security
Check on [this link](https://templ.guide/security/) when using `templ` as HTML template engine. At anytime as developer `Do not blame the farmer if you cook the rice til it burns.`

### Usage
- install go if you don't have
- run `go mod tidy` to fetch all needed modules
Expand All @@ -19,4 +26,8 @@
- alternatively you can compile into executable with `go build .`

### HTMX
Visit [https://github.com/rajasegar/awesome-htmx](https://github.com/rajasegar/awesome-htmx) to look for HTMX curated infos
Visit [https://github.com/rajasegar/awesome-htmx](https://github.com/rajasegar/awesome-htmx) to look for HTMX curated infos

###
Todo
- Use behavior to modular the _hyperscript scripts
5 changes: 1 addition & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,4 @@ module todomvc/go-templ-htmx-_hyperscript

go 1.21.5

require (
github.com/a-h/templ v0.2.501
github.com/google/uuid v1.5.0
)
require github.com/a-h/templ v0.2.501
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,3 @@ github.com/a-h/templ v0.2.501 h1:9rIo5u+B+NDJIkbHGthckUGRguCuWKY/7ri8e2ckn9M=
github.com/a-h/templ v0.2.501/go.mod h1:9gZxTLtRzM3gQxO8jr09Na0v8/jfliS97S9W5SScanM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
150 changes: 99 additions & 51 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
package main

import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"time"
"todomvc/go-templ-htmx-_hyperscript/tpl"

"github.com/a-h/templ"
)

var idCounter uint64

type Todo struct {
id uint64
Id uint64 `json:"id"`
title string
done bool
Done bool `json:"done"`
editing bool
}

Expand Down Expand Up @@ -50,7 +54,7 @@ func (t *todos) crudOps(action Action, todo Todo) Todo {
index := -1
if action != Create {
for i, r := range *t {
if r.id == todo.id {
if r.Id == todo.Id {
index = i
break
}
Expand All @@ -61,7 +65,7 @@ func (t *todos) crudOps(action Action, todo Todo) Todo {
*t = append(*t, todo)
return todo
case Toggle:
(*t)[index].done = todo.done
(*t)[index].Done = todo.Done
case Update:
title := strings.Trim(todo.title, " ")
if len(title) != 0 {
Expand All @@ -88,12 +92,14 @@ func main() {
t := &todos{}

// Register the routes.
http.Handle("/get-hash", http.HandlerFunc(t.getHash))
// http.Handle("/get-hash", http.HandlerFunc(t.getHash))
http.Handle("/set-hash", http.HandlerFunc(setHash))
http.Handle("/learn.json", http.HandlerFunc(learnHandler))

http.Handle("/update-counts", http.HandlerFunc(t.updateCounts))
http.Handle("/toggle-all", http.HandlerFunc(t.toggleAllHandler))
http.Handle("/completed", http.HandlerFunc(t.clearCompleted))
http.Handle("/footer", http.HandlerFunc(t.footerHandler))

http.Handle("/", http.HandlerFunc(t.pageHandler))

Expand All @@ -105,15 +111,18 @@ func main() {

http.Handle("/toggle-main", http.HandlerFunc(t.toggleMainHandler))
http.Handle("/toggle-footer", http.HandlerFunc(t.toggleFooterHandler))
http.Handle("/todo-list", http.HandlerFunc(t.todoListHandler))
http.Handle("/todo-json", http.HandlerFunc(t.getJSON))
http.Handle("/todo-item", http.HandlerFunc(t.todoItemHandler))

// Specify the directory containing your static files
dir := "./assets"
// this is used to serve axe-core for the todomvc test
dir := "./cypress-example-todomvc/node_modules"

// Use the http.FileServer to create a handler for serving static files
fs := http.FileServer(http.Dir(dir))

// Use the http.Handle to register the file server handler for a specific route
http.Handle("/assets/", http.StripPrefix("/assets/", fs))
http.Handle("/node_modules/", http.StripPrefix("/node_modules/", fs))

// start the server.
addr := os.Getenv("LISTEN_ADDRESS")
Expand All @@ -133,7 +142,7 @@ func main() {
func countNotDone(todos []Todo) int {
count := 0
for _, todo := range todos {
if !todo.done {
if !todo.Done {
count++
}
}
Expand All @@ -155,7 +164,7 @@ func defChecked(todos []Todo) bool {
// has completeTask checks if there is any completed task in the Todos slice
func hasCompleteTask(todos []Todo) bool {
for _, todo := range todos {
if todo.done {
if todo.Done {
return true
}
}
Expand Down Expand Up @@ -183,29 +192,30 @@ func learnHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(emptyJSON)
}

// getHash handles the GET request for the #/:name route.
// it updates the selected field of each filter based on the name query parameter.
// on initial fetch when todos is empty it will send "empty string" which usually
// ignored by htmx, the reason here we want to make use templ rendering to behave
// efficiently by rendering only needed html, this use case usually available in
// modern client framework i.e., in React you can do
// <>{todo.length && <TodoList />}</>
// so you can do the same in HTMX too!
func (t *todos) getHash(w http.ResponseWriter, r *http.Request) {
if len(*t) == 0 {
byteRenderer(w, r, "")
return
// this for acquiring todos as json where client can fetch todo to render when route change
// its pretty much as the same how react do DOM diffing instead we do it on server and send
// the needed rendered HTML as client check which one is missing
func (t *todos) getJSON(w http.ResponseWriter, r *http.Request) {
// set the Content-Type header to indicate JSON
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(*t)
}

func selectedFilter(filters []Filter) string {
for _, filter := range filters {
if filter.selected {
return filter.name
}
}
return "All"
}

func setHash(w http.ResponseWriter, r *http.Request) {

hash := r.FormValue("hash")
name := r.FormValue("name")

if len(name) == 0 {
if len(hash) != 0 {
name = hash
} else {
name = "All"
}
name = "All"
}
// loop through filters and update the selected field
for i := range filters {
Expand All @@ -216,15 +226,51 @@ func (t *todos) getHash(w http.ResponseWriter, r *http.Request) {
}
}

// render the filter component with the updated filters
templRenderer(w, r, filter(filters))
byteRenderer(w, r, "")
}

func generateRandomString(length int) (string, error) {
bytes := make([]byte, length)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes), nil
}

func (t *todos) footerHandler(w http.ResponseWriter, r *http.Request) {
templRenderer(w, r, footer(*t, filters, hasCompleteTask(*t)))
}

func (t *todos) pageHandler(w http.ResponseWriter, r *http.Request) {
// start with new todo data when refresh
// *t = make([]Todo, 0)
// idCounter = 0
templRenderer(w, r, Page(*t, filters, defChecked(*t), hasCompleteTask(*t)))
_, err := r.Cookie("sessionId")

if err == http.ErrNoCookie {
// fmt.Println("Error:", err)
newCookieValue, err := generateRandomString(32)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}

newCookie := http.Cookie{
Name: "sessionId",
Value: newCookieValue,
Expires: time.Now().Add(time.Second * 6000),
HttpOnly: true,
}
http.SetCookie(w, &newCookie)

// start with new todo data when session is reset
*t = make([]Todo, 0)
idCounter = 0
}

templRenderer(w, r, Page(*t, filters, defChecked(*t), hasCompleteTask(*t), selectedFilter(filters)))
}

func (t *todos) todoListHandler(w http.ResponseWriter, r *http.Request) {
templRenderer(w, r, todoList(*t, selectedFilter(filters)))
}

// toggle section main
Expand All @@ -251,18 +297,31 @@ func (t *todos) addTodoHandler(w http.ResponseWriter, r *http.Request) {
todo := t.crudOps(Create, Todo{id, title, false, false})

if len(*t) == 1 {
templRenderer(w, r, todoList(*t))
templRenderer(w, r, todoList(*t, selectedFilter(filters)))
} else {
templRenderer(w, r, todoItem(todo))
templRenderer(w, r, todoItem(todo, selectedFilter(filters)))
}

}

func (t *todos) todoItemHandler(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseUint(r.FormValue("id"), 0, 32)

if err != nil {
fmt.Println("Error:", err)
return
}

todo := t.crudOps(Edit, Todo{id, "", false, false})

templRenderer(w, r, todoItem(todo, selectedFilter(filters)))
}

func (t *todos) clearCompleted(w http.ResponseWriter, r *http.Request) {
// determine render "none" or "block" based on incomplete tasks
hasCompleted := hasCompleteTask(*t)
if hasCompleted {
templRenderer(w, r, clearCompleted(hasCompleteTask(*t)))
templRenderer(w, r, tpl.ClearCompleted(hasCompleteTask(*t)))
} else {
byteRenderer(w, r, "")
}
Expand Down Expand Up @@ -302,7 +361,7 @@ func (t *todos) toggleTodo(w http.ResponseWriter, r *http.Request) {

todo := t.crudOps(Toggle, Todo{id, "", !done, false})

templRenderer(w, r, todoItem(todo))
templRenderer(w, r, todoItem(todo, selectedFilter(filters)))
}

func (t *todos) editTodoHandler(w http.ResponseWriter, r *http.Request) {
Expand All @@ -320,7 +379,7 @@ func (t *todos) editTodoHandler(w http.ResponseWriter, r *http.Request) {
// since editing only client side changes
todo := t.crudOps(Edit, Todo{id, "", false, false})

templRenderer(w, r, editTodo(Todo{id, todo.title, todo.done, true}))
templRenderer(w, r, editTodo(Todo{id, todo.title, todo.Done, true}))
}

func (t *todos) updateTodo(w http.ResponseWriter, r *http.Request) {
Expand All @@ -331,25 +390,14 @@ func (t *todos) updateTodo(w http.ResponseWriter, r *http.Request) {
return
}

// esc, err := strconv.ParseBool(r.FormValue("esc"))
// if err != nil {
// fmt.Println("Error:", err)
// return
// }

title := r.FormValue("title")

// esc will not do update instead will send the original unmodified title
// if esc {
// todo := t.crudOps(Edit, Todo{id, "", false, false})
// templRenderer(w, r, todoItem(todo))
// } else {
todo := t.crudOps(Update, Todo{id, title, false, false})
if len(todo.title) == 0 {
byteRenderer(w, r, "")
return
}
templRenderer(w, r, todoItem(todo))
templRenderer(w, r, todoItem(todo, selectedFilter(filters)))
// }
}

Expand Down
Loading

0 comments on commit 87806bd

Please sign in to comment.