Replies: 5 comments
-
Interesting idea! I hadn't thought of that. This would be useful in the case that a template takes a long time to load, perhaps due to loading data from a slow database or API call. It is possible to achieve the same thing with HTMX though. HTMX has the concept of Here's how that looks in templ: package main
import "fmt"
import "net/url"
templ Lazy(get string) {
<div hx-trigger="load" hx-get={ get }>Loading...</div>
}
templ Header() {
<div>
Header
</div>
}
templ Footer() {
<div>
Footer
</div>
}
templ Body(text string) {
{ text }
}
templ Page(id string) {
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/[email protected]"></script>
</head>
<body>
@Header()
@Lazy(fmt.Sprintf("/body/%s", url.PathEscape(id)))
@Footer()
</body>
</html>
} The Go server looks like this: package main
import (
"fmt"
"net/http"
"time"
"github.com/a-h/templ"
"github.com/jba/muxpatterns"
)
func main() {
mux := muxpatterns.NewServeMux()
mux.HandleFunc("/body/{id}", func(w http.ResponseWriter, r *http.Request) {
// Simulate being slow!
time.Sleep(time.Second * 5)
// Render just the body content.
id := mux.PathValue(r, "id")
text := fmt.Sprintf("Body text for id: %q", id)
templ.Handler(Body(text)).ServeHTTP(w, r)
})
mux.HandleFunc("/{id}", func(w http.ResponseWriter, r *http.Request) {
id := mux.PathValue(r, "id")
templ.Handler(Page(id)).ServeHTTP(w, r)
})
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
templ.Handler(Page("Default content")).ServeHTTP(w, r)
})
http.ListenAndServe("localhost:9999", mux)
} There are some additional attributes in HTMX that can be used to extract specific parts of a full HTML page instead of having a specific endpoint for the body content. In that design, HTMX would issue one extra HTTP request from the client (plus the one to load the HTMX JS file), but it's also possible to use SSE to populate parts of the web page. In that case, on load, HTMX would initiate an SSE connection on load, then populate all of the data on the page as it becomes available. |
Beta Was this translation helpful? Give feedback.
-
Yeah, I am familiar with HTMX and I saw there was also discussion/issue about this, where you suggested hotwire turbo frames. But all of these require an extra request and that request happens after js is loaded. Maybe it's the out of the scope of this project, but I think it would be very useful to have this in the library. Maybe something like |
Beta Was this translation helpful? Give feedback.
-
I've been playing with this idea too. I came up with a solution that mixes using a Cat Facts
package main
import (
"context"
components "defer/components"
"encoding/json"
"io"
"net/http"
"time"
)
type CatFact struct {
Fact string `json:"fact"`
Length int `json:"length"`
}
type CatFactsApiResponse struct {
Data []CatFact `json:"data"`
}
func getFacts() ([]CatFact, error) {
// Simulate a slow API call
time.Sleep(2 * time.Second)
apiUrl := "https://catfact.ninja/facts?limit=10"
resp, err := http.Get(apiUrl)
defer resp.Body.Close()
if err != nil {
return nil, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var facts CatFactsApiResponse
err = json.Unmarshal(body, &facts)
if err != nil {
return nil, err
}
return facts.Data, nil
}
func factsHandler(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(
w,
"Server does not support Flusher interface",
http.StatusInternalServerError,
)
return
}
ctx := context.Background()
// Send the first batch of data to the browser
components.Index().Render(ctx, w)
flusher.Flush()
// Perform some slow operation
catFacts, err := getFacts()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var facts []string
for _, catFact := range catFacts {
facts = append(facts, catFact.Fact)
}
components.Facts(facts).Render(ctx, w)
// Send second batch of data
flusher.Flush()
}
func main() {
http.HandleFunc("/", factsHandler)
http.HandleFunc(
"/tailwind.css",
func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css")
http.ServeFile(w, r, "public/css/tailwind.css")
},
)
http.ListenAndServe(":3000", nil)
}
package components
import "strconv"
import "math/rand"
// This is the function that will be called each time a new Lazy Component is
// sent by the flusher
script lazyComponentLoader() {
window.loadLazyComponent = function loadLazyComponent(lazyId) {
const lazyComponent = document.querySelector(`div[data-lazy-id='${lazyId}']`);
const loadedComponent = document.querySelector(`div[data-lazy-id='${lazyId}'][data-lazy-status='loaded']`);
lazyComponent.parentElement.replaceChild(loadedComponent, lazyComponent);
}
}
templ layout() {
<!DOCTYPE html>
<html>
<head>
<title>Cat Facts</title>
<link href="/tailwind.css" rel="stylesheet"/>
</head>
<body>
<main>
{ children... }
</main>
@lazyComponentLoader()
</body>
</html>
}
// Here is the call to the loadLazyComponent function
script loadLazyComponent(lazyID string) {
window.loadLazyComponent(lazyID);
}
// lazyComponent receives a status. When the status is "loaded", it calls the
// loadLazyComponent function
templ lazyComponent(lazyID string, status string) {
<div
data-lazy-id={ lazyID }
data-lazy-status={ status }
>
{ children... }
if status == "loaded" {
@loadLazyComponent(lazyID)
}
</div>
}
css randomWidth() {
width: { strconv.Itoa(30 + rand.Intn(46)) + "%" };
}
templ Fact(fact string, factNum int) {
<li class="flex items-center gap-4">
<span class="fact-number font-semibold">{ strconv.Itoa(factNum) }{ "." }</span>
<span>
{ fact }
</span>
</li>
}
// Define a fallback component to be sent in the first batch, before the facts
// are loaded
templ FactFallback(factNum int) {
<li class="flex items-center gap-4">
<span class="fact-number font-semibold">{ strconv.Itoa(factNum) }{ "." }</span>
<span
class={ "animate-pulse bg-stone-200 h-4", randomWidth() }
></span>
</li>
}
// If facts is nil, it means that the component is still loading. This is just
// a convention I came up with
func getStatus(facts []string) string {
if facts == nil {
return "pending"
}
return "loaded"
}
templ Facts(facts []string) {
@lazyComponent("facts", getStatus(facts)) {
<ul id="facts-list" class="w-full flex flex-col gap-2">
if numFacts := len(facts); numFacts > 0 {
for idx, fact := range facts {
@Fact(fact, idx+1)
}
} else {
for idx := 0; idx < 10; idx++ {
@FactFallback(idx + 1)
}
}
</ul>
}
}
templ Index() {
@layout() {
<div class="p-8">
<h1 class="mb-4">Cat Facts</h1>
// Here in the Index component, we call the Facts component passing
// nil as the facts array so that it renders the fallback component
@Facts(nil)
</div>
}
} The result is this: Screen.Recording.2024-03-25.at.13.35.33.mov |
Beta Was this translation helpful? Give feedback.
-
We've made progress on this concept. See See #781 (reply in thread) and draft PR at #802 |
Beta Was this translation helpful? Give feedback.
-
Released in https://github.com/a-h/templ/releases/tag/v0.2.731 See examples at https://github.com/a-h/templ/tree/main/examples/streaming and https://github.com/a-h/templ/tree/main/examples/suspense |
Beta Was this translation helpful? Give feedback.
-
Hi, I recently discovered this project and was wondering if you considered adding something like Suspense?
What do you think? Maybe you can come up with better API design for this? Mine is pretty shitty and I don't even know how it would like with
templ
directivesBeta Was this translation helpful? Give feedback.
All reactions