File Upload

Two tiers from the same handler. Tier 1 is a plain multipart/form-data form — it just works, no JavaScript. Tier 2 adds lvt-upload to stream the file in chunks over the WebSocket and render a live <progress> bar as it arrives.

File Upload

Tier 1: Standard HTML

Tier 2: Chunked with Progress

Template

{{range .lvt.Uploads "chunked-doc"}} exposes per-file progress for the chunked input.

{{define "content"}}
<article>
    <h3>File Upload</h3>

    <h4>Tier 1: Standard HTML</h4>
    <form method="POST" enctype="multipart/form-data" lvt-form:preserve>
        <input type="file" name="document">
        <button type="submit" name="upload">Upload</button>
    </form>

    <h4>Tier 2: Chunked with Progress</h4>
    <form method="POST" lvt-form:preserve>
        <input type="file" lvt-upload="chunked-doc" name="chunked-doc">
        {{range .lvt.Uploads "chunked-doc"}}
        <div>
            <small><strong>{{.ClientName}}</strong> — {{.Progress}}%</small>
            <progress value="{{.Progress}}" max="100"></progress>
        </div>
        {{end}}
        <button type="submit" name="upload">Upload</button>
    </form>

    {{.lvt.FlashTag "success"}}
    {{.lvt.FlashTag "error"}}
</article>
{{end}}

file-upload.tmpl

Handler & state

WithUpload declares each named upload (size caps, and a small ChunkSize so the demo's progress is visible); Upload flashes the completed file's name.

type FileUploadController struct{}

func (c *FileUploadController) Upload(state FileUploadState, ctx *livetemplate.Context) (FileUploadState, error) {
	for _, name := range []string{"document", "chunked-doc"} {
		if ctx.HasUploads(name) {
			entries := ctx.GetCompletedUploads(name)
			if len(entries) > 0 {
				ctx.SetFlash("success", "Uploaded: "+entries[0].ClientName, livetemplate.FlashExpiry(flashSuccessExpiry))
				nudgeFlashExpiry(ctx, flashSuccessExpiry)
				return state, nil
			}
		}
	}
	ctx.SetFlash("error", "No file selected")
	return state, nil
}

func (c *FileUploadController) Refresh(state FileUploadState, ctx *livetemplate.Context) (FileUploadState, error) {
	return state, nil
}

func fileUploadHandler() http.Handler {
	tmpl := newLayoutTmplWithOpts(
		[]string{"templates/layout.tmpl", "templates/forms/file-upload.tmpl"},
		livetemplate.WithUpload("document", livetemplate.UploadConfig{
			MaxFileSize: 10 << 20, // 10 MB
			MaxEntries:  1,
		}),
		livetemplate.WithUpload("chunked-doc", livetemplate.UploadConfig{
			MaxFileSize: 10 << 20, // 10 MB
			MaxEntries:  1,
			ChunkSize:   1024, // 1KB chunks — small so progress is visible for demo files
		}),
	)
	return tmpl.Handle(&FileUploadController{}, livetemplate.AsState(&FileUploadState{
		Title:    "File Upload",
		Category: "Forms & Editing",
	}))
}

handlers_forms.go:182-221

type FileUploadState struct {
	Title    string
	Category string
}

state_forms.go:61-65

When to use

source: livetemplate/docs · path: examples/patterns/templates/forms/file-upload.tmpl