Build hotwire applications using Go

Front end development has changed a lot in last decade. Decade started with focus on Client Side Rendering(CSR) popularized by Angular.js.

angularJS revolutioned front-end, Browsers were not only Document viewers anymore. You could make "web apps" which were lot less dependent on Server. Then focus moved to React and till date is most popular way to write front end apps.

CSR come with own set of problems, Search engines not renedring CSR apps properly and increasing amound of client Javascript which slows down page load. In last few years SSR(using Next and Nuxt) and JamStack approaches are getting famous which tackles CSR issues.

Basecamp team, the same one behind hey.com came with new approach, Hotwire.

Hotwire is an alternative approach to building modern web applications without using much JavaScript by sending HTML instead of JSON over the wire.

This approach involves rendering templates on server and sending only the partial HTML to update the same page without full reload. They have also launched set of libraries to make hotwired apps.

  • Turbo: A set of complementary techniques for speeding up page changes and form submissions, dividing complex pages into components, and stream partial page updates over WebSocket.
  • Stimulus: While Turbo usually takes care of at least 80% of the interactivity that traditionally would have required JavaScript, there are still cases where a dash of custom code is required.
  • Strada: Standardizes the way that web and native parts of a mobile hybrid application talk to each other via HTML bridge attributes.

Turbo has a official rails implementaion. I have created a utility library (turbo-go)[https://github.com/akmittal/turbo-go] to make hotwired apps.

Lets make a simple a messaging app.

1. Integrate turbo client module #

Hotwire client library is published as ES6 module and can be loaded easily from CDN. Create a base template base.temp.html which import turbo.

{{define "base"}}
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>App</title>
  </head>

  <body>
   <main>
{{template "main" .}}
   </main>
  </body>

  <script type="module">
    import * as hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo';
  </script>
</html>

{{end}}

2. Render template #

Create main.go which render our base template, In base template we have used main block which is not defined. So create one more template main.temp.go which defines that and render messages.

{{template "base" .}}
{{define "main"}}
  <div id="messages">
    {{range .}}
      {{template "message" .}}
      <form method="POST" action="/send">
        <input type="text" name="message" placeholder="New Message">
        <input type="submit">
      </form>
    {{end}}
  </div>
{{end}}
{{define "message"}}
  <div>{{.Text}}</div>
{{end}}
package main

import (
	"fmt"
	"html/template"
	"net/http"

	"github.com/go-chi/chi"
)
type Message struct{
  Text string
}

var messages []Message

func main() {

	mux := chi.NewMux()
	mux.Get("/", getIndex)
	http.ListenAndServe(":8000", mux)
}
func getIndex(rw http.ResponseWriter, req *http.Request) {
	temp, err := template.ParseFiles("templates/base.temp.html", "templates/main.temp.html")
	if err != nil {
		http.Error(rw, err.Error(), 500)
	}
	temp.Execute(rw, messages)
}

3. Update view on new message #

Now we want to add a form to send messages in main.temp.html and Add pass a websocket connection to turbo. Any HTML received on websocket will be tracker by Turbo client and current view will be updated.

 <script type="module">
    import * as hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo';
    const es = new WebSocket('ws://localhost:8000/socket');
    hotwiredTurbo.connectStreamSource(es);
  </script>

in main.go file we will create a hub using github.com/akmittal/turbo-go/pkg/turbo package. Hub will keep track of all the clients.

Create one channel to push the messages when they are received from form.

package main

import (
	"fmt"
	"html/template"
	"net/http"

	"github.com/akmittal/turbo-go/examples/hotwire/message"
	"github.com/akmittal/turbo-go/pkg/turbo"
	"github.com/go-chi/chi"
)
type Message struct{
  Text string
}

var messages []Message

func main() {
	mux := chi.NewMux()
	mux.Get("/", getIndex)
	hub := turbo.NewHub()
	msgChan := make(chan interface{})
	mux.Post("/send", func(rw http.ResponseWriter, req *http.Request) {  // Post message on endpoint
		sendMessage(msgChan, hub, rw, req)
	})
	go hub.Run()
	mux.Get("/socket", func(rw http.ResponseWriter, req *http.Request) {
		getSocket(msgChan, hub, rw, req)
	})
	http.ListenAndServe(":8000", mux)
}
func getIndex(rw http.ResponseWriter, req *http.Request) {
	temp, err := template.ParseFiles("templates/main.temp.html", "templates/base.temp.html")
	if err != nil {
		http.Error(rw, err.Error(), 500)
	}
	temp.Execute(rw, messages)
}
// Parse form data and push to channel
func sendMessage(msgChan chan interface{}, hub *turbo.Hub, rw http.ResponseWriter, req *http.Request) {
	if err := req.ParseForm(); err != nil {
		http.Error(rw, err.Error(), 401)
		return
	}
	var msg Message
	msg.Text = req.FormValue("message")
	messages = append(messages, msg)
	go func() {
		msgChan <- msg
	}()
	fmt.Fprintf(rw, "%s", "Received")

}

func getSocket(msgChan chan interface{}, hub *turbo.Hub, rw http.ResponseWriter, req *http.Request) {
	temp, _ := template.ParseFiles("examples/hotwire/templates/messages.temp.html")
	messageTemp := temp.Lookup("message")
// Craete a Turbo stream which will keep on pushing new Messages to all clients connected to HUB
	appendMessage := turbo.Stream{
		Action:   turbo.APPEND,
		Template: messageTemp,
		Target:   "messages",
		Data:     msgChan,
	}

	appendMessage.Stream(hub, rw, req)

}

Working example can be found at https://github.com/akmittal/turbo-go/tree/main/examples/hotwire


Since you've made it this far, sharing this article on your favorite social media network would be highly appreciated ! For feedback, please ping me on Twitter.

Published