Table of Contents

02 – Web Template

Template: Render HTML Template

Terdapat banyak jenis template pada Go, yang akan kita pakai kali ini adalah template HTML. Package html/template berisi banyak sekali fungsi untuk kebutuhan rendering dan parsing file template jenis ini.

Struktur Direktori

my-app
 – assets
 – –  default.css
 – go.mod
 – main.go

Menyiapkan Backend

Hal pertama yang perlu dilakukan adalah mempersiapkan back end. Buka file main.go, import package net/httphtml/template, dan path. Siapkan juga rute /.

				
					package main

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

func main() {
	http.HandleFunc("/", handlerIndex)
	http.HandleFunc("/index", handlerIndex)
	http.HandleFunc("/hello", handlerHello)
	http.HandleFunc("/direct", func(w http.ResponseWriter, r *http.Request) {
		var message = "Langsung Jaya"
		w.Write([]byte(message))
	})
	http.Handle("/static/",
		http.StripPrefix("/static/",
			http.FileServer(http.Dir("assets"))))

	var address = "localhost:9000"
	fmt.Printf("server started at %s\n", address)
	err := http.ListenAndServe(address, nil)
	if err != nil {
		fmt.Println(err.Error())
	}
}

func handlerIndex(w http.ResponseWriter, r *http.Request) {
	var filepath = path.Join("views", "index.html")
	var tmpl, err = template.ParseFiles(filepath)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	var data = map[string]interface{}{
		"title": "Learning Golang Web",
		"name":  "Batman",
	}

	err = tmpl.Execute(w, data)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func handlerHello(w http.ResponseWriter, r *http.Request) {
	var message = "Hello world!"
	w.Write([]byte(message))
}

				
			
Fontend

OK, back end sudah siap, selanjutnya kita masuk ke bagian user interface. Pada file views/index.html, tuliskan kode html sederhana berikut.

				
					<!DOCTYPE html>
<html>
    <head>
        <title>{{.title}}</title>
    </head>
    <body>
        <p>Welcome {{.name}}</p>
    </body>
</html>
				
			

Untuk menampilkan variabel yang disisipkan ke dalam template, gunakan notasi {{.namaVariabel}}. Pada contoh di atas, data title dan name yang dikirim dari back end ditampilkan.

Tanda titik “.” pada {{.namaVariabel}} menerangkan bahwa variabel tersebut diakses dari current scope. Dan current scope default adalah data map atau objek yang dilempar back end.

Testing

Semua sudah siap, maka jalankan program lalu lakukan testing via browser.

Menambahkan Style CSS

Ingat sebelumnya kita sudah menambahkan assets di route static assets. Kita akan coba tambahkan sebuah stylesheet di sini. Langsung saja, buat file statis assets/default.css, isi dengan kode berikut.

				
					body {
    font-family: "Helvetica Neue";
    font-weight: bold;
    font-size: 24px;
    color: #07c;
}
				
			

Pada views/index.html, include-kan file css.

				
					<!DOCTYPE html>
<html>
    <head>
        <title>{{.title}}</title>
        <link rel="stylesheet" href="/static/default.css" />
    </head>
    <body>
        <p>Welcome {{.name}}</p>
    </body>
</html>
				
			

Jalankan lalu lihat hasilnya

Template: Render Partial HTML Template

Satu buah halaman yang berisikan html, bisa terbentuk dari banyak template html (parsial). Pada chapter ini kita akan belajar bagaimana membuat, mem-parsing, dan me-render semua file tersebut.

Ada beberapa metode yang bisa digunakan, dari ke semuanya akan kita bahas 2 di antaranya, yaitu:

  • Menggunakan fungsi template.ParseGlob().
  • Menggunakan fungsi template.ParseFiles().
Struktur Aplikasi

Mari langsung kita praktekan. Buat project baru, siapkan file dan folder dengan susunan seperti dengan gambar berikut.

my-app
 – assets
 – –  default.css
 – go.mod
 – main.go
 – views
 – –  _header.html
 – –  _message.html
 – –  about.html
 – –  index.html

Back End

Buka main.go, isi dengan kode berikut.

				
					package main

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

type M map[string]interface{}

func main() {
	var tmpl, err = template.ParseGlob("views/*")
	if err != nil {
		panic(err.Error())
	}
	http.Handle("/static/",
		http.StripPrefix("/static/",
			http.FileServer(http.Dir("assets"))))
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		var data = M{"name": "Bagoes"}
		err = tmpl.ExecuteTemplate(w, "index", data)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	})

	http.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
		var data = M{"name": "Bagoes"}
		err = tmpl.ExecuteTemplate(w, "about", data)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	})

	fmt.Println("server started at localhost:3000")
	http.ListenAndServe("localhost:9000", nil)
}

				
			

Penjelasan :

  1. Tipe M merupakan alias dari map[string]interface{}, disiapkan untuk mempersingkat penulisan tipe map tersebut. Pada pembahasan-pembahasan selanjutnya kita akan banyak menggunakan tipe ini. Penlasan interface kosong baca disini.
  2. Pada kode di atas, di dalam fungsi main(), fungsi template.ParseGlob() dipanggil, dengan parameter adalah pattern path "views/*". Fungsi ini digunakan untuk memparsing semua file yang match dengan pattern yang ditentukan, dan fungsi ini mengembalikan 2 objek: *template.Template & error
    • Pattern path pada fungsi template.ParseGlob() nantinya akan di proses oleh filepath.Glob()
    • Proses parsing semua file html dalam folder views dilakukan di awal, agar ketika mengakses rute-tertentu-yang-menampilkan-html, tidak terjadi proses parsing lagi.
    • Parsing semua file menggunakan template.ParseGlob() yang dilakukan di luar handler, tidak direkomendasikan dalam fase development. Karena akan mempersulit testing html. Lebih detailnya akan dibahas di bagian bawah.
  3. Karena semua file html sudah diparsing di awal, maka untuk render template tertentu cukup dengan memanggil method ExecuteTemplate(), dengan menyisipkan 3 parameter berikut:
    • Parameter ke-1, objek http.ResponseWriter
    • Parameter ke-2, nama template
    • Parameter ke-3, data
Frontend

index.html

				
					{{define "index"}}
<!DOCTYPE html>
<html>
    <head>
        {{template "_header"}}
    </head>
    <body>
        {{template "_message"}}
        <p>Page: Index</p>
        <p>Welcome {{.name}}</p>
    </body>
</html>
{{end}}
				
			

Pada kode di atas terlihat bahwa ada beberapa kode yang ditulis dengan notasinya {{ }}. Berikut adalah penjelasannya.

  1. Statement {{define "index"}}, digunakan untuk mendefinisikan nama template. Semua blok kode setelah statement tersebut (batasnya adalah hingga statement {{end}}) adalah milik template dengan nama index. keyword define digunakan dalam penentuan nama template.
  2. Statement {{template "_header"}} artinya adalah template bernama _header di-include ke bagian itu. keyword template digunakan untuk include template lain.
  3. Statement {{template "_message"}}, sama seperti sebelumnya, template bernama _message akan di-include.
  4. Statement {{.name}} akan memunculkan data, name, yang data ini sudah disisipkan oleh back end pada saat rendering.
  5. Statement {{end}} adalah penanda batas akhir pendefinisian template.

 

about.html

				
					{{define "about"}}
<!DOCTYPE html>
<html>
    <head>
        {{template "_header"}}
    </head>
    <body>
        {{template "_message"}}
        <p>Page: About</p>
        <p>Welcome {{.name}}</p>
    </body>
</html>
{{end}}
				
			

_header.html

				
					{{define "_header"}}
<title>Learn Golang Template</title>
<link rel="stylesheet" href="/static/default.css" />
{{end}}
				
			

_message

				
					{{define "_message"}}
<p>Welcome</p>
{{end}}
				
			

Hasilnya

Parsing Banyak File HTML Menggunakan template.ParseFiles()

Metode parsing menggunakan template.ParseGlob() memiliki kekurangan yaitu sangat tergantung terhadap pattern path yang digunakan. Jika dalam suatu proyek terdapat sangat banyak file html dan folder, sedangkan hanya beberapa yang digunakan, pemilihan pattern path yang kurang tepat akan menjadikan file lain ikut ter-parsing dengan sia-sia.

Dan juga, karena statement template.ParseGlob() dieksekusi diluar handler, maka ketika ada perubahan pada salah satu view, lalu halaman di refresh, output yang dihasilkan akan tetap sama. Solusi dari masalah ini adalah dengan memanggil template.ParseGlob() di tiap handler rute-rute yang diregistrasikan.

Best practices yang bisa diterapkan, ketika environment adalah production, maka tempatkan template.ParseGlob() di luar (sebelum) handler. Sedangkan pada environment development, taruh template.ParseGlob() di dalam masing-masing handler. Gunakan seleksi kondisi untuk mengakomodir skenario ini.

Alternatif metode lain yang bisa digunakan, yang lebih efisien, adalah dengan memanfaatkan fungsi template.ParseFiles(). Fungsi ini selain bisa digunakan untuk parsing satu buah file saja (seperti yang sudah dicontohkan pada chapter sebelumnya), bisa digunakan untuk parsing banyak file.

Mari kita praktekan. Ubah handler rute /index dan /about. Gunakan template.ParseFiles() dengan isi parameter (variadic) adalah path dari file-file html yang akan dipergunakan di masing-masing rute. Lalu hapus statement template.ParseGlob()

				
					package main

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

type M map[string]interface{}

func main() {
	http.Handle("/static/",
		http.StripPrefix("/static/",
			http.FileServer(http.Dir("assets"))))

	http.HandleFunc("/", handlerIndex)

	http.HandleFunc("/about", handlerAbout)

	fmt.Println("server started at localhost:9000")
	http.ListenAndServe("localhost:9000", nil)
}

func handlerIndex(w http.ResponseWriter, r *http.Request) {
	var data = M{"name": "Batman"}
	var tmpl = template.Must(template.ParseFiles(
		"views/index.html",
		"views/_header.html",
		"views/_message.html",
	))

	var err = tmpl.ExecuteTemplate(w, "index", data)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}
func handlerAbout(w http.ResponseWriter, r *http.Request) {
	var data = M{"name": "Batman"}
	var tmpl = template.Must(template.ParseFiles(
		"views/about.html",
		"views/_header.html",
		"views/_message.html",
	))

	var err = tmpl.ExecuteTemplate(w, "about", data)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}