Media Library Step 5: Coding the Frontend (Image Upload)
Go ahead and open up the three main files in your editor:
- image-upload.ts
- image-upload.html
- image-upload.css
Note: If you come from the older Angular days, notice that they've dropped the '.component' from the filename!
The HTML template
Because this is a form, let's build the HTML first. Here's the HTML you'll want to paste into image-upload.html:
<div class="upload-container">
<h2>Upload an Image</h2>
<form (ngSubmit)="onUpload()" #uploadForm="ngForm" class="upload-form">
<!-- File Input -->
<div class="form-group">
<label for="imageFile">Choose Image*</label>
<input
type="file"
id="imageFile"
(change)="onFileSelected($event)"
accept="image/*"
required
>
</div>
<!-- Filename Override -->
<div class="form-group">
<label for="filename">Filename (optional override)</label>
<input type="text" id="filename" name="filename" [(ngModel)]="filename">
</div>
<!-- Tags -->
<div class="form-group">
<label for="tags">Tags (comma-separated)*</label>
<input type="text" id="tags" name="tags" [(ngModel)]="tags" required>
</div>
<!-- Custom Annotations -->
<div class="form-group custom-tags-group">
<label>Optional Custom Annotations</label>
@for(annotation of customAnnotations; track $index) {
<div class="custom-tag-pair">
<input type="text" placeholder="Key {{$index + 1}}" name="custom_key_{{$index}}" [(ngModel)]="annotation.key">
<input type="text" placeholder="Value {{$index + 1}}" name="custom_value_{{$index}}" [(ngModel)]="annotation.value">
</div>
}
</div>
<!-- Upload Button -->
<button type="submit" [disabled]="!selectedFile">Upload</button>
<!-- Progress Bar and Messages -->
@if (uploadProgress !== null) {
<div class="progress-bar-container">
<div class="progress-bar" [style.width.%]="uploadProgress"></div>
</div>
}
@if (uploadMessage) {
<p class="message" [class.error]="isError">{{ uploadMessage }}</p>
}
</form>
</div>
This is really just a bunch of HTML elements for entering:
- A file
- An optional filename override
- Comma-separated tags
- Three optional custom tags
- A button that "submits" the form
- A progress bar
- A message
Note that the submit button doesn't actually submit the form in the traditional manner; rather, it will call some typescript code we'll be providing next. You can see this through the ngSubmit attribute of the form element:
(ngSubmit)="onUpload()"
The TypeScript Code-behind
Now let's add in the TypeScript code. Here is the full thing:
import { Component, inject } from "@angular/core"
import { CommonModule } from "@angular/common"
import {
HttpClient,
HttpClientModule,
HttpEventType,
HttpResponse,
} from "@angular/common/http"
import { FormsModule } from "@angular/forms"
@Component({
selector: "app-image-upload",
standalone: true,
imports: [CommonModule, FormsModule, HttpClientModule],
templateUrl: "./image-upload.html",
styleUrls: ["./image-upload.css"],
})
export class ImageUploadComponent {
private http = inject(HttpClient)
private backendUrl = "http://localhost:3000"
// --- Form Data Properties ---
public selectedFile: File | null = null
public filename: string = ""
public tags: string = "landscape, nature, sunset"
// Use an array for custom tags for easier management
public customAnnotations = [
{ key: "", value: "" },
{ key: "", value: "" },
{ key: "", value: "" },
]
// --- UI State Properties ---
public uploadProgress: number | null = null
public uploadMessage: string | null = null
public isError: boolean = false
/**
* Handles the file selection from the input element.
* @param event The file input change event.
*/
onFileSelected(event: Event): void {
const element = event.target as HTMLInputElement
if (element.files && element.files.length > 0) {
this.selectedFile = element.files[0]
this.uploadMessage = null // Clear previous messages
this.uploadProgress = null
}
}
/**
* Constructs FormData and sends it to the backend on upload.
*/
onUpload(): void {
if (!this.selectedFile) {
this.uploadMessage = "Please select a file to upload."
this.isError = true
return
}
this.uploadProgress = 0
this.isError = false
this.uploadMessage = null
const formData = new FormData()
formData.append("imageFile", this.selectedFile, this.selectedFile.name)
// Append optional and required fields only if they have content
if (this.filename.trim()) {
formData.append("filename", this.filename.trim())
}
if (this.tags.trim()) {
formData.append("tags", this.tags.trim())
}
// Append custom annotations that have both a key and a value
this.customAnnotations.forEach((annotation, index) => {
if (annotation.key.trim() && annotation.value.trim()) {
formData.append(`custom_key${index + 1}`, annotation.key.trim())
formData.append(
`custom_value${index + 1}`,
annotation.value.trim()
)
}
})
const apiUrl = `${this.backendUrl}/upload`
this.http
.post(apiUrl, formData, {
reportProgress: true,
observe: "events",
})
.subscribe({
next: (event) => {
if (event.type === HttpEventType.UploadProgress) {
this.uploadProgress = Math.round(
100 * (event.loaded / (event.total || 1))
)
} else if (event instanceof HttpResponse) {
this.uploadMessage = "Upload successful!"
this.selectedFile = null // Clear file input after success
}
},
error: (err) => {
this.uploadMessage = "Upload failed. Please try again."
this.isError = true
this.uploadProgress = null
console.error("Upload error:", err)
},
})
}
}
Much of this is boilerplate TypeScript code. Notice that when the file is selected, we grab the file information, and we clear out the progress bar and messages.
Then comes the onUpload method. We create a FormData instance, and we add everything in. Then we make the call to the backend's POST /upload
route, sending out the packaged up FormData.
Now just to make the UI nice, we subscribe to events from the upload process so that we can advance the progress bar. Because the main scope of this documentation is the Golem-Base information, we won't go into much detail here. If you're curious how this works, go ahead and ask ChatGPT or similar about "HttpEventType.UploadProgress".
The CSS
Finally, here's the CSS. We won't do any code walkthroughs here; we'll just present it as-is:
:host {
display: block;
font-family: sans-serif;
color: #333;
}
.upload-container {
max-width: 600px;
margin: 2em auto;
padding: 2em;
border: 1px solid #e0e0e0;
border-radius: 12px;
background-color: #f9f9f9;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
h2 {
text-align: center;
margin-bottom: 1.5em;
color: #0056b3;
}
.upload-form {
display: flex;
flex-direction: column;
gap: 1.5em;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5em;
}
.form-group label {
font-weight: 600;
font-size: 0.9em;
}
.form-group input[type="text"],
.form-group input[type="file"] {
padding: 12px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 8px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-group input[type="text"]:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.2);
}
.custom-tags-group {
gap: 1em;
}
.custom-tag-pair {
display: flex;
gap: 10px;
}
.custom-tag-pair input {
flex: 1;
}
button[type="submit"] {
padding: 12px 20px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
border: none;
background-color: #007bff;
color: white;
border-radius: 8px;
transition: background-color 0.2s, transform 0.1s;
}
button[type="submit"]:hover:not(:disabled) {
background-color: #0056b3;
transform: translateY(-1px);
}
button[type="submit"]:disabled {
background-color: #a0c7e4;
cursor: not-allowed;
}
.progress-bar-container {
width: 100%;
height: 8px;
background-color: #e9ecef;
border-radius: 4px;
overflow: hidden;
margin-top: 1em;
}
.progress-bar {
height: 100%;
background-color: #28a745;
transition: width 0.4s ease;
}
.message {
text-align: center;
margin-top: 1em;
font-weight: 600;
}
.message.error {
color: #dc3545;
}
Next let's build the Thumbnail Gallery component!
Head to Step 6.