Skip to content

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.