Forms
Forms are used to create or edit content in Minoro, you can define forms to do virtually anything from calling your own API, saving to a DB and much more.
Defining Forms
To define forms in Minoro, you first need a view in which the form will be displayed. You can then use the defineForm method to create a form within that view.
Let's create a very simple view with a form.
This form will allow users to create a new post with a title and content.
import { defineView } from 'minoro'
const PostCreate = z.object({ title: z.string(), content: z.string() })
export const formView = defineView(
{ name: 'Form view', icon: 'lucide:info' },
(view) => {
view.defineForm({
name: 'New Post',
input: PostCreate,
jsonSchema: z.toJsonSchema(PostCreate),
render: async () => {
return {}
},
handler: async ({ fields }) => {
await sql`
INSERT INTO posts (title, content)
VALUES (${fields.title}, ${fields.content})
`.execute()
},
})
},
)Forms need a few things to be defined:
- A name: This is the name of the form, which will be displayed in the admin panel.
- An input schema: This is a standard schema (e.g. Zod, Valibot, etc.) that defines the structure of the form input. This schema is used to validate the form input and should represent the structure of an object.
- A JSON schema: This is a JSON schema that defines the structure of the form fields. This schema is used to generate the form fields in the frontend.
- An optional render function: This function is called on every request of the form and can be used to provide additional metadata for the form fields, such as field types, descriptions, or default values.
- A handler function: This function is called when the form is submitted. You can access the form fields and the view's parameters (if any) from this function. This is where you would typically handle the form submission, such as saving the data to a database or calling an API.
Default types of fields
The default fields types that are handled by the frontend are: text, number, booleans (rendered as a switch) and enums (rendered as a select).
Handling Special Field Types & Default Values
Sometimes you might want to handle different content types in your forms, such as assets, dates or rich text fields (i.e. markdown content). You can do this by using the render function of the form to provide additional metadata for the form fields.
You can also use this function to provide default values for the form fields, which will be used to prepopulate the form when it is rendered.
import { defineView } from 'minoro'
const PostCreate = z.object({ title: z.string(), content: z.string() })
export const formView = defineView(
{ name: 'Form view', icon: 'lucide:info' },
(view) => {
view.defineForm({
name: 'New Post',
input: PostCreate,
jsonSchema: z.toJsonSchema(PostCreate),
render: async () => {
return {
fields: {
content: {
type: 'markdown', // this will render a rich text editor and a code editor (with vim mode capabilities) instead of a standard text input//
},
},
defaultValues: {
title: 'Default title',
},
},
},
handler: async ({ fields }) => {
await sql`
INSERT INTO posts (title, content)
VALUES (${fields.title}, ${fields.content})
`.execute()
},
})
},
)Handling Dates
You may want to handle dates in your forms, for example to create a post with a publication date.
You could define a schema such as:
const PostCreate = z.object({
title: z.string(),
content: z.string(),
publicationDate: z.date(),
})IMPORTANT
This would not work! Since the frontend needs a JSON schema to generate the form fields, you cannot simply use a date type in the schema since Date is not a valid JSON type.
In the case of dates, you would have to define two schemas, one for the input and one for the JSON schema where the date is represented as an integer (timestamp in seconds or milliseconds):
const PostCreate = z.object({
title: z.string(),
content: z.string(),
publicationDate: z.date(),
})
const PostCreateJsonSchema = PostCreate.extend({
publicationDate: z.int()
})Then you have to use the render function to provide the field type of date:
import { defineView } from 'minoro'
import { z } from 'zod'
export const formView = defineView(
{ name: 'Form view', icon: 'lucide:info' },
(view) => {
view.defineForm({
name: 'New Post',
input: PostCreate,
jsonSchema: z.toJsonSchema(PostCreate),
jsonSchema: z.toJsonSchema(PostCreateJsonSchema),
render: async () => {
return {
fields: {
publicationDate: {
/**
* This **must** be specified as a `date` type, otherwise the field will be a standard number input
* and the `fields.publicationDate` will not be converted into a `Date` object.
*/
type: 'date', // this will render a date picker
},
},
defaultValues: {
// You still populate the date field with a `Date` object, but the frontend will convert it to a timestamp when it renders the form
publicationDate: new Date(), // This will prepopulate the date picker with the current date
},
},
},
handler: async ({ fields }) => {
// You are getting a `Date` object here because the timestamp is converted before the form is submitted
console.log(fields.publicationDate) // <-- `Date` object
},
})
},
)Advanced Field Types
type FieldTypes =
// Code editor with vim mode capabilities + a rich text editor
| { type: 'markdown' }
// Code editor with vim mode capabilities and json syntax highlighting
| { type: 'json' }
// Asset picket, more info in the Assets documentation
| {
type: 'asset'
/** The ID of the view that contains the assets */
fromView: string
/** The ID of the gallery that contains the assets */
fromGallery: string
}
// A date picker
| { type: 'date' }
// A select input with a set of options, enums are supported by default without having to manually specify the options but it can be useful to specify options manually in some cases like SQL relations.
| { type: 'enum'; options: string[] | { label: string; value: string }[] }Form Options
type Options = {
/**
* A unique name for this form
*/
name: string
/**
* A Standard Schema V1 compliant schema (zod, valibot, etc.) that defines how to validate the form input.
* This should represent the structure of an object.
*/
input: unknown
/**
* JSON schema for the form. This is used to generate the form fields.
* It should be a valid JSON schema object.
*/
jsonSchema: Record<string, unknown>
/**
* Function that is called on every request of the form.
* Can be used to provide additional metadata for the form fields,
* such as field types, descriptions, or default values.
*/
render?: () => MaybePromise<RenderResult>
/**
* A handler function that is called when the form is submitted.
* You can access the form fields and the view's parameters (if any) from this function.
*/
handler: (args: { fields: Output; params: Params }) => MaybePromise<void>
}Tips
Generating schema from your ORM
Some ORMS and third-party libraries provide utilities to generate Standard Schema compatible schemas from your database tables or models. This can significantly reduce the amount of boilerplate code you have to write to define forms.
If you are using Drizzle ORM for example, you can use drizzle-zod, drizzle-valibot or drizzle-arktype.
import { defineView } from 'minoro'
import { z } from 'zod'
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
import { db, schema } from '../db'
const PostSchema = z.object({
...createSelectSchema(schema.posts).shape,
// ... add any additional fields you want to include in the form
// !!! Be sure to override any non JSON compatible type like `Date`
})
const formView = defineView(
{ name: 'Form view', icon: 'lucide:info' },
(view) => {
view.defineForm({
name: 'New Post',
input: PostSchema,
jsonSchema: z.toJsonSchema(PostSchema),
render: async () => {
return {}
},
handler: async ({ fields }) => {
await db
.insert(schema.posts)
.values(fields)
},
})
},
)