How to handle images in sealgen forms

This tutorial will guide you through adding image / photo inputs to your form and then saving them as parts of a sealious collection.

1. Create a sealious app (if you haven’t already)

git clone http://hub.sealcode.org/diffusion/PLAY/sealious-playground.git my-app
cd my-app
npm install
docker-compose up -d

2. Start the app

npm run watch

Go to localhost:8080 in your browser and confirm that the application is working

3. Add a collection that contains image fields

In a separate container, run npx sealgen add collection. It will ask you about the name of the collection - let’s name it photos:

image

Then, edit open the src/back/collections/photos.ts in your IDE and create two fields: photo and description:

// src/back/collections/photos.ts
import { Collection, FieldTypes, Policies } from "sealious";

export default class Photos extends Collection {
	fields = {
		photo: new FieldTypes.Image(),
		description: new FieldTypes.Text(),
	};
	defaultPolicy = new Policies.Public();
}

4. Add a form with an image input

Run npx sealgen add-route, and add an add photo route. Make it a form:

Then, go to the src/back/routes/add-photo.form.ts file and make some modifications.

Change the fields

Change the const fields value to the following:

const fields = {
	photo: new Fields.File(true),
	description: new Fields.SimpleFormField(true),
};

This is going to register the fields in the form, but it’s not going to change how what inputs the form displays to the user. We change that by setting the `controllers property of the class:

import { imageRouter } from "../image-router.js";
///...

export default new (class SomeFormForm extends Form<typeof fields, void> {
//...
	controls = [
		new Controls.Photo(fields.photo, imageRouter),
		new Controls.SimpleInput(fields.description),
	];

	async validateValues(): Promise<{ valid: boolean; error: string }> {
		// no validation for now
		return { valid: true, error: "" };
	}

Now, visit the form to see the effect:

If you fill the form and submit it…

image

…not much is going to happen yet. Now it’s time to do something with the values provided by the user.

5. Process the submitted values

With the onSubmit handler, we can run any logic we want with the provided values. Here, we’re going to add a new item to the photos collection we created in step 3.

//...
import { hasShape, predicates } from "@sealcode/ts-predicates";
import {File as SealiousFile} from "sealious";

//...
export default new (class SomeFormForm extends Form<typeof fields, void> {
//...
	async onSubmit(ctx: Context) {
		const data = await this.getParsedValues(ctx);
		const photo = data.photo;
		if (
			!hasShape(
				{
					new: predicates.maybe(predicates.instanceOf(SealiousFile)),
					old: predicates.maybe(predicates.instanceOf(SealiousFile)),
				},
				photo
			)
		) {
			throw new Error("Expected 'photo' to be a file");
		}
		await ctx.$app.collections.photos.create(ctx.$context, {
			photo: photo.new,
			description: data.description,
		});
		return;
	}

Now when you go to http://localhost:8080/add-photo/, fill the form, and press submit, a new item is going to be added to the photos collection. You can see the newly added items by visiting http://localhost:8080/api/v1/collections/photos?format[photo]=url

Summary

And that’s it! We’ve added a form that handles image upload to store it in a field of a sealious collection. Here’s the full content of the photos collection file and of the form itself, for reference:

// src/back/collections/photos.ts
import { Collection, FieldTypes, Policies } from "sealious";

export default class Photos extends Collection {
	fields = {
		photo: new FieldTypes.Image(),
		description: new FieldTypes.Text(),
	};
	defaultPolicy = new Policies.Public();
}
// src/back/routes/add-photo.form.ts

import type { Context } from "koa";
import type { FormData } from "@sealcode/sealgen";
import { Form, Fields, Controls, fieldsToShape } from "@sealcode/sealgen";
import html from "../html.js";
import { imageRouter } from "../image-router.js";
import { hasShape, predicates } from "@sealcode/ts-predicates";
import { File as SealiousFile } from "sealious";

export const actionName = "SomeForm";

const fields = {
	photo: new Fields.File(true),
	description: new Fields.SimpleFormField(true),
};

export const SomeFormShape = fieldsToShape(fields);

export default new (class SomeFormForm extends Form<typeof fields, void> {
	defaultSuccessMessage = "Formularz wypełniony poprawnie";
	fields = fields;

	controls = [
		new Controls.Photo(fields.photo, imageRouter),
		new Controls.SimpleInput(fields.description),
	];

	async validateValues(): Promise<{ valid: boolean; error: string }> {
		return { valid: true, error: "" };
	}

	// eslint-disable-next-line @typescript-eslint/no-unused-vars
	async canAccess(_: Context) {
		return { canAccess: true, message: "" };
	}

	async onSubmit(ctx: Context) {
		const data = await this.getParsedValues(ctx);
		const photo = data.photo;
		if (
			!hasShape(
				{
					new: predicates.maybe(predicates.instanceOf(SealiousFile)),
					old: predicates.maybe(predicates.instanceOf(SealiousFile)),
				},
				photo
			)
		) {
			throw new Error("Expected 'photo' to be a file");
		}
		await ctx.$app.collections.photos.create(ctx.$context, {
			photo: photo.new,
			description: data.description,
		});
		return;
	}

	async render(ctx: Context, data: FormData, show_field_errors: boolean) {
		return html(ctx, "SomeForm", await super.render(ctx, data, show_field_errors));
	}
})();

1 Like