Part 2 of the 3 part series showing a real-world application of Signal forms. Now the addition of images, since a picture can say more than a thousand words.
Like the first article (adding a rich text editor to Angular Signal Forms), again the strict implementation route is followed:
- Requirements gathering from business
- Interface composition
- Object initial state
- Form validation schema
- Services
- Form components
1. Requirements gathering
The first series article provided all requirements. Reading carefully images can be added in 2 seperate places:
- As a required main image for an article.
- As an optional image for each chapter added to the article.
2. Interface composition
export interface Image {
imageUrl: string;
imageFile: File | null;
}Where the imageUrl is the location once the image is uploaded to the database/filesystem or a temporary placeholder when uploading the image. This image can then be added to a news item thus creating a first simple Article model.v
export interface ArticleItem extends NewsItem {
image: Image;
}
export interface Article extends News, ArticleItem;3. Object initial state
export const imageInitialState: Image = {
imageUrl: '',
imageFile: null,
};And add this to the articleInitialState.
export const articleInitialState: Article = {
id: '',
title: '',
excerpt: '',
categoryID: '',
updatedAt: new Date(),
text: '',
createdAt: new Date(),
status: Status.Concept,
image: imageInitialState,
}4. Form validation schema
Start by creating a simple schema which makes the imageUrl required.
export const imageSchema = schema<Image>((schemaPath) => {
required(schemaPath.imageUrl,
{ message: 'Please upload an image. It is required !'});
});✨ The required Article main image
Creating the imageSchema isn't enough, it must be applied for the article's main image ensuring the article's form validity only if the image (through its URL) is provided.
export const articleSchema = schema<Article>((schemaPath) => {
// All article validation rules like the newsSchema
...
// apply the imageSchema, making the article's main image required.
apply(schemaPath.image, imageSchema);
});Creating an object with an optional image? Don't apply the imageSchema to the image attribute.
5. Services
How to implement the ImageService and API is dependent on your environment. Unless you want to keep previously used images implementation of the create and delete API methods will suffice. Updating an image becoming out with the old, in with the new.
✨ 6. Form components: the reusable image form
Again up till now pretty straightforward. Now for the real magic: creating a reusable image child form.
The image form component needs 3 attributes:
- The image child form as retrieved from the Article parent form.
- A default placeholder image shown to the user
- A container for the uploaded image File
and 2 methods:
- Handling of the image file selection event
- Optional deletion of the uploaded image (fallback to placeholder image), only when the image is not required!
export class ImageForm {
image = model.required<FieldTree<Image>>();
imageSrc = signal<any>('/images/placeholder.jpg');
private uploadedFile!: File;
onFileSelection($event: Event) {
const input = $event.target as HTMLInputElement;
if(input.files && input.files[0]) {
const file = input.files[0];
this.uploadedFile = file;
const reader = new FileReader();
reader.onload = (e) => {
this.imageSrc.update(() => e.target?.result);
this.image().imageUrl().value.set(this.imageSrc());
this.image().imageFile().value.set(this.uploadedFile);
};
reader.readAsDataUrl(file);
}
}
delete() {
this.image().imageUrl().value.set('');
this.image().imageFile().value.set(null);
this.imageSrc.set('/images/placeholder.jpg');
}
}How to implement the image form is up to you, as long as you implement the (optional) required requirement correctly, hiding the delete button for required images. An example how the form could look like (without paying any attention on formatting):
@let state = image()();
@if (state.touched() && state.errors().length) {
<div class="flex flex-row items-center">
<mat-icon class="text-red">warning</mat-icon>
@for (error of state.errors(); track $index) {
<span>{{ error.message }}</span>
}
</div>
}
<img [src]="image().imageUrl().value().length() > 0 ?
image().imageUrl().value() : imageSrc() />
<input
type="file"
hidden
#fileInput
(change)="onFileSelection($event)"
accept="image/*"
/>
<button (click)="fileInput.click()">upload image</button>
<button
(click)="delete()"
[class]="{ invisible : !image().imageUrl().value() ||
image().imageUrl().required() }"
></button>⚠️ Usage in the parent form
Where in the ArticleForm component the form model and schema are created, in the template the ImageForm is used directly. That's really all there is to it. If the save button is disabled while the article form is invalid, it will remain disabled as long as no image is uploaded.
export class ArticleForm {
private articleModel = signal<Article>(articleInitialState);
protected readonly articleForm = form(this.articleModel, articleSchema);
...
}
<image-form [image]="articleForm.image" />Conclusions
Creating a reusable image uploader for Signal Forms is pretty straightforward. Probably the hardest parts are the template (implementing the correct UX) and the API.